完成 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
+239
View File
@@ -0,0 +1,239 @@
import React from 'react';
import HashNavigation from '../lib/navigation.js';
const navigate = view => HashNavigation.navigateTo(view);
const DEMO_CONVERSATIONS = [
{id: 'system', title: '系统消息', summary: '您有一条新的安全通知', time: '昨天 21:03', icon: 'notifications', tone: 'orange', system: true},
{id: 'sun', title: '孙光洪', summary: '最近忙什么', time: '昨天 21:03', unread: 3, avatar: 'img/bkg/p03.jpg', online: true},
{id: 'files', title: '文件助手', summary: '你上传了 1 份文件', time: '昨天 21:03', icon: 'folder', tone: 'green', fileAssistant: true},
{id: 'zhao', title: '赵吾行', summary: '好久不见,怎样了,都挺好的', time: '昨天 21:03', unread: 3, avatar: 'img/bkg/p07.jpg'},
{id: 'wang', title: '王厚哲', summary: '恭喜你呀', time: '昨天 21:03', unread: 3, avatar: 'img/bkg/p12.jpg', online: true},
{id: 'sunmu', title: '孙牧森', summary: '真有意思', time: '昨天 21:03', muted: true, avatar: 'img/bkg/p18.jpg'}
];
const FREQUENT_CONTACTS = [
{id: 'a', name: '吃火锅', avatar: 'img/bkg/p02.jpg', online: true},
{id: 'b', name: '爱上草莓', avatar: 'img/bkg/p05.jpg', online: true},
{id: 'c', name: '好久鼻尖', avatar: 'img/bkg/p09.jpg'},
{id: 'd', name: '太想念你', avatar: 'img/bkg/p14.jpg', online: true},
{id: 'e', name: '夏纳', avatar: 'img/bkg/p21.jpg'}
];
const SYSTEM_NOTIFICATIONS = [
{
id: 'device-1', date: '6月28日 10:04', title: '新设备登录成功',
loginAt: '2026/08/24 14:17', device: 'Chrome · macOS', ip: '220.198.***.212', read: false
},
{
id: 'device-2', date: '6月30日 13:04', title: '新设备登录成功',
loginAt: '2026/08/20 09:42', device: 'iPhone 15', ip: '116.25.***.86', read: true
}
];
const QUICK_ACTIONS = [
{id: 'friends', icon: 'group', label: '朋友'},
{id: 'profile', icon: 'person_outline', label: '我的'},
{id: 'create-group', icon: 'add_comment', label: '创建群组'},
{id: 'add-friend', icon: 'person_add_alt', label: '添加好友'},
{id: 'scan', icon: 'qr_code_scanner', label: '扫一扫'}
];
const MESSAGE_ACTIONS = [
{id: 'quote', icon: 'format_quote', label: '引用'},
{id: 'copy', icon: 'content_copy', label: '复制'},
{id: 'favorite', icon: 'inventory_2', label: '收藏'},
{id: 'translate', icon: 'translate', label: '翻译'},
{id: 'forward', icon: 'reply', label: '转发'},
{id: 'recall', icon: 'undo', label: '撤回'},
{id: 'delete', icon: 'delete_outline', label: '删除'},
{id: 'select', icon: 'select_all', label: '多选'},
{id: 'receipts', icon: 'published_with_changes', label: '群回执'}
];
function Icon({children}) {
return <i className="material-icons" aria-hidden="true">{children}</i>;
}
function Toast({message, onDone}) {
React.useEffect(() => {
if (!message) return undefined;
const timer = window.setTimeout(onDone, 2200);
return () => window.clearTimeout(timer);
}, [message, onDone]);
return message ? <div className="messages-toast" role="status">{message}</div> : null;
}
function Avatar({item, small = false}) {
const className = 'messages-avatar ' + (small ? 'small ' : '') + (item.tone || 'blue');
return <div className={className}>
{item.avatar ? <img src={item.avatar} alt="" /> : <Icon>{item.icon || 'person'}</Icon>}
{item.online !== undefined ? <span className={item.online ? 'online' : 'offline'} /> : null}
</div>;
}
function normalizeConversations(chatList, myUserId) {
if (!chatList || chatList.length === 0) return DEMO_CONVERSATIONS;
return chatList.map((item, index) => {
const latest = typeof item.latestMessage === 'function' ? item.latestMessage() : null;
let summary = item.private && item.private.comment ? item.private.comment : '暂无消息';
if (latest && typeof latest.content === 'string') summary = latest.content;
if (latest && latest.from === myUserId) summary = '我:' + summary;
return {
id: item.topic || item.user || String(index),
title: item.public && item.public.fn ? item.public.fn : '未命名会话',
summary,
time: item.touched ? new Date(item.touched).toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'}) : '',
unread: Math.max(Number(item.unread) || 0, 0),
online: !!item.online,
muted: !!(item.acs && item.acs.isMuted && item.acs.isMuted()),
pinned: !!item.pinned
};
});
}
function BottomNavigation() {
return <nav className="messages-bottom-nav" aria-label="主导航">
<button type="button" onClick={() => navigate('workbench')}><Icon>dashboard</Icon><span>工作台</span></button>
<button type="button" className="active"><Icon>person</Icon><span>消息</span></button>
</nav>;
}
function QuickMenu({onClose, onAction}) {
return <div className="messages-menu-backdrop" onMouseDown={onClose}>
<div className="messages-quick-menu" onMouseDown={e => e.stopPropagation()} role="menu">
{QUICK_ACTIONS.map(item => <button type="button" role="menuitem" key={item.id} onClick={() => onAction(item)}>
<Icon>{item.icon}</Icon><span>{item.label}</span>
</button>)}
</div>
</div>;
}
function MessageHome({view, connected, chatList, myUserId, onTopicSelected}) {
const [query, setQuery] = React.useState('');
const [menuOpen, setMenuOpen] = React.useState(view === 'messages-menu');
const [toast, setToast] = React.useState('');
const offline = view === 'messages-offline' || (!!myUserId && connected === false);
const conversations = normalizeConversations(chatList, myUserId);
const filtered = conversations.filter(item => `${item.title}${item.summary}`.toLowerCase().includes(query.trim().toLowerCase()));
const selectConversation = item => {
if (item.system || item.id === 'system') {
navigate('system-messages');
} else if (chatList && chatList.length && onTopicSelected) {
onTopicSelected(item.id);
} else {
navigate('chat-standard');
}
};
const runQuickAction = item => {
setMenuOpen(false);
if (item.id === 'friends') return navigate('address-book');
if (item.id === 'profile') return navigate('personal-center');
if (item.id === 'create-group') return navigate('group-list');
if (item.id === 'add-friend') return navigate('add-friend');
setToast(item.id === 'scan' ? '扫码功能将在客户端能力章节提供' : `${item.label}页面将在对应章节中提供`);
};
return <div className="messages-product-page">
<header className="messages-home-header">
{offline ? <div className="messages-connection"><span className="messages-spinner" />加载中</div> : <h1>消息</h1>}
<button type="button" className="messages-menu-button" onClick={() => setMenuOpen(true)} aria-label="更多功能"><Icon>menu</Icon></button>
</header>
<main className="messages-home-content">
<label className="messages-search"><Icon>search</Icon><input value={query} onChange={e => setQuery(e.target.value)} placeholder="搜索" /></label>
<section className="messages-frequent">
<h2>常联系的人</h2>
<div className="messages-frequent-scroll">
{FREQUENT_CONTACTS.map(item => <button type="button" key={item.id} onClick={() => navigate('chat-standard')}>
<Avatar item={item} small /><span>{item.name}</span>
</button>)}
</div>
</section>
<section className="messages-conversations" aria-label="会话列表">
{filtered.map(item => <button type="button" className={'messages-conversation' + (item.pinned ? ' pinned' : '')}
key={item.id} onClick={() => selectConversation(item)}>
<Avatar item={item} />
<span className="messages-conversation-copy"><strong>{item.title}</strong><small>{item.summary}</small></span>
<span className="messages-conversation-meta"><time>{item.time}</time>
{item.unread ? <b>{item.unread > 99 ? '99+' : item.unread}</b> : item.muted ? <Icon>notifications_off</Icon> : null}
</span>
</button>)}
{filtered.length === 0 ? <div className="messages-empty"><Icon>search_off</Icon><p>未找到相关会话</p></div> : null}
</section>
</main>
<BottomNavigation />
{menuOpen ? <QuickMenu onClose={() => setMenuOpen(false)} onAction={runQuickAction} /> : null}
<Toast message={toast} onDone={() => setToast('')} />
</div>;
}
function MessageActions() {
const [visible, setVisible] = React.useState(true);
const [toast, setToast] = React.useState('');
const [selected, setSelected] = React.useState('');
const runAction = item => {
setSelected(item.id);
if (item.id === 'copy' && navigator.clipboard) {
navigator.clipboard.writeText('明天上午十点开项目会,请准时参加。').catch(() => {});
}
setToast(`${item.label}操作已选择`);
setVisible(false);
};
return <div className="message-actions-page">
<header className="messages-sub-header"><button onClick={() => navigate('messages-home')} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button><h1>项目协作群</h1><span /></header>
<main className="message-actions-chat">
<p className="message-time">今天 10:24</p>
<div className="message-demo-row"><Avatar item={{avatar: 'img/bkg/p09.jpg'}} /><button type="button" className="message-demo-bubble" onContextMenu={e => {e.preventDefault(); setVisible(true);}}>明天上午十点开项目会,请准时参加。</button></div>
<div className="message-demo-row mine"><button type="button" className="message-demo-bubble mine" onContextMenu={e => {e.preventDefault(); setVisible(true);}}>收到,我会提前准备材料。</button><Avatar item={{avatar: 'img/bkg/p05.jpg'}} /></div>
{selected ? <p className="message-operation-result">已选择:{MESSAGE_ACTIONS.find(item => item.id === selected).label}</p> : null}
</main>
<footer className="message-demo-composer"><Icon>mic_none</Icon><span>输入消息</span><Icon>sentiment_satisfied_alt</Icon><Icon>add_circle_outline</Icon></footer>
{visible ? <div className="message-action-backdrop" onMouseDown={() => setVisible(false)}>
<div className="message-action-menu" onMouseDown={e => e.stopPropagation()} role="menu">
{MESSAGE_ACTIONS.map(item => <button type="button" role="menuitem" key={item.id} onClick={() => runAction(item)}>
<span>{item.label}</span><Icon>{item.icon}</Icon>
</button>)}
</div>
</div> : null}
<Toast message={toast} onDone={() => setToast('')} />
</div>;
}
function SystemMessages({authenticated}) {
const [items, setItems] = React.useState(SYSTEM_NOTIFICATIONS);
const [toast, setToast] = React.useState('');
const markRead = id => setItems(items.map(item => item.id === id ? {...item, read: true} : item));
return <div className="system-messages-page">
<header className="messages-sub-header"><button onClick={() => navigate('messages-home')} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button><h1>系统消息</h1><span /></header>
<main className="system-notification-list">
{items.map(item => <article key={item.id} className={item.read ? 'read' : ''} onClick={() => markRead(item.id)}>
<time>{item.date}</time>
<div className="system-notification-card">
<header><h2>{item.title}</h2>{!item.read ? <span>新</span> : null}</header>
<p>亲爱的用户,您的账号于 <button type="button" onClick={() => setToast('登录时间:' + item.loginAt)}>{item.loginAt}</button> 在新设备上登录成功。</p>
<dl><div><dt>设备:</dt><dd>{item.device}</dd></div><div><dt>IP地址:</dt><dd>{item.ip}</dd></div></dl>
<p>如果不是本人操作,请及时修改密码并检查登录设备。</p>
<button type="button" className="system-security-link" onClick={() => navigate(authenticated ? 'security' : 'identity-check')}>前往安全设置 <Icon>chevron_right</Icon></button>
<footer>保持警惕<br />即时通讯安全团队</footer>
</div>
</article>)}
</main>
<Toast message={toast} onDone={() => setToast('')} />
</div>;
}
export default function MessageCenterView(props) {
switch (props.view) {
case 'system-messages': return <SystemMessages authenticated={!!props.myUserId} />;
case 'message-actions': return <MessageActions />;
case 'messages-offline':
case 'messages-menu':
case 'messages-home':
default: return <MessageHome {...props} />;
}
}