修复"解散群组"仍用原生 window.confirm(),是本文件唯一未统一的危险操作确认弹层

现象:本工程已经在"退出当前登录"那次修复(46a98782)里明确认定原生 window.confirm()
和其余确认弹层不一致、且在部分内嵌 WebView/kiosk 环境下会被直接禁用(同步对话框调用
无提示地恒返回 false,操作被静默拦下且用户毫无感知)。全仓库复查后发现"解散群组"是
group-management-view.jsx 里唯一还在用这个模式的危险操作——同样不可逆、后果同样重大,
却没有跟上那次统一。

修复:改成两段式状态(先点"解散群组"进入确认态,再点确认态里的"确定"才真正调用
delTopic),复用本文件 OwnerSelect 早就在用的同款弹层视觉语言
(.group-sheet-backdrop/.group-confirm-sheet),不再依赖浏览器原生对话框。

验证:真实浏览器操作,用真实账号在真实群里走了"点解散→点取消"(弹层消失、群未受
影响、群列表可正常继续访问)和"点解散→点确定"(原生 WS {del hard:true} 成功执行,
200 ok,正确跳转回群列表且群已从列表消失)两条路径。

顺带记录:content-library-view.jsx 的收藏批量删除也还在用裸 window.confirm(),
是同一类问题,但属于第七章范围,本轮不动,留给对应章节的测试处理。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-06 16:30:06 +08:00
co-authored by Claude Sonnet 5
parent 30e6d5e300
commit 4773ee5f64
+21 -5
View File
@@ -649,6 +649,7 @@ function Governance({topic, myRole, tinode, onToast}) {
// and returns, so this is one of the highest-traffic pages in the file. // and returns, so this is one of the highest-traffic pages in the file.
const [gov, setGov] = React.useState(() => (topic && readCachedData('group-governance:' + topic)) || null); const [gov, setGov] = React.useState(() => (topic && readCachedData('group-governance:' + topic)) || null);
const [dissolving, setDissolving] = React.useState(false); const [dissolving, setDissolving] = React.useState(false);
const [confirmingDissolve, setConfirmingDissolve] = React.useState(false);
const [loadError, setLoadError] = React.useState(false); const [loadError, setLoadError] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
if (!topic) return; if (!topic) return;
@@ -664,18 +665,27 @@ function Governance({topic, myRole, tinode, onToast}) {
updateGroupGovernance(topic, {[key]: value, version: gov ? gov.version : 0}).then(data => { writeCachedData('group-governance:' + topic, data); setGov(data); }) updateGroupGovernance(topic, {[key]: value, version: gov ? gov.version : 0}).then(data => { writeCachedData('group-governance:' + topic, data); setGov(data); })
.catch(err => onToast(err.message || '设置失败')); .catch(err => onToast(err.message || '设置失败'));
}; };
const dissolve = () => { // 原来是裸 window.confirm()——本工程已经在"退出登录"那次修复里明确认定这类原生弹窗和
// 其余确认弹层不一致(personal-settings-view.jsx 的 LogoutConfirm/ConfirmSheet 那条注释:
// "a native confirm() is also environment-dependent: some embedded WebViews/kiosk contexts
// disable synchronous dialogs outright...silently preventing [the action] with no error
// shown")。解散群组同样不可逆、后果同样重大,却是本文件里唯一还在用 window.confirm() 的
// 危险操作——本文件自己在 OwnerSelect 里已经有现成的同款弹层视觉语言(.group-sheet-backdrop
// /.group-confirm-sheet),改成两段式状态复用它,不再依赖浏览器原生对话框。
const requestDissolve = () => {
// Backend already refuses to actually destroy the group for a non-owner (the same native // Backend already refuses to actually destroy the group for a non-owner (the same native
// {del hard:true} call it silently downgrades into "remove me from this group" instead) -- // {del hard:true} call it silently downgrades into "remove me from this group" instead) --
// but that response is indistinguishable from a real success, so without this gate a member // but that response is indistinguishable from a real success, so without this gate a member
// who somehow lands on this page got told "群聊已解散" when they'd really just left it. // who somehow lands on this page got told "群聊已解散" when they'd really just left it.
if (myRole !== 'owner') return; if (myRole !== 'owner') return;
if (!window.confirm('解散后群聊将无法恢复,确定要解散该群聊吗?')) return; setConfirmingDissolve(true);
};
const dissolve = () => {
const t = tinode ? tinode.getTopic(topic) : null; const t = tinode ? tinode.getTopic(topic) : null;
if (!t) return; if (!t) { setConfirmingDissolve(false); return; }
setDissolving(true); setDissolving(true);
t.delTopic(true).then(() => { onToast('群聊已解散'); navigate('group-list'); }) t.delTopic(true).then(() => { onToast('群聊已解散'); navigate('group-list'); })
.catch(err => onToast(err.message || '解散失败')).finally(() => setDissolving(false)); .catch(err => { onToast(err.message || '解散失败'); setConfirmingDissolve(false); }).finally(() => setDissolving(false));
}; };
if (!gov) return <div className="group-page"><Header title="群组管理" back="group-settings" backTopic={topic} /><main className="group-governance"><p className="group-inline-empty">{loadError ? '群组管理加载失败' : '加载中…'}</p></main></div>; if (!gov) return <div className="group-page"><Header title="群组管理" back="group-settings" backTopic={topic} /><main className="group-governance"><p className="group-inline-empty">{loadError ? '群组管理加载失败' : '加载中…'}</p></main></div>;
return <div className="group-page"> return <div className="group-page">
@@ -694,8 +704,14 @@ function Governance({topic, myRole, tinode, onToast}) {
<span><strong>{label}</strong><small>{desc}</small></span> <span><strong>{label}</strong><small>{desc}</small></span>
<Toggle checked={!!gov[key]} disabled={gov[key + '_editable'] === false} onChange={v => setField(key, v)} /> <Toggle checked={!!gov[key]} disabled={gov[key + '_editable'] === false} onChange={v => setField(key, v)} />
</div>)}</section> </div>)}</section>
<button type="button" className="group-danger" disabled={dissolving || myRole !== 'owner'} onClick={dissolve}>解散群组</button> <button type="button" className="group-danger" disabled={dissolving || myRole !== 'owner'} onClick={requestDissolve}>解散群组</button>
</main> </main>
{confirmingDissolve ? <div className="group-sheet-backdrop"><section className="group-confirm-sheet">
<h2>解散群组</h2>
<p>解散后群聊将无法恢复,确定要解散该群聊吗?</p>
<button type="button" className="danger" disabled={dissolving} onClick={dissolve}>{dissolving ? '处理中…' : '确定'}</button>
<button type="button" disabled={dissolving} onClick={() => setConfirmingDissolve(false)}>取消</button>
</section></div> : null}
</div>; </div>;
} }