import React from 'react'; import QRCode from 'qrcodejs'; import HashNavigation from '../lib/navigation.js'; import { useCachedData, readCachedData, writeCachedData } from '../lib/use-cached-data.js'; import { BASE_APP_NAME, LINK_CONTACT_US } from '../config.js'; import { PACKAGE_VERSION } from '../version.js'; import { getMeProfile, updateMeProfile, getFileUrl, uploadLargeFile, getMePrivacy, updateMePrivacy, getYouthMode, enableYouthMode, disableYouthMode, verifyIndependentPassword, createMyQRCode, getRealnameStatus } from '../lib/im-api.js'; const navigate = (view, params) => { let hash = HashNavigation.setUrlSidePanel(window.location.hash, view); Object.entries(params || {}).forEach(([key, value]) => { hash = value ? HashNavigation.addUrlParam(hash, key, value) : HashNavigation.removeUrlParam(hash, key); }); HashNavigation.navigateTo(hash); }; export const PERSONAL_SETTINGS_VIEWS = [ 'personal-center', 'personal-profile', 'profile-nickname', 'profile-birth-date', 'profile-signature', 'profile-gender', 'profile-region', 'personal-qrcode', 'android-settings', 'cache-clear-confirm', 'clear-history-confirm', 'change-password', 'delete-account-confirm', 'app-version', 'privacy-settings', 'friend-discovery-settings', 'youth-mode-off', 'youth-mode-on', 'youth-mode-verify', 'notification-permission', 'permission-settings-reminder', 'camera-permission-info', 'microphone-permission-info' ]; function Icon({children}) { return ; } function Header({title, back = 'personal-center', action, actionLabel, onAction, actionDisabled}) { return

{title}

{action ? : }
; } function Toast({message, onDone}) { React.useEffect(() => { if (!message) return undefined; const timer = window.setTimeout(onDone, 2100); return () => window.clearTimeout(timer); }, [message, onDone]); return message ?
{message}
: null; } function Toggle({checked, disabled, onChange}) { return ; } function MenuRow({icon, title, value, route, onClick, danger}) { return ; } // docs/images/我的.png 的"企业名称"字段实际上是租户名,不是组织架构的部门 -- 复用 Ch.9 workbench // 已经验证过的读取方式(登录时 tenants/resolve 缓存进 localStorage['im-tenant'])。 function tenantName() { try { return JSON.parse(window.localStorage.getItem('im-tenant') || '{}').name || ''; } catch (_) { return ''; } } function ImgFromFile({fileId, className}) { const [url, setUrl] = React.useState(null); React.useEffect(() => { let live = true; getFileUrl(fileId).then(u => live && setUrl(u)).catch(() => {}); return () => { live = false; }; }, [fileId]); return url ? : ; } function PersonalCenter() { // Was `useState(null)` + its own effect -- remounting this (e.g. tabbing back from 设置) blanked // the name/avatar back to the loading placeholder for a beat even though nothing had changed, // same "switching pages feels like it reloads every time" complaint this cache already fixed for // the workbench/message list/address book. const [profile] = useCachedData('me-profile', getMeProfile, []); return
; } // docs/images/个人信息.png 两张截图字段不完全一样(一张有用户名/职位/公司,另一张有ID号/地区)-- // 代码调研确认 /me/profile 真实字段只有 avatar/nickname/gender/birth_date/region/signature/email; // "职位/公司"属于第9章组织架构数据源(这里没有反查接口,不展示);"ID号"、"用户名"是原型臆造字段, // 后端没有对应概念(用户id恒为usrXXX格式),不做。 function PersonalProfile({onToast, tinode}) { // Shares the 'me-profile' key with PersonalCenter's already-fixed cache -- this is the hub every // field editor below navigates back to on save, so it's revisited constantly. const [profile, setProfile] = React.useState(() => readCachedData('me-profile') || null); const fileInputRef = React.useRef(null); const [uploading, setUploading] = React.useState(false); const load = () => getMeProfile().then(p => { writeCachedData('me-profile', p); setProfile(p); }).catch(() => {}); React.useEffect(() => { load(); }, []); const pickAvatar = () => fileInputRef.current && fileInputRef.current.click(); const handleAvatarFile = event => { const file = event.target.files && event.target.files[0]; event.target.value = ''; if (!file || !tinode) return; setUploading(true); uploadLargeFile(tinode, file).then(({fileId}) => updateMeProfile({avatar_file_id: fileId, version: profile.version})) .then(load).then(() => onToast('头像已更新')) .catch(err => onToast(err.message || '头像上传失败')).finally(() => setUploading(false)); }; const genderLabel = {male: '男', female: '女', other: '其他'}[profile && profile.gender] || '未设置'; return
; } // Real field_rules confirmed against server/im_http.go: nickname max_length=32, signature // max_length=120 -- the reference screenshots show 120/20 respectively (swapped and wrong), // mock code already had the right numbers by luck; kept here but sourced from a constant that // matches the server contract rather than the screenshots. const FIELD_LIMITS = {nickname: 32, signature: 120}; function TextEditor({field, multiline, title, onToast}) { const [profile, setProfile] = React.useState(null); const [value, setValue] = React.useState(''); const [saving, setSaving] = React.useState(false); React.useEffect(() => { getMeProfile().then(p => { setProfile(p); setValue(p[field] || ''); }).catch(() => {}); }, [field]); const save = () => { const normalized = value.trim(); if (field === 'nickname' && !normalized) { onToast('昵称不能为空'); return; } setSaving(true); updateMeProfile({[field]: normalized, version: profile.version}).then(_ => { onToast('资料已保存'); window.setTimeout(() => navigate('personal-profile'), 500); }).catch(err => onToast(err.message || '保存失败')).finally(() => setSaving(false)); }; const maxLength = FIELD_LIMITS[field]; return