完成 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
+137
View File
@@ -0,0 +1,137 @@
import React from 'react';
import QRCode from 'qrcodejs';
import HashNavigation from '../lib/navigation.js';
const navigate = view => HashNavigation.navigateTo(view);
export const CONTACT_MANAGEMENT_VIEWS = [
'address-book', 'friend-requests', 'add-friend', 'my-friend-qr', 'friend-search-result',
'friend-verification', 'friend-profile', 'group-member-profile', 'friend-settings',
'friend-settings-blocked', 'friend-delete-confirm', 'block-list', 'block-confirm',
'clear-chat-confirm'
];
const FRIENDS = [
{id: 'annie', name: '安妮', remark: '安妮 · 产品', initial: 'A', avatar: 'img/bkg/p09.jpg', account: 'AN1024'},
{id: 'chen', name: '陈晨', initial: 'C', avatar: 'img/bkg/p05.jpg', account: 'CHEN88'},
{id: 'lizi', name: '栗子来了', remark: '栗子', initial: 'L', avatar: 'img/bkg/p15.jpg', account: 'LIZI66'},
{id: 'wang', name: '王珊珊', initial: 'W', avatar: 'img/bkg/p14.jpg', account: 'WSS2026'}
];
const REQUESTS = [
{id: 'r1', name: '祈万事安好', message: '我是设计交流群里的小祈', time: '10:24', status: 'pending', avatar: 'img/bkg/p03.jpg'},
{id: 'r2', name: '一颗栗子', message: '你好,想认识一下', time: '昨天', status: 'accepted', avatar: 'img/bkg/p15.jpg'},
{id: 'r3', name: '林间风', message: '通过手机号搜索到你', time: '星期一', status: 'rejected', avatar: 'img/bkg/p18.jpg'}
];
function Icon({children}) { return <i className="material-icons" aria-hidden="true">{children}</i>; }
function Header({title, back = 'messages-home', right, onRight}) {
return <header className="people-header"><button type="button" onClick={() => navigate(back)} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button><h1>{title}</h1>{right ? <button type="button" className="text" onClick={onRight}>{right}</button> : <span />}</header>;
}
function Avatar({person, icon}) {
return <span className="people-avatar">{person && person.avatar ? <img src={person.avatar} alt="" /> : <Icon>{icon || 'person'}</Icon>}</span>;
}
function Toast({message, onDone}) {
React.useEffect(() => {
if (!message) return undefined;
const timer = window.setTimeout(onDone, 2000);
return () => window.clearTimeout(timer);
}, [message, onDone]);
return message ? <div className="chat-toast" role="status">{message}</div> : null;
}
function Search({value, onChange, placeholder}) {
return <label className="people-search"><Icon>search</Icon><input value={value} onChange={event => onChange(event.target.value)} placeholder={placeholder} /></label>;
}
function QrCard({group = false}) {
const ref = React.useRef(null);
React.useEffect(() => {
if (!ref.current) return;
ref.current.innerHTML = '';
new QRCode(ref.current, {text: group ? 'im://qr?token=demo-group-2026&purpose=join_group' : 'im://qr?token=demo-user-2026&purpose=add_friend', width: 190, height: 190, correctLevel: QRCode.CorrectLevel.M});
}, [group]);
return <section className="people-qr-card"><div className="people-qr-profile"><Avatar person={group ? {avatar: 'img/bkg/p22.jpg'} : FRIENDS[1]} /><span><strong>{group ? '产品研发群' : '陈晨'}</strong><small>{group ? '18 位成员' : '账号 CHEN88'}</small></span></div><div className="people-qrcode" ref={ref} /><p>{group ? '扫码申请加入群聊' : '扫一扫上面的二维码,加我为好友'}</p><small>二维码 24 小时内有效</small></section>;
}
function AddressBook() {
const [query, setQuery] = React.useState('');
const visible = FRIENDS.filter(item => `${item.name}${item.remark || ''}`.includes(query));
const groups = visible.reduce((result, item) => ({...result, [item.initial]: (result[item.initial] || []).concat(item)}), {});
const entries = [
{icon: 'person_add_alt', label: '新朋友', route: 'friend-requests', badge: 1, tone: 'blue'},
{icon: 'group', label: '群聊', route: 'group-list', tone: 'green'},
{icon: 'folder_special', label: '文件助手', route: 'chat-standard', tone: 'orange'},
{icon: 'notifications', label: '系统消息', route: 'system-messages', tone: 'red'}
];
return <div className="people-page"><Header title="通讯录" right="添加" onRight={() => navigate('add-friend')} /><Search value={query} onChange={setQuery} placeholder="搜索联系人" /><main className="people-scroll"><section className="people-entry-list">{entries.map(item => <button type="button" key={item.label} onClick={() => navigate(item.route)}><Avatar icon={item.icon} /><strong>{item.label}</strong>{item.badge ? <em>{item.badge}</em> : null}<Icon>chevron_right</Icon></button>)}</section>{Object.keys(groups).map(initial => <section className="people-group" key={initial}><h2>{initial}</h2>{groups[initial].map(person => <button type="button" key={person.id} onClick={() => navigate('friend-profile')}><Avatar person={person} /><span><strong>{person.remark || person.name}</strong>{person.remark ? <small>{person.name}</small> : null}</span></button>)}</section>)}</main><nav className="people-index">{Object.keys(groups).map(value => <a href={`#people-${value}`} key={value}>{value}</a>)}</nav></div>;
}
function FriendRequests({onToast}) {
const [items, setItems] = React.useState(REQUESTS);
const handle = (id, status) => {setItems(values => values.map(item => item.id === id ? {...item, status} : item)); onToast(status === 'accepted' ? '已通过好友申请' : '已拒绝好友申请');};
return <div className="people-page"><Header title="新朋友" back="address-book" /><main className="people-request-list">{items.map(item => <article key={item.id}><Avatar person={item} /><div><strong>{item.name}</strong><p>{item.message}</p><small>{item.time}</small></div>{item.status === 'pending' ? <span><button type="button" onClick={() => handle(item.id, 'rejected')}>拒绝</button><button type="button" className="primary" onClick={() => handle(item.id, 'accepted')}>通过</button></span> : <em>{item.status === 'accepted' ? '已通过' : '已拒绝'}</em>}</article>)}</main></div>;
}
function AddFriend({result = false}) {
const [query, setQuery] = React.useState(result ? 'AN1024' : '');
return <div className="people-page"><Header title="添加好友" back="address-book" /><Search value={query} onChange={setQuery} placeholder="输入账号或手机号" /><main>{query ? <section className="people-search-result"><h2>搜索结果</h2><button type="button" onClick={() => navigate('group-member-profile')}><Avatar person={FRIENDS[0]} /><span><strong>安妮</strong><small>账号 AN1024 · 产品设计组</small></span><em>添加</em></button></section> : <section className="people-add-actions"><button type="button"><Icon>qr_code_scanner</Icon><span><strong>扫一扫</strong><small>扫描好友或群二维码</small></span><Icon>chevron_right</Icon></button><button type="button" onClick={() => navigate('my-friend-qr')}><Icon>qr_code_2</Icon><span><strong>我的二维码</strong><small>让朋友扫一扫添加我</small></span><Icon>chevron_right</Icon></button><button type="button"><Icon>share</Icon><span><strong>分享好友</strong><small>发送我的个人名片</small></span><Icon>chevron_right</Icon></button></section>}<p className="people-my-account">我的账号:CHEN88</p></main></div>;
}
function QrPage({onToast}) {
return <div className="people-page people-qr-page"><Header title="我的二维码" back="add-friend" /><main><QrCard /><div className="people-qr-actions"><button type="button" onClick={() => onToast('二维码名片已分享')}><Icon>share</Icon><span>分享好友</span></button><button type="button" onClick={() => onToast('二维码已保存到相册')}><Icon>download</Icon><span>保存到相册</span></button></div></main></div>;
}
function Verification({onToast}) {
const [message, setMessage] = React.useState('我是产品研发群里的陈晨');
return <div className="people-page"><Header title="好友验证" back="group-member-profile" /><main className="people-verify"><Avatar person={FRIENDS[0]} /><h2>安妮</h2><label><span>验证留言</span><textarea value={message} maxLength="50" onChange={event => setMessage(event.target.value)} /><small>{message.length}/50</small></label><button type="button" className="people-primary" disabled={!message.trim()} onClick={() => {onToast('好友申请已发送'); window.setTimeout(() => navigate('group-member-profile'), 800);}}>发送申请</button></main></div>;
}
function Profile({friend, onToast}) {
const person = friend ? FRIENDS[0] : FRIENDS[2];
return <div className="people-page"><Header title="个人资料" back={friend ? 'address-book' : 'chat-standard'} right={friend ? '设置' : null} onRight={() => navigate('friend-settings')} /><main className="people-profile"><section><Avatar person={person} /><div><h2>{person.remark || person.name}</h2><p>{person.remark ? `昵称:${person.name}` : '产品研发群成员'}</p><p>账号:{person.account}</p></div><Icon>qr_code_2</Icon></section><dl><div><dt>性别</dt><dd>女</dd></div><div><dt>地区</dt><dd>上海 浦东新区</dd></div><div><dt>个性签名</dt><dd>保持好奇,认真生活。</dd></div></dl>{friend ? <div className="people-profile-actions"><button type="button" className="people-primary" onClick={() => navigate('chat-standard')}><Icon>chat_bubble_outline</Icon>发消息</button><button type="button" onClick={() => navigate('call-history')}><Icon>call</Icon>音视频通话</button></div> : <button type="button" className="people-primary" onClick={() => navigate('friend-verification')}><Icon>person_add_alt</Icon>添加好友</button>}</main></div>;
}
function Toggle({checked, onChange}) { return <button type="button" role="switch" aria-checked={checked} className={'people-switch' + (checked ? ' checked' : '')} onClick={() => onChange(!checked)}><i /></button>; }
function Settings({initialBlocked, onToast}) {
const [pinned, setPinned] = React.useState(false);
const [muted, setMuted] = React.useState(false);
const [blocked, setBlocked] = React.useState(initialBlocked);
return <div className="people-page"><Header title="聊天设置" back="friend-profile" /><main className="people-settings"><section className="people-settings-profile"><Avatar person={FRIENDS[0]} /><strong>安妮 · 产品</strong></section><section><button type="button" onClick={() => navigate('history-search')}><span>查找聊天记录</span><Icon>chevron_right</Icon></button><button type="button"><span>消息保存方式</span><small>永久</small><Icon>chevron_right</Icon></button></section><section><div><span>置顶聊天</span><Toggle checked={pinned} onChange={setPinned} /></div><div><span>消息免打扰</span><Toggle checked={muted} onChange={setMuted} /></div><div><span>加入黑名单</span><Toggle checked={blocked} onChange={value => value ? navigate('block-confirm') : (setBlocked(false), onToast('已移出黑名单'))} /></div></section><section><button type="button"><span>设置备注</span><Icon>chevron_right</Icon></button><button type="button" onClick={() => navigate('abuse-report-reasons')}><span>投诉与举报</span><Icon>chevron_right</Icon></button><button type="button" onClick={() => navigate('clear-chat-confirm')}><span>清除聊天记录</span><Icon>chevron_right</Icon></button></section><button type="button" className="people-danger" onClick={() => navigate('friend-delete-confirm')}>删除好友</button></main></div>;
}
function BlockList({onToast}) {
const [people, setPeople] = React.useState([FRIENDS[3], FRIENDS[2]]);
return <div className="people-page"><Header title="黑名单" back="friend-settings" /><main className="people-block-list">{people.length ? people.map(person => <article key={person.id}><Avatar person={person} /><span><strong>{person.name}</strong><small>已阻止消息和通话</small></span><button type="button" onClick={() => {setPeople(items => items.filter(item => item.id !== person.id)); onToast('已移出黑名单');}}>移出</button></article>) : <div className="people-empty"><Icon>person_off</Icon><strong>黑名单为空</strong></div>}</main></div>;
}
function ConfirmSheet({type, onToast}) {
const config = {
delete: {title: '删除好友', text: '删除后将解除好友关系,对方将无法继续向你发送消息。聊天记录仍会保留。', action: '确认删除', route: 'address-book', toast: '好友已删除'},
block: {title: '加入黑名单', text: '加入黑名单后,你将不再收到对方的消息和通话。', action: '确认拉黑', route: 'friend-settings-blocked', toast: '已加入黑名单'},
clear: {title: '清除聊天记录', text: '仅清除你自己的聊天记录,不会影响对方,也不会删除已收藏内容。', action: '确认清除', route: 'friend-settings', toast: '聊天记录已清除'}
}[type];
return <div className="people-page"><Settings initialBlocked={type === 'block'} onToast={onToast} /><div className="people-sheet-backdrop"><section className="people-confirm-sheet" role="dialog" aria-modal="true"><i /><h2>{config.title}</h2><p>{config.text}</p><button type="button" className="danger" onClick={() => {onToast(config.toast); window.setTimeout(() => navigate(config.route), 700);}}>{config.action}</button><button type="button" onClick={() => navigate('friend-settings')}>取消</button></section></div></div>;
}
export default function ContactManagementView({view}) {
const [toast, setToast] = React.useState('');
let content;
if (view === 'address-book') content = <AddressBook />;
else if (view === 'friend-requests') content = <FriendRequests onToast={setToast} />;
else if (view === 'add-friend' || view === 'friend-search-result') content = <AddFriend result={view === 'friend-search-result'} />;
else if (view === 'my-friend-qr') content = <QrPage onToast={setToast} />;
else if (view === 'friend-verification') content = <Verification onToast={setToast} />;
else if (view === 'friend-profile' || view === 'group-member-profile') content = <Profile friend={view === 'friend-profile'} onToast={setToast} />;
else if (view === 'friend-settings' || view === 'friend-settings-blocked') content = <Settings initialBlocked={view === 'friend-settings-blocked'} onToast={setToast} />;
else if (view === 'block-list') content = <BlockList onToast={setToast} />;
else if (view === 'friend-delete-confirm') content = <ConfirmSheet type="delete" onToast={setToast} />;
else if (view === 'block-confirm') content = <ConfirmSheet type="block" onToast={setToast} />;
else content = <ConfirmSheet type="clear" onToast={setToast} />;
return <>{content}<Toast message={toast} onDone={() => setToast('')} /></>;
}