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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
76156a1c75
commit
37d08e8989
@@ -0,0 +1,48 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
/**
|
||||
* Replaces `window.confirm()` app-wide (README's own design language never intended a native
|
||||
* browser dialog to show up inside this UI -- it was just the fastest way to gate a destructive
|
||||
* action while the real dialog didn't exist yet). A global store + a single `<ConfirmDialogRoot>`
|
||||
* mounted once at the app root (see App.tsx), not a local `useState` per call site: this needs to be
|
||||
* callable from any event handler, including ones with no natural "own a piece of JSX for this"
|
||||
* home (e.g. GroupFilesModal's per-row delete button), the same reasoning `stores/calls.ts`/
|
||||
* `stores/messageJump.ts` already established for other "any component, any time" actions.
|
||||
*
|
||||
* `requestConfirm()` returns a Promise the same shape `window.confirm()`'s return value has (resolves
|
||||
* true/false, never rejects) so every existing `if (!window.confirm(...)) return;` call site becomes
|
||||
* `if (!(await requestConfirm({ message: ... }))) return;` with the enclosing function marked async --
|
||||
* a mechanical, low-risk edit, not a rewrite of the surrounding logic.
|
||||
*/
|
||||
export interface ConfirmOptions {
|
||||
title?: string;
|
||||
message: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
/** Red confirm button for irreversible/destructive actions (delete, disband, deregister) --
|
||||
* omitted (default blue) for merely consequential ones (log out, switch tenant). */
|
||||
danger?: boolean;
|
||||
}
|
||||
|
||||
interface PendingConfirm extends ConfirmOptions {
|
||||
resolve: (value: boolean) => void;
|
||||
}
|
||||
|
||||
interface ConfirmDialogState {
|
||||
pending: PendingConfirm | null;
|
||||
respond: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const useConfirmDialogStore = create<ConfirmDialogState>((set, get) => ({
|
||||
pending: null,
|
||||
respond: (value) => {
|
||||
get().pending?.resolve(value);
|
||||
set({ pending: null });
|
||||
},
|
||||
}));
|
||||
|
||||
export function requestConfirm(options: ConfirmOptions): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
useConfirmDialogStore.setState({ pending: { ...options, resolve } });
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
/**
|
||||
* Replaces `window.alert()` app-wide, same rationale as `stores/confirmDialog.ts`'s replacement of
|
||||
* `window.confirm()`: a global store + a single `<GlobalToastRoot>` mounted once at the app root
|
||||
* renders the app's own existing `Toast` component (already used everywhere for a mutation's error
|
||||
* state) instead of the browser's native dialog. `notify()` is deliberately fire-and-forget (no
|
||||
* Promise, no return value) -- an alert-style message never blocked on a user response the way
|
||||
* confirm did, so there's nothing to await.
|
||||
*/
|
||||
interface GlobalToastState {
|
||||
message: string;
|
||||
variant: 'success' | 'error';
|
||||
token: number;
|
||||
}
|
||||
|
||||
export const useGlobalToastStore = create<GlobalToastState>(() => ({
|
||||
message: '',
|
||||
variant: 'error',
|
||||
token: 0,
|
||||
}));
|
||||
|
||||
export function notify(message: string, variant: 'success' | 'error' = 'error') {
|
||||
// `token` (not just `message`) is what Toast's own auto-dismiss effect keys off of via the caller
|
||||
// -- see GlobalToastRoot -- so firing the SAME message twice in a row (e.g. two clicks on a
|
||||
// still-disabled "该功能暂未开放" button before the first toast dismisses) restarts the timer
|
||||
// instead of the second call being a no-op change that React skips re-rendering for.
|
||||
useGlobalToastStore.setState((s) => ({ message, variant, token: s.token + 1 }));
|
||||
}
|
||||
|
||||
/** Every existing `window.alert('该功能暂未开放,如需联系客服。')` call site (settings, composer
|
||||
* toolbar, add-friend, group/contact detail panels) used the identical hand-typed Chinese string --
|
||||
* one shared helper instead of that string living independently in five files that could each drift
|
||||
* from the others if the wording ever needs to change. */
|
||||
export function notImplemented() {
|
||||
notify('该功能暂未开放,如需联系客服。');
|
||||
}
|
||||
Reference in New Issue
Block a user