Files
im_webapp/web/src/app/ConfirmDialogRoot.tsx
T
Your NameandClaude Sonnet 5 37d08e8989 feat(web): 用符合系统风格的组件替换全部 window.alert/confirm
全项目搜出15处 window.confirm、9处 window.alert(其中3处是各文件
各自复制的 notImplemented() 辅助函数,文案还都一样)用在退出登录、
删除消息/收藏/好友、清空聊天记录、解散群聊、转让群主、危险链接
二次确认等场景——全是浏览器原生弹窗,样式和整个应用的设计语言完全
脱节。

新增两个全局UI基础设施(Zustand store + 挂载在App.tsx根节点的
渲染组件,参照stores/calls.ts/messageJump.ts已有的"任何组件都能调用"
惯例,不是每个弹窗各自管理一份本地状态):

- stores/confirmDialog.ts + app/ConfirmDialogRoot.tsx:替换
  window.confirm。requestConfirm()返回Promise<boolean>,和
  window.confirm的返回值语义一致,所以原来"if (!window.confirm(...))
  return"这一行改成"if (!(await requestConfirm({message:...})))
  return",外层函数标成async,不用大改周围逻辑。危险/不可恢复操作
  (删除、解散群聊、注销账号等)传danger:true,确认按钮变红色,
  弹窗视觉沿用转发弹窗已有的"描边取消+实心确认"按钮搭配。
- stores/globalToast.ts + app/GlobalToastRoot.tsx:替换window.alert,
  复用应用里本来就有、每个页面各自本地状态管理的Toast组件。顺手把
  3个文件里各自重复定义、文案完全一样的notImplemented()合并成
  一个共享导出。

实机验证了非危险确认(清空缓存,蓝色确认按钮)、危险确认(删除
消息,红色确认按钮)、取消按钮、Escape键取消、全局提示(点"投诉
举报")——均正常工作,且原有页面各自的Toast错误提示不受影响。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 00:08:15 +08:00

43 lines
2.0 KiB
TypeScript

import { useEscapeKey } from '@/lib/hooks';
import { useConfirmDialogStore } from '@/stores/confirmDialog';
/** Mounted once at the app root (App.tsx) -- renders whatever `requestConfirm()` (confirmDialog.ts)
* most recently asked for. Visual language matches this app's other small action dialogs (e.g. the
* forward picker's outlined-gray "取消" + solid colored confirm button, `02-messages.dc.html`'s own
* spec for that pairing) rather than inventing a new button treatment just for this. */
export function ConfirmDialogRoot() {
const pending = useConfirmDialogStore((s) => s.pending);
const respond = useConfirmDialogStore((s) => s.respond);
// Always called (hooks can't be conditional) -- a no-op onEscape while nothing is pending is
// harmless, matching how every other modal in this app registers unconditionally too.
useEscapeKey(() => respond(false));
if (!pending) return null;
const { title, message, confirmLabel = '确定', cancelLabel = '取消', danger } = pending;
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-[#22262d]/25" onClick={() => respond(false)}>
<div className="w-[340px] rounded-xl bg-white p-5 shadow-modal" onClick={(e) => e.stopPropagation()}>
{title ? <strong className="mb-2 block text-base font-semibold text-text-heading">{title}</strong> : null}
<p className="m-0 text-sm leading-relaxed text-text-body">{message}</p>
<div className="mt-5 flex justify-end gap-3">
<button
type="button"
onClick={() => respond(false)}
className="grid h-9 place-items-center rounded-lg border border-[#d5d9df] px-4 text-sm text-text-secondary"
>
{cancelLabel}
</button>
<button
type="button"
onClick={() => respond(true)}
className={`grid h-9 place-items-center rounded-lg px-4 text-sm font-medium text-white ${danger ? 'bg-danger-text' : 'bg-btn'}`}
>
{confirmLabel}
</button>
</div>
</div>
</div>
);
}