From 533a325c861e361044d85c711b2caa988b375cf8 Mon Sep 17 00:00:00 2001 From: chengkai3 Date: Mon, 8 Jun 2026 23:57:44 +0800 Subject: [PATCH] =?UTF-8?q?[fix]:[FL-53][=E7=B3=BB=E7=BB=9F=E4=B8=AD?= =?UTF-8?q?=E6=89=80=E6=9C=89=E7=9A=84=E6=88=90=E5=8A=9F=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E4=BC=98=E5=8C=96]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- MEMORY.md | 5 ++ memory/2026-06-08.md | 31 ++++++++++ web/src/app/admin/elevation/page.tsx | 24 ++++---- web/src/app/admin/fault-recurrence/page.tsx | 6 ++ web/src/app/admin/files/page.tsx | 18 ++---- web/src/app/admin/lightning-currents/page.tsx | 51 ++++++++--------- web/src/app/admin/menus/page.tsx | 18 ++---- .../app/admin/power-lines/atp-viewer/page.tsx | 19 +++++-- web/src/app/admin/power-lines/page.tsx | 17 +++--- web/src/app/admin/roles/page.tsx | 18 ++---- web/src/app/admin/system-params/page.tsx | 31 +++------- web/src/app/admin/tower-models/page.tsx | 11 ++-- web/src/app/admin/users/page.tsx | 26 ++++----- web/src/app/admin/wine-runner/page.tsx | 12 +++- web/src/app/globals.css | 16 ++++++ web/src/components/ui-antd.tsx | 7 ++- web/src/hooks/use-toast-feedback.ts | 56 +++++++++++++++++++ 17 files changed, 229 insertions(+), 137 deletions(-) create mode 100644 web/src/hooks/use-toast-feedback.ts diff --git a/MEMORY.md b/MEMORY.md index dd8c74a..78a8b62 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -254,6 +254,11 @@ - Phase B 样板页已落地:`/admin/users`、`/admin/requirements`、`/admin/menus`;后续页面迁移默认保持“业务逻辑不动,仅替换操作入口承载组件”的最小改动策略。 - 后台左侧导航默认不展示“系统菜单”标题与底部“当前角色/账号状态”文案,避免重复信息占用导航空间(移动端抽屉同样不显示该标题)。 +## 前端提示反馈口径(2026-06-08) + +- 全局瞬时成功/失败反馈统一使用 Ant Design `message`,展示在页面右上角,并在短时间后自动消失。 +- `Alert` 仅保留给需要常驻占位的状态:权限不足、列表加载失败、解析警告、空态说明等;不要再把普通 CRUD 成功/失败提示固定渲染在页面顶部。 + ## 数据库连接口径(2026-04-23) - API 默认数据库连接切换为本地 PostgreSQL:优先读取 `DATABASE_URL`;若未设置则由 `DB_HOST/DB_PORT/DB_NAME/DB_USERNAME/DB_PASSWORD` 组装。 diff --git a/memory/2026-06-08.md b/memory/2026-06-08.md index 6bbb65b..de65e3a 100644 --- a/memory/2026-06-08.md +++ b/memory/2026-06-08.md @@ -116,3 +116,34 @@ - 风险与关注点: - 已经以错误编码写入数据库的历史 ATP 文本不会被自动修复;本次修复只覆盖后续上传与预览入口。 + +## Work Log - 全局成功失败提示切换为右上角弹消息(2026-06-08) + +- 背景: + - 后台多个页面仍把普通 CRUD 成功/失败提示渲染在页面顶部,占用列表与表单空间。 + - 现有页面已经混用 Ant Design `message` 与顶部 `Alert`,交互不统一。 + +- 本次处理: + - `web/src/components/ui-antd.tsx` + - 为全局 Ant Design `message` 统一配置顶部偏移与默认停留时长。 + - `web/src/app/globals.css` + - 将 `message` 容器样式调整为右上角浮层展示。 + - `web/src/hooks/use-toast-feedback.ts` + - 新增轻量 hook,用于把页面本地 `error/success` 状态统一转成右上角自动消失的提示。 + - 后台页面: + - `users`、`system-params`、`wine-runner`、`lightning-currents`、`power-lines/atp-viewer` + - `roles`、`menus`、`files`、`tower-models`、`elevation`、`power-lines`、`fault-recurrence` + - 去掉顶部“操作成功/失败”提示条,改为右上角弹消息;保留权限不足、加载失败、解析提醒等需要常驻占位的 `Alert`。 + +- 验证: + - 基线: + - 初次 `npm --workspace web exec tsc --noEmit --pretty false` 因当前环境缺少 `web/node_modules` 且默认 npm cache 不可写未能执行。 + - 补装依赖后,基线 `NPM_CONFIG_CACHE=/tmp/fquiz-npm-cache npm --workspace web exec tsc --noEmit --pretty false` 通过。 + - 修改后: + - `NPM_CONFIG_CACHE=/tmp/fquiz-npm-cache npm --workspace web exec tsc --noEmit --pretty false` 通过。 + - `NPM_CONFIG_CACHE=/tmp/fquiz-npm-cache npm --workspace web exec eslint src/components/ui-antd.tsx src/hooks/use-toast-feedback.ts src/app/admin/users/page.tsx src/app/admin/system-params/page.tsx src/app/admin/wine-runner/page.tsx src/app/admin/lightning-currents/page.tsx src/app/admin/power-lines/atp-viewer/page.tsx src/app/admin/roles/page.tsx src/app/admin/menus/page.tsx src/app/admin/files/page.tsx src/app/admin/tower-models/page.tsx src/app/admin/elevation/page.tsx src/app/admin/power-lines/page.tsx src/app/admin/fault-recurrence/page.tsx` + - 仅剩仓库原有 warning(`users`/`tower-models` 的 hooks 依赖与 `img` 提示),无新增 error。 + - `git diff --check` 通过。 + +- 风险与关注点: + - 本次只统一“瞬时成功/失败反馈”;权限态、加载态、解析告警等长驻提示仍保留 `Alert`,属于有意设计,不是遗漏。 diff --git a/web/src/app/admin/elevation/page.tsx b/web/src/app/admin/elevation/page.tsx index e9d5a3b..d77459c 100644 --- a/web/src/app/admin/elevation/page.tsx +++ b/web/src/app/admin/elevation/page.tsx @@ -27,6 +27,7 @@ import type { ColumnsType } from "antd/es/table"; import { useAuth } from "@/components/auth-provider"; import { ElevationPreviewCesiumMap } from "@/components/elevation-preview-cesium-map"; import { Card } from "@/components/ui-antd"; +import { useToastFeedback } from "@/hooks/use-toast-feedback"; import { useTopicSubscription } from "@/hooks/use-topic-subscription"; import { getApiBaseUrl, readApiError } from "@/lib/api"; import { readLinePreparation } from "@/lib/line-preparation"; @@ -214,6 +215,15 @@ export default function AdminElevationPage() { }); }, [queryClient]); + useToastFeedback({ + errorMessage: + error + || (datasetsQuery.error instanceof Error ? datasetsQuery.error.message : "") + || (jobsQuery.error instanceof Error ? jobsQuery.error.message : "") + || (linesQuery.error instanceof Error ? linesQuery.error.message : ""), + clearError: () => setError(""), + }); + const refreshPowerLines = useCallback(async () => { await queryClient.invalidateQueries({ predicate: (query) => @@ -725,20 +735,6 @@ export default function AdminElevationPage() {
{messageContextHolder} - {(error || datasetsQuery.error || jobsQuery.error || linesQuery.error) && ( - - )} - setError(""), + }); + const analyzeMutation = useMutation({ mutationFn: async (file: File) => { if (!canRead) { diff --git a/web/src/app/admin/files/page.tsx b/web/src/app/admin/files/page.tsx index e8dfdf8..fd277a5 100644 --- a/web/src/app/admin/files/page.tsx +++ b/web/src/app/admin/files/page.tsx @@ -13,7 +13,6 @@ import { Table as AntTable, Typography, Upload, - Alert, Progress, Dropdown, message as antdMessage, @@ -38,6 +37,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } import { useAuth } from "@/components/auth-provider"; import { Button, Card } from "@/components/ui-antd"; +import { useToastFeedback } from "@/hooks/use-toast-feedback"; import { useTopicSubscription } from "@/hooks/use-topic-subscription"; import { readApiError } from "@/lib/api"; import type { @@ -539,6 +539,11 @@ export default function AdminFilesPage() { }; const listError = filesQuery.error instanceof Error ? filesQuery.error.message : ""; + + useToastFeedback({ + errorMessage: errorMessage || listError, + clearError: () => setErrorMessage(""), + }); const listData = filesQuery.data; const items = listData?.items ?? []; const operationBusy = @@ -820,17 +825,6 @@ export default function AdminFilesPage() {
{messageContextHolder} - {(listError || errorMessage) && ( - setErrorMessage("")} - /> - )} -
diff --git a/web/src/app/admin/lightning-currents/page.tsx b/web/src/app/admin/lightning-currents/page.tsx index ee9559e..307dc47 100644 --- a/web/src/app/admin/lightning-currents/page.tsx +++ b/web/src/app/admin/lightning-currents/page.tsx @@ -25,6 +25,7 @@ import { useCallback, useMemo, useRef, useState, type CSSProperties } from "reac import { useAuth } from "@/components/auth-provider"; import { LightningDistributionMap } from "@/components/lightning-distribution-map"; import { Card } from "@/components/ui-antd"; +import { useToastFeedback } from "@/hooks/use-toast-feedback"; import { useTopicSubscription } from "@/hooks/use-topic-subscription"; import { readApiError } from "@/lib/api"; import { readLinePreparation } from "@/lib/line-preparation"; @@ -396,6 +397,29 @@ export default function AdminLightningCurrentsPage() { }, }); + const listError = eventsQuery.error instanceof Error ? eventsQuery.error.message : ""; + const sampleError = samplesQuery.error instanceof Error ? samplesQuery.error.message : ""; + const statsError = exceedanceQuery.error instanceof Error ? exceedanceQuery.error.message : ""; + const distributionError = distributionStatsQuery.error instanceof Error ? distributionStatsQuery.error.message : ""; + const towerBufferError = towerBufferQuery.error instanceof Error ? towerBufferQuery.error.message : ""; + const compareError = syntheticCompareQuery.error instanceof Error ? syntheticCompareQuery.error.message : ""; + const reportError = reportQuery.error instanceof Error ? reportQuery.error.message : ""; + + useToastFeedback({ + errorMessage: + error + || listError + || sampleError + || statsError + || distributionError + || towerBufferError + || compareError + || reportError, + successMessage: success, + clearError: () => setError(""), + clearSuccess: () => setSuccess(""), + }); + const refreshAll = useCallback(async () => { await queryClient.invalidateQueries({ predicate: (query) => @@ -797,35 +821,8 @@ export default function AdminLightningCurrentsPage() { ); } - const listError = eventsQuery.error instanceof Error ? eventsQuery.error.message : ""; - const sampleError = samplesQuery.error instanceof Error ? samplesQuery.error.message : ""; - const statsError = exceedanceQuery.error instanceof Error ? exceedanceQuery.error.message : ""; - const distributionError = distributionStatsQuery.error instanceof Error ? distributionStatsQuery.error.message : ""; - const towerBufferError = towerBufferQuery.error instanceof Error ? towerBufferQuery.error.message : ""; - const compareError = syntheticCompareQuery.error instanceof Error ? syntheticCompareQuery.error.message : ""; - const reportError = reportQuery.error instanceof Error ? reportQuery.error.message : ""; - return ( - {(error || listError || sampleError || statsError || distributionError || towerBufferError || compareError || reportError) && ( - - )} - {success && } - diff --git a/web/src/app/admin/menus/page.tsx b/web/src/app/admin/menus/page.tsx index d0d3cbb..780a853 100644 --- a/web/src/app/admin/menus/page.tsx +++ b/web/src/app/admin/menus/page.tsx @@ -3,7 +3,6 @@ import Link from "next/link"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { - Alert, App, Button, Card, @@ -26,6 +25,7 @@ import { import type { CSSProperties, ComponentType } from "react"; import { useAuth } from "@/components/auth-provider"; +import { useToastFeedback } from "@/hooks/use-toast-feedback"; import { useTopicSubscription } from "@/hooks/use-topic-subscription"; import { readApiError } from "@/lib/api"; import { normalizeAppRoutePath } from "@/lib/app-route-path"; @@ -110,6 +110,11 @@ export default function AdminMenusPage() { const canRead = hasPermission("menu.read") || hasPermission("menu.manage"); const canManage = hasPermission("menu.manage"); + useToastFeedback({ + errorMessage: error, + clearError: () => setError(""), + }); + const parentOptions = useMemo( () => menus.map((menu) => ({ @@ -496,17 +501,6 @@ export default function AdminMenusPage() { return (
- {error && ( - {error}} - onClose={() => setError("")} - /> - )} - setError(""), + clearSuccess: () => setSuccess(""), + }); + const refreshModels = useCallback(async () => { await queryClient.invalidateQueries({ predicate: (query) => @@ -881,14 +894,8 @@ export default function PowerLinesAtpViewerPage() { const versionError = currentVersionQuery.error instanceof Error ? currentVersionQuery.error.message : ""; const runError = runsQuery.error instanceof Error ? runsQuery.error.message : ""; const engineError = engineQuery.error instanceof Error ? engineQuery.error.message : ""; - return ( - {(error || modelError || versionError || runError || engineError) && ( - - )} - {success && } - setError(""), + }); + const towerProfileGeometryParseResult = useMemo(() => { try { return { @@ -1374,7 +1383,6 @@ export default function AdminPowerLinesPage() { ); const mapHeight = Math.max(POWER_LINES_MAP_MIN_HEIGHT, rightContentHeight - 32); const towerTableScrollY = Math.max(POWER_LINES_TABLE_MIN_SCROLL_Y, rightContentHeight - 54); - if (initializing || linesQuery.isLoading) { return ( @@ -1409,16 +1417,9 @@ export default function AdminPowerLinesPage() { ); } - const lineError = linesQuery.error instanceof Error ? linesQuery.error.message : ""; - const towerError = towersQuery.error instanceof Error ? towersQuery.error.message : ""; - return ( <> - {(error || lineError || towerError) && ( - - )} -
setError(""), + }); + const menuOptions = useMemo( () => menus.map((menu) => ({ value: menu.id, label: `${menu.name} (${menu.code})` })), [menus], @@ -417,17 +422,6 @@ export default function AdminRolesPage() { return (
- {error && ( - {error}} - onClose={() => setError("")} - /> - )} - setError(""), + clearSuccess: () => setSuccess(""), + }); const columns = useMemo>(() => { const baseColumns: TableColumnsType = [ @@ -421,27 +427,6 @@ export default function AdminSystemParamsPage() { return (
- {displayError && ( - {displayError}} - onClose={() => setError("")} - /> - )} - {success && ( - setSuccess("")} - /> - )} -

参数列表

diff --git a/web/src/app/admin/tower-models/page.tsx b/web/src/app/admin/tower-models/page.tsx index 29092f3..a87416e 100644 --- a/web/src/app/admin/tower-models/page.tsx +++ b/web/src/app/admin/tower-models/page.tsx @@ -3,7 +3,6 @@ import Link from "next/link"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { - Alert, App, Button, Empty, @@ -26,6 +25,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, typ import { useAuth } from "@/components/auth-provider"; import { Card } from "@/components/ui-antd"; +import { useToastFeedback } from "@/hooks/use-toast-feedback"; import { useTopicSubscription } from "@/hooks/use-topic-subscription"; import { readApiError } from "@/lib/api"; import type { @@ -402,6 +402,11 @@ export default function AdminTowerModelsPage() { }); const listError = towerModelsQuery.error instanceof Error ? towerModelsQuery.error.message : ""; + + useToastFeedback({ + errorMessage: error || listError, + clearError: () => setError(""), + }); const listData = towerModelsQuery.data; const listItems = listData?.items ?? []; const totalItems = listData?.total ?? listItems.length; @@ -771,10 +776,6 @@ export default function AdminTowerModelsPage() { return ( - {(error || listError) && ( - - )} - setError(""), + clearSuccess: () => setSuccess(""), + }); + const updateTableScrollY = useCallback(() => { if (typeof window === "undefined") { return; @@ -462,7 +469,10 @@ export default function AdminUsersPage() { }, []); useEffect(() => { - updateTableScrollY(); + if (typeof window === "undefined") { + return; + } + window.requestAnimationFrame(updateTableScrollY); }, [anyError, pagination.current, pagination.pageSize, users.length, usersQuery.isFetching, updateTableScrollY]); useEffect(() => { @@ -660,18 +670,6 @@ export default function AdminUsersPage() { return (
- {anyError && ( - {anyError}} - onClose={() => setError("")} - /> - )} - {success && setSuccess("")} />} - setError(""), + clearSuccess: () => setSuccess(""), + }); + const loadStatus = useCallback(async () => { if (!user || !canRead) { return; @@ -308,9 +317,6 @@ export default function AdminWineRunnerPage() { return ( - {error ? : null} - {success ? : null} - - + {children} diff --git a/web/src/hooks/use-toast-feedback.ts b/web/src/hooks/use-toast-feedback.ts new file mode 100644 index 0000000..7360b19 --- /dev/null +++ b/web/src/hooks/use-toast-feedback.ts @@ -0,0 +1,56 @@ +"use client"; + +import { App } from "antd"; +import { useEffect, useRef } from "react"; + +type ToastFeedbackOptions = { + errorTitle?: string; + successTitle?: string; + errorMessage: string; + successMessage?: string; + clearError?: () => void; + clearSuccess?: () => void; +}; + +export function useToastFeedback({ + errorTitle = "操作失败", + successTitle = "操作成功", + errorMessage, + successMessage, + clearError, + clearSuccess, +}: ToastFeedbackOptions) { + const { message } = App.useApp(); + const lastErrorRef = useRef(""); + const lastSuccessRef = useRef(""); + + useEffect(() => { + if (!errorMessage) { + lastErrorRef.current = ""; + return; + } + if (!errorMessage || errorMessage === lastErrorRef.current) { + return; + } + lastErrorRef.current = errorMessage; + message.error({ + content: `${errorTitle}:${errorMessage}`, + }); + clearError?.(); + }, [clearError, errorMessage, errorTitle, message]); + + useEffect(() => { + if (!successMessage) { + lastSuccessRef.current = ""; + return; + } + if (!successMessage || successMessage === lastSuccessRef.current) { + return; + } + lastSuccessRef.current = successMessage; + message.success({ + content: successTitle === "操作成功" ? successMessage : `${successTitle}:${successMessage}`, + }); + clearSuccess?.(); + }, [clearSuccess, message, successMessage, successTitle]); +}