完成 IM Web Android 端产品页面开发
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import React from 'react';
|
||||
import QRCode from 'qrcodejs';
|
||||
|
||||
import HashNavigation from '../lib/navigation.js';
|
||||
|
||||
const navigate = view => HashNavigation.navigateTo(view);
|
||||
|
||||
export const PERSONAL_SETTINGS_VIEWS = [
|
||||
'personal-center', 'personal-profile', 'profile-nickname', 'profile-birth-date',
|
||||
'profile-signature', 'personal-qrcode', 'android-settings', 'cache-clear-confirm',
|
||||
'app-version', 'app-update-available', 'privacy-settings', 'friend-discovery-settings',
|
||||
'youth-mode-off', 'youth-mode-confirm', 'youth-mode-on', 'youth-mode-disable',
|
||||
'notification-permission', 'permission-settings-reminder', 'camera-permission-info',
|
||||
'microphone-permission-info'
|
||||
];
|
||||
|
||||
const PROFILE_KEY = 'im-demo-personal-profile';
|
||||
const PRIVACY_KEY = 'im-demo-privacy-settings';
|
||||
const YOUTH_KEY = 'im-demo-youth-mode';
|
||||
const DEFAULT_PROFILE = {nickname: '陈晨', birthDate: '1998-06-18', signature: '保持好奇,认真生活。'};
|
||||
|
||||
function readObject(key, fallback) {
|
||||
try { return {...fallback, ...JSON.parse(window.localStorage.getItem(key))}; } catch (_) { return fallback; }
|
||||
}
|
||||
|
||||
function Icon({children}) { return <i className="material-icons" aria-hidden="true">{children}</i>; }
|
||||
|
||||
function Header({title, back = 'personal-center', action, actionLabel, onAction}) {
|
||||
return <header className="personal-header"><button type="button" onClick={() => navigate(back)} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button><h1>{title}</h1>{action ? <button type="button" className="text" onClick={onAction}>{actionLabel || action}</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="personal-toast" role="status">{message}</div> : null;
|
||||
}
|
||||
|
||||
function Toggle({checked, disabled, onChange}) {
|
||||
return <button type="button" role="switch" aria-checked={checked} disabled={disabled} className={'personal-switch' + (checked ? ' checked' : '')} onClick={() => onChange(!checked)}><i /></button>;
|
||||
}
|
||||
|
||||
function MenuRow({icon, title, value, route, onClick, danger}) {
|
||||
return <button type="button" className={'personal-menu-row' + (danger ? ' danger' : '')} onClick={onClick || (() => route && navigate(route))}>{icon ? <Icon>{icon}</Icon> : null}<span><strong>{title}</strong>{value ? <small>{value}</small> : null}</span>{route || onClick ? <Icon>chevron_right</Icon> : null}</button>;
|
||||
}
|
||||
|
||||
function PersonalCenter() {
|
||||
const profile = readObject(PROFILE_KEY, DEFAULT_PROFILE);
|
||||
return <div className="personal-page personal-center-page"><Header title="我的" back="messages-home" /><main><button type="button" className="personal-identity" onClick={() => navigate('personal-profile')}><img src="img/bkg/p05.jpg" alt="" /><span><strong>{profile.nickname}</strong><small>产品研发部 · 产品经理</small><small>账号:CHEN88</small></span><Icon>chevron_right</Icon></button><section><MenuRow icon="qr_code_2" title="我的二维码" route="personal-qrcode" /></section><section><MenuRow icon="inventory_2" title="我的收藏" route="favorites" /><MenuRow icon="feedback" title="意见反馈" route="user-feedback" /><MenuRow icon="settings" title="设置" route="android-settings" /></section></main></div>;
|
||||
}
|
||||
|
||||
function PersonalProfile({onToast}) {
|
||||
const profile = readObject(PROFILE_KEY, DEFAULT_PROFILE);
|
||||
return <div className="personal-page"><Header title="个人信息" /><main className="personal-profile-list"><button type="button" onClick={() => onToast('头像选择器已打开')}><strong>头像</strong><img src="img/bkg/p05.jpg" alt="" /><Icon>chevron_right</Icon></button><MenuRow title="昵称" value={profile.nickname} route="profile-nickname" /><MenuRow title="出生日期" value={profile.birthDate} route="profile-birth-date" /><MenuRow title="二维码名片" value="" route="personal-qrcode" /><MenuRow title="账号" value="CHEN88" /><MenuRow title="职位" value="产品经理" /><MenuRow title="公司" value="星海科技有限公司" /><MenuRow title="个性签名" value={profile.signature || '未填写'} route="profile-signature" /></main></div>;
|
||||
}
|
||||
|
||||
function TextEditor({field, multiline, title, maxLength, onToast}) {
|
||||
const profile = readObject(PROFILE_KEY, DEFAULT_PROFILE);
|
||||
const [value, setValue] = React.useState(profile[field] || '');
|
||||
const save = () => {
|
||||
const normalized = value.trim();
|
||||
if (field === 'nickname' && !normalized) return onToast('昵称不能为空');
|
||||
const next = {...profile, [field]: field === 'signature' ? value.trim() : normalized};
|
||||
window.localStorage.setItem(PROFILE_KEY, JSON.stringify(next));
|
||||
onToast('资料已保存');
|
||||
window.setTimeout(() => navigate('personal-profile'), 650);
|
||||
};
|
||||
return <div className="personal-page profile-editor-page"><Header title={title} back="personal-profile" action="完成" onAction={save} /><main><label>{multiline ? <textarea value={value} maxLength={maxLength} autoFocus onChange={event => setValue(event.target.value)} /> : <input value={value} maxLength={maxLength} autoFocus onChange={event => setValue(event.target.value)} />}<small>{value.length}/{maxLength}</small>{value ? <button type="button" onClick={() => setValue('')} aria-label="清空"><Icon>cancel</Icon></button> : null}</label><p>{field === 'nickname' ? '昵称将展示在联系人资料和聊天页面中' : '签名将展示在你的个人资料页'}</p></main></div>;
|
||||
}
|
||||
|
||||
function BirthDateEditor({onToast}) {
|
||||
const profile = readObject(PROFILE_KEY, DEFAULT_PROFILE);
|
||||
const [value, setValue] = React.useState(profile.birthDate);
|
||||
const save = () => {window.localStorage.setItem(PROFILE_KEY, JSON.stringify({...profile, birthDate: value})); onToast('出生日期已保存'); window.setTimeout(() => navigate('personal-profile'), 650);};
|
||||
return <div className="personal-page profile-editor-page"><Header title="出生日期" back="personal-profile" action="完成" onAction={save} /><main className="birth-date-editor"><Icon>cake</Icon><label><span>选择出生日期</span><input type="date" value={value} max="2026-08-25" min="1900-01-01" onChange={event => setValue(event.target.value)} /></label><p>出生日期的可见范围由隐私设置决定</p></main></div>;
|
||||
}
|
||||
|
||||
function PersonalQr({onToast}) {
|
||||
const profile = readObject(PROFILE_KEY, DEFAULT_PROFILE);
|
||||
const [token, setToken] = React.useState(Date.now());
|
||||
const qrRef = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
qrRef.current.innerHTML = '';
|
||||
new QRCode(qrRef.current, {text: `im://profile-card?token=demo-${token}&purpose=add_friend`, width: 210, height: 210, correctLevel: QRCode.CorrectLevel.M});
|
||||
}, [token]);
|
||||
return <div className="personal-page personal-qr-page"><Header title="我的二维码" /><main><section><div className="personal-qr-profile"><img src="img/bkg/p05.jpg" alt="" /><span><strong>{profile.nickname}</strong><small>星海科技有限公司</small></span></div><div className="personal-qrcode" ref={qrRef} /><p>扫一扫上面的二维码,加我为好友</p><small>二维码凭证有效期为 24 小时</small></section><div className="personal-qr-actions"><button type="button" onClick={() => {setToken(Date.now()); onToast('二维码已刷新');}}><Icon>refresh</Icon><span>刷新</span></button><button type="button" onClick={() => onToast('二维码名片已分享')}><Icon>share</Icon><span>分享</span></button></div></main></div>;
|
||||
}
|
||||
|
||||
function AndroidSettings({onToast}) {
|
||||
return <div className="personal-page"><Header title="设置" /><main className="android-settings-list"><section><MenuRow icon="shield" title="账号与安全" route="security" /><MenuRow icon="notifications_none" title="消息通知" route="notification-permission" /><MenuRow icon="lock_outline" title="隐私设置" route="privacy-settings" /></section><section><MenuRow icon="photo_camera" title="相机权限说明" route="camera-permission-info" /><MenuRow icon="mic_none" title="麦克风权限说明" route="microphone-permission-info" /></section><section><MenuRow icon="delete_sweep" title="清空缓存" value="126 MB" route="cache-clear-confirm" /><MenuRow icon="child_care" title="青少年模式" value={window.localStorage.getItem(YOUTH_KEY) === 'enabled' ? '已开启' : '未开启'} route={window.localStorage.getItem(YOUTH_KEY) === 'enabled' ? 'youth-mode-on' : 'youth-mode-off'} /><MenuRow icon="system_update" title="版本更新" value="1.2.4" route="app-version" /></section><section><MenuRow icon="block" title="黑名单" route="block-list" /><MenuRow icon="wallpaper" title="聊天背景" route="wallpapers" /><MenuRow icon="info_outline" title="关于应用" route="app-version" /></section><button type="button" className="personal-logout" onClick={() => onToast('退出登录需在后端对接阶段完成会话撤销')}>退出当前登录</button></main></div>;
|
||||
}
|
||||
|
||||
function CacheClear({onToast}) {
|
||||
const [cleared, setCleared] = React.useState(false);
|
||||
return <div className="personal-page"><AndroidSettings onToast={onToast} /><div className="personal-sheet-backdrop"><section className="personal-confirm-sheet" role="dialog" aria-modal="true"><Icon>{cleared ? 'check_circle' : 'delete_sweep'}</Icon><h2>{cleared ? '缓存已清理' : '清空缓存'}</h2><p>{cleared ? '已释放 126 MB 存储空间。' : '将清除缩略图、临时文件和可重新下载的数据,不会删除聊天记录、收藏或登录信息。'}</p>{cleared ? <button type="button" className="primary" onClick={() => navigate('android-settings')}>完成</button> : <><button type="button" className="danger" onClick={() => {setCleared(true); onToast('缓存清理完成');}}>确认清理</button><button type="button" onClick={() => navigate('android-settings')}>取消</button></>}</section></div></div>;
|
||||
}
|
||||
|
||||
function AppVersion({updatePrompt, onToast}) {
|
||||
const [checking, setChecking] = React.useState(false);
|
||||
const check = () => {setChecking(true); window.setTimeout(() => {setChecking(false); navigate('app-update-available');}, 700);};
|
||||
return <div className="personal-page app-version-page"><Header title="关于与更新" back="android-settings" /><main><section className="app-brand"><span><Icon>forum</Icon></span><h2>光讯通讯</h2><p>Android 版本 1.2.4</p></section><section className="personal-menu-section"><MenuRow title="用户条款" route="app-version" /><MenuRow title="隐私协议" route="app-version" /><MenuRow title="检测新版本" value={checking ? '检测中…' : '有新版本'} onClick={check} /><MenuRow title="联系我们" value="support@im.example" /></section></main>{updatePrompt ? <div className="update-backdrop"><section className="update-dialog" role="dialog" aria-modal="true"><span><Icon>rocket_launch</Icon></span><h2>发现新版本</h2><strong>版本 1.3.0</strong><ul><li>优化消息与工作台加载速度</li><li>完善隐私设置和 Android 权限引导</li><li>修复已知问题</li></ul><button type="button" onClick={() => onToast('更新任务已交给 Android 下载管理器')}>立即更新</button><button type="button" onClick={() => navigate('app-version')}>稍后提醒</button></section></div> : null}</div>;
|
||||
}
|
||||
|
||||
function PrivacySettings({onToast}) {
|
||||
const initial = readObject(PRIVACY_KEY, {groupVerification: true, readReceipts: true, lastSeen: true});
|
||||
const [settings, setSettings] = React.useState(initial);
|
||||
const update = (key, value) => {const next = {...settings, [key]: value}; setSettings(next); window.localStorage.setItem(PRIVACY_KEY, JSON.stringify(next)); onToast('隐私设置已保存');};
|
||||
return <div className="personal-page"><Header title="隐私设置" back="android-settings" /><main className="privacy-list"><section><MenuRow title="添加我的方式" value="全部允许" route="friend-discovery-settings" /></section><section><div><span><strong>邀请我入群需要验证</strong><small>关闭后仍受企业安全策略保护</small></span><Toggle checked={settings.groupVerification} onChange={value => update('groupVerification', value)} /></div><div><span><strong>显示消息已读状态</strong><small>关闭后你也无法查看对方的已读状态</small></span><Toggle checked={settings.readReceipts} onChange={value => update('readReceipts', value)} /></div><div><span><strong>展示最后在线时间</strong><small>仅向联系人显示</small></span><Toggle checked={settings.lastSeen} onChange={value => update('lastSeen', value)} /></div></section><section><MenuRow title="黑名单" route="block-list" /></section></main></div>;
|
||||
}
|
||||
|
||||
function FriendDiscoverySettings({onToast}) {
|
||||
const initial = readObject(PRIVACY_KEY, {account: true, phone: true, qr: true, group: true});
|
||||
const [methods, setMethods] = React.useState(initial);
|
||||
const update = (key, value) => {const next = {...methods, [key]: value}; setMethods(next); window.localStorage.setItem(PRIVACY_KEY, JSON.stringify(next)); onToast('添加方式已更新');};
|
||||
const rows = [{key: 'account', title: '账号'}, {key: 'phone', title: '手机号'}, {key: 'qr', title: '二维码'}, {key: 'group', title: '群聊'}];
|
||||
return <div className="personal-page"><Header title="添加我的方式" back="privacy-settings" /><main className="privacy-list"><p className="privacy-note">关闭某种方式后,其他人将无法通过该渠道搜索或添加你。</p><section>{rows.map(row => <div key={row.key}><span><strong>可通过{row.title}添加我</strong></span><Toggle checked={methods[row.key]} onChange={value => update(row.key, value)} /></div>)}</section></main></div>;
|
||||
}
|
||||
|
||||
const YOUTH_RULES = ['每日使用时长不超过 40 分钟', '限制主动或被动添加好友、加入群聊的次数', '隐藏不适合未成年人的内容与功能'];
|
||||
|
||||
function YouthMode({state, onToast}) {
|
||||
const enabled = state === 'on' || state === 'disable';
|
||||
const [password, setPassword] = React.useState('');
|
||||
const enable = () => {window.localStorage.setItem(YOUTH_KEY, 'enabled'); onToast('青少年模式已开启'); window.setTimeout(() => navigate('youth-mode-on'), 650);};
|
||||
const disable = () => {if (password.length < 6) return onToast('请输入 6 位独立密码'); window.localStorage.removeItem(YOUTH_KEY); setPassword(''); onToast('青少年模式已关闭'); window.setTimeout(() => navigate('youth-mode-off'), 650);};
|
||||
return <div className="personal-page youth-mode-page"><Header title="青少年模式" back="android-settings" /><main><Icon>{enabled ? 'verified_user' : 'child_care'}</Icon><h2>{enabled ? '青少年模式已开启' : '青少年模式未开启'}</h2><ul>{YOUTH_RULES.map(rule => <li key={rule}>{rule}</li>)}</ul>{enabled ? <p className="youth-usage">今日已使用:00:24:05</p> : null}</main><footer><button type="button" className={enabled ? 'outline' : ''} onClick={() => navigate(enabled ? 'youth-mode-disable' : 'youth-mode-confirm')}>{enabled ? '关闭青少年模式' : '开启青少年模式'}</button><small>可在设置中查看青少年模式状态</small></footer>{state === 'confirm' ? <div className="personal-sheet-backdrop"><section className="personal-confirm-sheet youth-confirm" role="dialog" aria-modal="true"><Icon>family_restroom</Icon><h2>开启青少年模式</h2><p>开启后将按照当前企业的青少年保护策略限制使用时长、社交次数和部分内容。独立密码用于关闭模式。</p><button type="button" className="primary" onClick={enable}>确认并开启</button><button type="button" onClick={() => navigate('youth-mode-off')}>取消</button></section></div> : null}{state === 'disable' ? <div className="personal-sheet-backdrop"><section className="personal-confirm-sheet youth-confirm" role="dialog" aria-modal="true"><Icon>password</Icon><h2>关闭青少年模式</h2><p>请输入开启青少年模式时设置的 6 位独立密码。</p><input type="password" inputMode="numeric" maxLength="6" value={password} autoFocus onChange={event => setPassword(event.target.value.replace(/\D/g, ''))} placeholder="独立密码" /><button type="button" className="primary" onClick={disable}>验证并关闭</button><button type="button" onClick={() => navigate('youth-mode-on')}>取消</button></section></div> : null}</div>;
|
||||
}
|
||||
|
||||
const PERMISSIONS = {
|
||||
notification: {icon: 'notifications_active', title: '获取通知权限', text: '用于及时送达新消息、会议提醒和安全通知。', action: '开启通知', route: 'permission-settings-reminder'},
|
||||
reminder: {icon: 'settings', title: '消息推送暂未开启', text: '请前往 Android 系统设置,在通知管理中允许光讯通讯发送通知。', action: '前往设置', route: 'android-settings'},
|
||||
camera: {icon: 'photo_camera', title: '相机权限使用说明', text: '用于拍照、扫码和视频通话。拒绝后仍可从相册选择图片。', action: '继续授权', route: 'messages-home'},
|
||||
microphone: {icon: 'mic', title: '麦克风权限使用说明', text: '用于语音消息、语音输入和音视频通话,未授权时不会创建录音文件。', action: '继续授权', route: 'messages-home'}
|
||||
};
|
||||
|
||||
function PermissionGuide({type, onToast}) {
|
||||
const config = PERMISSIONS[type];
|
||||
const proceed = () => {onToast(type === 'reminder' ? '已打开 Android 应用设置' : '等待 Android 系统授权结果'); window.setTimeout(() => navigate(config.route), 650);};
|
||||
return <div className="personal-page permission-guide-page"><div className="permission-preview"><header><h1>消息</h1><Icon>menu</Icon></header><label><Icon>search</Icon><span>搜索</span></label>{['系统消息', '产品研发群', '文件助手', '安妮'].map((name, index) => <div key={name}><span className={'permission-avatar tone-' + index}><Icon>{index === 0 ? 'notifications' : index === 2 ? 'folder' : 'person'}</Icon></span><span><strong>{name}</strong><small>{index ? '最近有一条新消息' : '账号安全提醒'}</small></span></div>)}</div><div className="permission-overlay"><section role="dialog" aria-modal="true"><span><Icon>{config.icon}</Icon></span><h2>{config.title}</h2><p>{config.text}</p><button type="button" onClick={proceed}>{config.action}</button><button type="button" onClick={() => navigate('android-settings')}>暂不开启</button></section></div></div>;
|
||||
}
|
||||
|
||||
export default function PersonalSettingsView({view}) {
|
||||
const [toast, setToast] = React.useState('');
|
||||
let content;
|
||||
if (view === 'personal-center') content = <PersonalCenter />;
|
||||
else if (view === 'personal-profile') content = <PersonalProfile onToast={setToast} />;
|
||||
else if (view === 'profile-nickname') content = <TextEditor field="nickname" title="设置昵称" maxLength={32} onToast={setToast} />;
|
||||
else if (view === 'profile-signature') content = <TextEditor field="signature" title="个性签名" maxLength={120} multiline onToast={setToast} />;
|
||||
else if (view === 'profile-birth-date') content = <BirthDateEditor onToast={setToast} />;
|
||||
else if (view === 'personal-qrcode') content = <PersonalQr onToast={setToast} />;
|
||||
else if (view === 'android-settings') content = <AndroidSettings onToast={setToast} />;
|
||||
else if (view === 'cache-clear-confirm') content = <CacheClear onToast={setToast} />;
|
||||
else if (view === 'app-version' || view === 'app-update-available') content = <AppVersion updatePrompt={view === 'app-update-available'} onToast={setToast} />;
|
||||
else if (view === 'privacy-settings') content = <PrivacySettings onToast={setToast} />;
|
||||
else if (view === 'friend-discovery-settings') content = <FriendDiscoverySettings onToast={setToast} />;
|
||||
else if (view.startsWith('youth-mode')) content = <YouthMode state={view.replace('youth-mode-', '')} onToast={setToast} />;
|
||||
else content = <PermissionGuide type={view === 'notification-permission' ? 'notification' : view === 'permission-settings-reminder' ? 'reminder' : view === 'camera-permission-info' ? 'camera' : 'microphone'} onToast={setToast} />;
|
||||
return <>{content}<Toast message={toast} onDone={() => setToast('')} /></>;
|
||||
}
|
||||
Reference in New Issue
Block a user