清除缓存/清除聊天记录/注销账号确认弹层改为叠加显示,不再是整页路由跳转

这三个"确认"操作原来是独立的hash路由,点击后"设置"整个列表(含标题栏)连同Header一起卸载,
只剩ConfirmSheet的半透明蒙层盖在空白背景上——蒙层本来是设计成衬着被弹出的页面看的,但因为
底下真的没有渲染任何东西,视觉上跟跳转到一个新的空白页面没有区别。

改为SettingsList自己的本地state(sheet: null|'cache'|'history'|'delete'),确认层作为兄弟
节点叠加渲染,列表本身不卸载,不再经过hash导航——照抄group-management-view.jsx的OwnerSelect
(选择新群主)已经在用的正确模式。同时清理了tinode-web.jsx两处硬编码视图白名单里对应的
三个route名(不再是独立路由,无需保留)。

contact-management-view.jsx同类的删除好友/拉黑/清空聊天记录确认层核实过不受影响——它的
ConfirmSheet会重新渲染Settings页面作为背景,视觉上本来就是正确的。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-05 17:32:39 +08:00
co-authored by Claude Sonnet 5
parent d759f67b11
commit f688578714
4 changed files with 70 additions and 49 deletions
+27 -19
View File
@@ -22,7 +22,7 @@ const navigate = (view, params) => {
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',
'change-password',
'app-version', 'privacy-settings', 'friend-discovery-settings',
'youth-mode-off', 'youth-mode-on', 'youth-mode-verify',
'notification-permission', 'permission-settings-reminder', 'camera-permission-info',
@@ -282,7 +282,7 @@ function PersonalQr({onToast}) {
// docs/images/设置(iOS).png 只有7项,docs/images/设置(安卓).png 多了"青少年模式"/"实名认证"两行状态
// 展示 -- 两个平台业务能力理应一致(文字需求也这么写),采用更完整的安卓版列表,不再区分iOS/安卓两份。
function SettingsList({onToast, onLogout}) {
function SettingsList({onToast, onLogout, tinode, chatList, onDeleteAccount}) {
// 'youth-mode' is shared with the YouthMode page's own fetch of the same endpoint; 'me-realname'
// has no other reader today but gets the same treatment for consistency -- both status badges here
// otherwise flash a plausible-but-possibly-wrong value (realnameLabel's own fallback below shows
@@ -291,6 +291,13 @@ function SettingsList({onToast, onLogout}) {
// blank one.
const [youthSettings, setYouthSettings] = React.useState(() => readCachedData('youth-mode') || null);
const [realname, setRealname] = React.useState(() => readCachedData('me-realname') || null);
// 清空缓存/清除聊天记录/注销账号三个确认弹层原来是各自独立的 view(hash 路由),route 一跳,
// "设置"这个列表整个连同 Header 一起卸载重挂载——ConfirmSheet 的 .personal-sheet-backdrop 本来是
// 设计成半透明蒙层盖在被弹出页面上方的(backdrop 4:52 rgba,明显是"衬着底下内容看"的视觉语言),
// 但因为底下真的没有任何东西在渲染,视觉上跟直接跳转到一个空白新页面没有区别。改成本组件自己的
// 本地 state,弹层作为兄弟节点叠在列表上方,列表本身不卸载——group-management-view.jsx 的
// OwnerSelect(选择新群主)已经是这个正确模式,这里照抄,不是发明新写法。
const [sheet, setSheet] = React.useState(null); // null | 'cache' | 'history' | 'delete'
React.useEffect(() => {
getYouthMode().then(d => { writeCachedData('youth-mode', d); setYouthSettings(d); }).catch(() => {});
getRealnameStatus().then(d => { writeCachedData('me-realname', d); setRealname(d); }).catch(() => {});
@@ -305,8 +312,8 @@ function SettingsList({onToast, onLogout}) {
};
return <div className="personal-page"><Header title="设置" /><main className="android-settings-list">
<section>
<MenuRow icon="delete_sweep" title="清除缓存" route="cache-clear-confirm" />
<MenuRow icon="mark_chat_read" title="清除所有聊天记录" route="clear-history-confirm" />
<MenuRow icon="delete_sweep" title="清除缓存" onClick={() => setSheet('cache')} />
<MenuRow icon="mark_chat_read" title="清除所有聊天记录" onClick={() => setSheet('history')} />
<MenuRow icon="password" title="修改密码" route="change-password" />
</section>
<section>
@@ -321,9 +328,13 @@ function SettingsList({onToast, onLogout}) {
用的是同一套 hash 导航,可以直接指过去。 */}
<MenuRow icon="shield" title="安全教育" route="fraud-risk-warning" />
</section>
<section><MenuRow icon="no_accounts" title="注销账号" route="delete-account-confirm" danger /></section>
<section><MenuRow icon="no_accounts" title="注销账号" onClick={() => setSheet('delete')} danger /></section>
<button type="button" className="personal-logout" onClick={logout}>退出当前登录</button>
</main></div>;
</main>
{sheet === 'cache' ? <CacheClear onToast={onToast} onClose={() => setSheet(null)} /> : null}
{sheet === 'history' ? <ClearHistoryConfirm onToast={onToast} tinode={tinode} chatList={chatList} onClose={() => setSheet(null)} /> : null}
{sheet === 'delete' ? <DeleteAccountConfirm onToast={onToast} onDeleteAccount={onDeleteAccount} onClose={() => setSheet(null)} /> : null}
</div>;
}
function ConfirmSheet({icon, title, body, confirmLabel = '确认', confirmDanger = true, busy, onConfirm, onCancel, children}) {
@@ -339,7 +350,7 @@ function ConfirmSheet({icon, title, body, confirmLabel = '确认', confirmDanger
// server/main.go's ~74 routes have nothing for either). Cache clearing empties Cache Storage/IDB;
// clearing all history calls the native Tinode delMessagesAll(false) per open conversation, same
// method contact-management-view.jsx/group-management-view.jsx already use for single-topic clears.
function CacheClear({onToast}) {
function CacheClear({onToast, onClose}) {
const [busy, setBusy] = React.useState(false);
const [done, setDone] = React.useState(false);
const [freed, setFreed] = React.useState(0);
@@ -370,25 +381,25 @@ function CacheClear({onToast}) {
return <ConfirmSheet icon={done ? 'check_circle' : 'delete_sweep'} title={done ? '缓存已清理' : '清空缓存'}
body={done ? `已释放约 ${(freed / (1024 * 1024)).toFixed(1)} MB 存储空间。` : '确定清空本地的缓存数据?将清除缩略图、临时文件和可重新下载的数据,不会删除聊天记录、收藏或登录信息。'}
confirmLabel={done ? '完成' : '确定'} confirmDanger={!done} busy={busy}
onConfirm={done ? () => navigate('android-settings') : confirm} onCancel={() => navigate('android-settings')} />;
onConfirm={done ? onClose : confirm} onCancel={onClose} />;
}
function ClearHistoryConfirm({tinode, chatList, onToast}) {
function ClearHistoryConfirm({tinode, chatList, onToast, onClose}) {
const [busy, setBusy] = React.useState(false);
const confirm = () => {
if (!tinode || !chatList || !chatList.length) { onToast('没有可清除的会话'); navigate('android-settings'); return; }
if (!tinode || !chatList || !chatList.length) { onToast('没有可清除的会话'); onClose(); return; }
setBusy(true);
Promise.all(chatList.map(c => {
const topic = tinode.getTopic(c.name);
return topic ? topic.delMessagesAll(false).catch(() => {}) : Promise.resolve();
})).then(() => {
onToast('聊天记录已清空');
navigate('android-settings');
onClose();
}).finally(() => setBusy(false));
};
return <ConfirmSheet icon="mark_chat_read" title="清除所有聊天记录"
body="确定清除所有聊天记录?此操作不可恢复,但不会影响好友/群聊关系和收藏内容。" busy={busy}
onConfirm={confirm} onCancel={() => navigate('android-settings')} />;
onConfirm={confirm} onCancel={onClose} />;
}
// Changing password has no REST endpoint -- it's native Tinode {acc} protocol
@@ -431,7 +442,7 @@ function ChangePassword({onUpdatePassword, onToast}) {
// no cooldown/soft-delete and no password re-check (confirmed against server/user.go) -- adding a
// password confirmation step here is a deliberate, conservative frontend-only safeguard given how
// irreversible and consequential this action is, not something the backend requires.
function DeleteAccountConfirm({onDeleteAccount, onToast}) {
function DeleteAccountConfirm({onDeleteAccount, onToast, onClose}) {
const [step, setStep] = React.useState(1);
const [password, setPassword] = React.useState('');
const [busy, setBusy] = React.useState(false);
@@ -448,10 +459,10 @@ function DeleteAccountConfirm({onDeleteAccount, onToast}) {
// to be disclosed here since it's the only place the user sees before confirming.
return <ConfirmSheet icon="warning" title="注销账号"
body="注销后账号数据将被永久删除且无法恢复,包括聊天记录、好友关系和收藏内容。如果你是某些群的群主,这些群也将被永久解散,其他成员将无法继续使用。确定要继续吗?"
confirmLabel="继续" onConfirm={() => setStep(2)} onCancel={() => navigate('android-settings')} />;
confirmLabel="继续" onConfirm={() => setStep(2)} onCancel={onClose} />;
}
return <ConfirmSheet icon="no_accounts" title="输入密码确认注销" body="请输入登录密码以确认注销账号(该操作立即生效,不可撤销)。"
busy={busy} onConfirm={proceed} onCancel={() => navigate('android-settings')}>
busy={busy} onConfirm={proceed} onCancel={onClose}>
<input type="password" autoFocus value={password} onChange={e => setPassword(e.target.value)} placeholder="登录密码" />
</ConfirmSheet>;
}
@@ -755,11 +766,8 @@ export default function PersonalSettingsView({view, hashParams, myUserId, tinode
else if (view === 'profile-gender') content = <GenderEditor onToast={setToast} />;
else if (view === 'profile-region') content = <RegionEditor onToast={setToast} />;
else if (view === 'personal-qrcode') content = <PersonalQr onToast={setToast} />;
else if (view === 'android-settings') content = <SettingsList onToast={setToast} onLogout={onLogout} />;
else if (view === 'cache-clear-confirm') content = <CacheClear onToast={setToast} />;
else if (view === 'clear-history-confirm') content = <ClearHistoryConfirm onToast={setToast} tinode={tinode} chatList={chatList} />;
else if (view === 'android-settings') content = <SettingsList onToast={setToast} onLogout={onLogout} tinode={tinode} chatList={chatList} onDeleteAccount={onDeleteAccount} />;
else if (view === 'change-password') content = <ChangePassword onToast={setToast} onUpdatePassword={onUpdatePassword} />;
else if (view === 'delete-account-confirm') content = <DeleteAccountConfirm onToast={setToast} onDeleteAccount={onDeleteAccount} />;
else if (view === 'app-version') content = <AppVersion onToast={setToast} />;
else if (view === 'privacy-settings') content = <PrivacySettings onToast={setToast} />;
else if (view === 'friend-discovery-settings') content = <FriendDiscoverySettings onToast={setToast} />;
+2 -2
View File
@@ -140,7 +140,7 @@ const SIDE_PANEL_VIEWS = ['acc_general','archive','blocked','contacts','cred','c
'executive-mailbox','executive-mailbox-notice',
'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',
'change-password',
'app-version','privacy-settings','friend-discovery-settings',
'youth-mode-off','youth-mode-on','youth-mode-verify',
'notification-permission','permission-settings-reminder','camera-permission-info',
@@ -2677,7 +2677,7 @@ class TinodeWeb extends React.Component {
'monthly-report-detail', 'executive-mailbox', 'executive-mailbox-notice'].includes(activeProductView);
const personalSettingsActive = ['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',
'change-password',
'app-version', 'privacy-settings',
'friend-discovery-settings', 'youth-mode-off', 'youth-mode-on', 'youth-mode-verify',
'notification-permission', 'permission-settings-reminder',