完成 IM Web Android 端产品页面开发

This commit is contained in:
richjohnrambo
2026-08-25 18:30:40 +08:00
parent ca5e74ee98
commit ba8f48ee83
19 changed files with 20783 additions and 54 deletions
+134
View File
@@ -0,0 +1,134 @@
import React from 'react';
import HashNavigation from '../lib/navigation.js';
const navigate = view => HashNavigation.navigateTo(view);
export const WORKPLACE_COLLABORATION_VIEWS = [
'workbench', 'organization-directory', 'meeting-organizer', 'meeting-attendee',
'daily-report-editor', 'daily-report-mine', 'daily-report-team', 'daily-report-detail',
'monthly-report-editor', 'monthly-report-list', 'monthly-report-detail',
'executive-mailbox', 'executive-mailbox-notice'
];
const APPS = [
{code: 'monthly', name: '月报', icon: 'event_note', tone: 'violet', route: 'monthly-report-list'},
{code: 'daily', name: '日报', icon: 'task_alt', tone: 'orange', route: 'daily-report-mine', badge: 1},
{code: 'organization', name: '组织架构', icon: 'account_tree', tone: 'red', route: 'organization-directory'},
{code: 'meeting', name: '会议通知', icon: 'notifications_active', tone: 'blue', route: 'meeting-attendee', badge: 2},
{code: 'mailbox', name: '总经理信箱', icon: 'mail_outline', tone: 'green', route: 'executive-mailbox'}
];
const PEOPLE = [
{name: '李家家', title: '财务专员', avatar: 'img/bkg/p03.jpg'},
{name: '何耐耐', title: '前端工程师', avatar: 'img/bkg/p05.jpg'},
{name: '符小小', title: '产品经理', avatar: 'img/bkg/p09.jpg'},
{name: '王子明', title: '测试工程师', avatar: 'img/bkg/p14.jpg'}
];
const DAILY_REPORTS = [
{name: '清爽的白鸽', date: '2026-08-25', status: '已提交', content: '完成工作台与组织架构页面开发,梳理会议通知交互方案;修复两个样式问题并完善产品文档。'},
{name: '轻松的小虾', date: '2026-08-25', status: '已提交', content: '梳理用户反馈,输出需求优先级清单;与设计沟通新功能交互方案。'},
{name: '轻松的雨雨', date: '2026-08-25', status: '已提交', content: '完成接口文档整理,联调日报列表与统计页面。'}
];
function Icon({children}) { return <i className="material-icons" aria-hidden="true">{children}</i>; }
function Header({title, back = 'workbench', action, actionLabel, onAction}) {
return <header className="workplace-header"><button type="button" onClick={() => navigate(back)} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button><h1>{title}</h1>{action ? <button type="button" onClick={onAction} aria-label={actionLabel || '操作'}><Icon>{action}</Icon></button> : <span />}</header>;
}
function Toast({message, onDone}) {
React.useEffect(() => {
if (!message) return undefined;
const timer = window.setTimeout(onDone, 2100);
return () => window.clearTimeout(timer);
}, [message, onDone]);
return message ? <div className="workplace-toast" role="status">{message}</div> : null;
}
function Workbench() {
return <div className="workplace-page workbench-page"><main>
<section className="workbench-profile"><img src="img/bkg/p05.jpg" alt="" /><span><strong>陈晨</strong><small>产品研发部 · 员工</small></span><button type="button" aria-label="工作台设置"><Icon>tune</Icon></button></section>
<section className="workbench-banner"><div><strong>高效协作,从工作台开始</strong><small>今天有 3 项待处理事项</small></div><Icon>business_center</Icon></section>
<section className="workbench-apps" aria-label="办公应用">{APPS.map(app => <button type="button" key={app.code} onClick={() => navigate(app.route)}><span className={app.tone}><Icon>{app.icon}</Icon>{app.badge ? <em>{app.badge}</em> : null}</span><strong>{app.name}</strong></button>)}</section>
</main><nav className="workplace-bottom-nav" aria-label="主导航"><button type="button" className="active"><Icon>dashboard</Icon><span>工作台</span></button><button type="button" onClick={() => navigate('messages-home')}><Icon>chat_bubble_outline</Icon><span>消息</span></button></nav></div>;
}
function OrganizationDirectory() {
const [query, setQuery] = React.useState('');
const [expanded, setExpanded] = React.useState(true);
const visible = PEOPLE.filter(person => `${person.name}${person.title}`.includes(query));
return <div className="workplace-page"><Header title="组织架构" /><main className="organization-content"><label className="workplace-search"><Icon>search</Icon><input value={query} onChange={event => setQuery(event.target.value)} placeholder="搜索成员或部门" /></label><section className="organization-tree"><button type="button" className="organization-root" onClick={() => setExpanded(!expanded)}><Icon>domain</Icon><span><strong>星海科技有限公司</strong><small>3 个部门 · 50 人</small></span><Icon>{expanded ? 'expand_more' : 'chevron_right'}</Icon></button>{expanded ? <><button type="button" className="organization-department"><Icon>folder_open</Icon><span><strong>产品研发部</strong><small>18 人</small></span><Icon>expand_more</Icon></button><div className="organization-members">{visible.map(person => <button type="button" key={person.name} onClick={() => navigate('group-member-profile')}><img src={person.avatar} alt="" /><span><strong>{person.name}</strong><small>{person.title}</small></span><Icon>chevron_right</Icon></button>)}</div><button type="button" className="organization-department"><Icon>folder</Icon><span><strong>财务部</strong><small>6 人</small></span><Icon>chevron_right</Icon></button><button type="button" className="organization-department"><Icon>folder</Icon><span><strong>市场销售部</strong><small>26 人</small></span><Icon>chevron_right</Icon></button></> : null}</section></main></div>;
}
function MeetingCard({organizer, response, onResponse, onToast}) {
return <article className="meeting-card"><header><span><small>今天 14:30</small><strong>产品版本评审会</strong></span><em className={response || 'pending'}>{response === 'accepted' ? '已接受' : response === 'declined' ? '已拒绝' : response === 'tentative' ? '待定' : '待确认'}</em></header><dl><div><dt><Icon>person_outline</Icon>发起人</dt><dd>李女士</dd></div><div><dt><Icon>schedule</Icon>时间</dt><dd>8月25日 14:30 - 15:30</dd></div><div><dt><Icon>location_on</Icon>地点</dt><dd>5F 海棠会议室</dd></div><div><dt><Icon>groups</Icon>参会人</dt><dd>李明、张婷、陈宇等 8 人</dd></div></dl>{organizer ? <footer><button type="button" onClick={() => onToast('已提醒 3 位未响应成员')}><Icon>notifications</Icon>提醒未响应</button><button type="button" onClick={() => onToast('会议管理面板已打开')}>管理会议</button></footer> : response ? <footer><button type="button" onClick={() => onResponse('')}>修改回应</button></footer> : <footer className="meeting-response"><button type="button" onClick={() => onResponse('declined')}>无法参加</button><button type="button" onClick={() => onResponse('tentative')}>待定</button><button type="button" className="primary" onClick={() => onResponse('accepted')}>接受</button></footer>}</article>;
}
function Meetings({organizer, onToast}) {
const [response, setResponse] = React.useState('');
const [created, setCreated] = React.useState(false);
const respond = value => {setResponse(value); if (value) onToast(value === 'accepted' ? '已接受会议邀请' : value === 'declined' ? '已拒绝会议邀请' : '已标记为待定');};
const createMeeting = () => {
if (!organizer) return navigate('meeting-organizer');
setCreated(true);
onToast('已新建会议草稿');
};
return <div className="workplace-page"><Header title="会议通知" action="add" actionLabel="发起会议" onAction={createMeeting} /><main className="meeting-list"><nav className="workplace-tabs"><button type="button" className="active">待处理</button><button type="button">已响应</button><button type="button">历史</button></nav>{created ? <article className="meeting-draft"><Icon>edit_calendar</Icon><span><strong>新会议草稿</strong><small>时间、地点和参会人待完善</small></span><button type="button" onClick={() => setCreated(false)}>继续编辑</button></article> : null}<MeetingCard organizer={organizer} response={response} onResponse={respond} onToast={onToast} />{organizer ? <section className="meeting-status"><h2>参会确认</h2><div><span><b>5</b><small>已接受</small></span><span><b>3</b><small>待响应</small></span><span><b>0</b><small>已拒绝</small></span></div></section> : null}</main></div>;
}
function ReportEditor({monthly, onToast}) {
const storageKey = monthly ? 'monthly-report-draft' : 'daily-report-draft';
const initial = (() => {try { return JSON.parse(window.localStorage.getItem(storageKey)) || {}; } catch (_) { return {}; }})();
const [summary, setSummary] = React.useState(initial.summary || '');
const [plan, setPlan] = React.useState(initial.plan || '');
const [saved, setSaved] = React.useState(false);
React.useEffect(() => {
const timer = window.setTimeout(() => {window.localStorage.setItem(storageKey, JSON.stringify({summary, plan})); setSaved(true);}, 600);
setSaved(false);
return () => window.clearTimeout(timer);
}, [summary, plan, storageKey]);
const submit = () => {
if (!summary.trim() || (monthly && !plan.trim())) return onToast('请完成必填内容');
window.localStorage.removeItem(storageKey);
onToast(monthly ? '月报已提交' : '日报已提交');
window.setTimeout(() => navigate(monthly ? 'monthly-report-detail' : 'daily-report-detail'), 650);
};
return <div className="workplace-page report-editor-page"><Header title={monthly ? '填写月报' : '填写日报'} back={monthly ? 'monthly-report-list' : 'daily-report-mine'} /><main><section className="report-period"><Icon>{monthly ? 'calendar_month' : 'today'}</Icon><span><strong>{monthly ? '2026 年 8 月' : '2026 年 8 月 25 日'}</strong><small>{saved ? '草稿已自动保存' : '正在保存草稿…'}</small></span></section><label className="report-field"><span><b>*</b>{monthly ? '本月工作总结' : '今日工作总结'}</span><textarea value={summary} maxLength="1000" onChange={event => setSummary(event.target.value)} placeholder="请输入工作内容、进展与结果" /><small>{summary.length}/1000</small></label>{monthly ? <label className="report-field"><span><b>*</b>下月工作计划</span><textarea value={plan} maxLength="1000" onChange={event => setPlan(event.target.value)} placeholder="请输入目标、计划与需要的支持" /><small>{plan.length}/1000</small></label> : <button type="button" className="report-attachment" onClick={() => onToast('已选择附件:项目进度表.xlsx')}><Icon>attach_file</Icon><span><strong>添加附件</strong><small>支持图片、文档,最多 9 个</small></span><Icon>chevron_right</Icon></button>}</main><footer className="workplace-submit-bar"><button type="button" onClick={submit}>提交</button></footer></div>;
}
function ReportList({monthly, team, onToast}) {
const [query, setQuery] = React.useState('');
const reports = DAILY_REPORTS.filter(item => `${item.name}${item.content}${item.date}`.includes(query));
return <div className="workplace-page report-list-page"><Header title={monthly ? '月报' : '日报'} action="add" actionLabel={monthly ? '填写月报' : '填写日报'} onAction={() => navigate(monthly ? 'monthly-report-editor' : 'daily-report-editor')} /><main>{team ? <section className="report-statistics"><header><span><small>2026年8月25日</small><strong>产品研发部</strong></span><button type="button" onClick={() => navigate('daily-report-mine')}>我的日报</button></header><div><span><b>18</b><small>应提交</small></span><span><b>14</b><small>已提交</small></span><span><b>4</b><small>未提交</small></span></div><button type="button" onClick={() => onToast('已提醒 4 位未提交成员')}><Icon>notifications_active</Icon>提醒未提交成员</button></section> : null}<label className="workplace-search"><Icon>search</Icon><input value={query} onChange={event => setQuery(event.target.value)} placeholder={monthly ? '搜索月份或内容' : '搜索姓名、部门或日期'} /></label>{!monthly && !team ? <nav className="report-role-switch"><button type="button" className="active">我的日报</button><button type="button" onClick={() => navigate('daily-report-team')}>团队统计</button></nav> : null}<section className="report-cards">{monthly ? ['2026 年 8 月', '2026 年 7 月', '2026 年 6 月'].map((month, index) => <button type="button" key={month} onClick={() => navigate(index ? 'monthly-report-detail' : 'monthly-report-editor')}><span className="report-month"><Icon>calendar_month</Icon></span><span><strong>{month}</strong><small>{index ? '工作总结与下月计划' : '截止 8月31日 18:00'}</small></span><em className={index ? 'submitted' : 'draft'}>{index ? '已提交' : '待填写'}</em><Icon>chevron_right</Icon></button>) : reports.map(report => <article key={report.name}><header><img src={PEOPLE[reports.indexOf(report) % PEOPLE.length].avatar} alt="" /><span><strong>{report.name}</strong><small>{team ? '产品研发部 · 员工' : report.date}</small></span><em>{report.status}</em></header><p>{report.content}</p><footer><span><Icon>schedule</Icon>{report.date}</span><button type="button" onClick={() => navigate('daily-report-detail')}>查看详情<Icon>chevron_right</Icon></button></footer></article>)}</section></main></div>;
}
function ReportDetail({monthly}) {
return <div className="workplace-page"><Header title={monthly ? '月报详情' : '日报详情'} back={monthly ? 'monthly-report-list' : 'daily-report-mine'} /><main className="report-detail"><section className="report-author"><img src="img/bkg/p05.jpg" alt="" /><span><strong>陈晨</strong><small>产品研发部 · 产品经理</small></span><em>已提交</em></section><section><h2>{monthly ? '2026 年 7 月月报' : '2026 年 8 月 25 日日报'}</h2><small>提交于 18:06 · 当前为最新版本</small></section><section><h3>{monthly ? '本月工作总结' : '今日工作总结'}</h3><p>完成企业工作台页面和办公协同模块的交互开发;梳理组织架构、会议通知与报告流程,补充 Android 端适配并完成回归验证。</p></section>{monthly ? <section><h3>下月工作计划</h3><p>推进后端接口联调,完善冲突处理、权限状态及数据加载反馈,持续跟进测试结果。</p></section> : <section className="report-file"><Icon>description</Icon><span><strong>项目进度表.xlsx</strong><small>128 KB</small></span><Icon>download</Icon></section>}<p className="report-updated"><Icon>history</Icon>最后更新于 2026-08-25 18:06</p></main></div>;
}
function ExecutiveMailbox({noticeOnly, onToast}) {
const [noticeOpen, setNoticeOpen] = React.useState(noticeOnly);
const [subject, setSubject] = React.useState('');
const [content, setContent] = React.useState('');
const [anonymous, setAnonymous] = React.useState(false);
const submit = () => {
if (!subject.trim() || !content.trim()) return onToast('请填写主题和反馈内容');
setSubject(''); setContent(''); onToast('提交成功');
};
return <div className="workplace-page mailbox-page"><Header title="总经理信箱" action="info_outline" actionLabel="查看使用说明" onAction={() => setNoticeOpen(true)} /><main><button type="button" className="mailbox-notice" onClick={() => setNoticeOpen(true)}><Icon>verified_user</Icon><span><strong>提交前请阅读隐私说明</strong><small>了解信息接收方与匿名处理规则</small></span><Icon>chevron_right</Icon></button><label><span>反馈主题</span><input value={subject} maxLength="80" onChange={event => setSubject(event.target.value)} placeholder="请简要概括问题或建议" /></label><label className="mailbox-content"><span>反馈内容</span><textarea value={content} maxLength="1000" onChange={event => setContent(event.target.value)} placeholder="请详细描述事情经过与诉求" /><small>{content.length}/1000</small></label><button type="button" className="mailbox-upload" onClick={() => onToast('已选择附件')}><Icon>upload_file</Icon><span>上传附件</span><small>图片或文档,最多 5 个</small></button><label className="mailbox-anonymous"><input type="checkbox" checked={anonymous} onChange={event => setAnonymous(event.target.checked)} /><span><strong>匿名提交</strong><small>接收人员将无法看到你的员工身份</small></span></label><button type="button" className="workplace-primary" onClick={submit}>提交反馈</button></main>{noticeOpen ? <div className="mailbox-notice-backdrop"><section role="dialog" aria-modal="true" className="mailbox-notice-dialog"><Icon>policy</Icon><h2>信箱使用与隐私说明</h2><p>本信箱用于收集员工对经营管理、工作环境和制度流程的意见。提交内容由公司授权管理人员处理,请勿填写与事项无关的敏感信息。</p><p>匿名提交时,业务接收人员不会看到你的身份。具体处理和审计范围以企业当前政策为准。</p><small>政策版本:2026.08 · 更新于 2026-08-20</small><button type="button" onClick={() => {setNoticeOpen(false); if (noticeOnly) navigate('executive-mailbox');}}>我已了解</button></section></div> : null}</div>;
}
export default function WorkplaceCollaborationView({view}) {
const [toast, setToast] = React.useState('');
let content;
if (view === 'workbench') content = <Workbench />;
else if (view === 'organization-directory') content = <OrganizationDirectory />;
else if (view === 'meeting-organizer' || view === 'meeting-attendee') content = <Meetings organizer={view === 'meeting-organizer'} onToast={setToast} />;
else if (view === 'daily-report-editor' || view === 'monthly-report-editor') content = <ReportEditor monthly={view === 'monthly-report-editor'} onToast={setToast} />;
else if (view === 'daily-report-mine' || view === 'daily-report-team' || view === 'monthly-report-list') content = <ReportList monthly={view === 'monthly-report-list'} team={view === 'daily-report-team'} onToast={setToast} />;
else if (view === 'daily-report-detail' || view === 'monthly-report-detail') content = <ReportDetail monthly={view === 'monthly-report-detail'} />;
else content = <ExecutiveMailbox noticeOnly={view === 'executive-mailbox-notice'} onToast={setToast} />;
return <>{content}<Toast message={toast} onDone={() => setToast('')} /></>;
}