完成 IM Web Android 端产品页面开发
This commit is contained in:
@@ -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} />;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user