[fix]:[FL-53][系统中所有的成功失败提示优化]

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-08 23:57:44 +08:00
parent 1adec62d6c
commit 533a325c86
17 changed files with 229 additions and 137 deletions
+5
View File
@@ -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` 组装。
+31
View File
@@ -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`,属于有意设计,不是遗漏。
+10 -14
View File
@@ -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() {
<div className="space-y-6">
{messageContextHolder}
{(error || datasetsQuery.error || jobsQuery.error || linesQuery.error) && (
<Alert
type="error"
showIcon
message={error || (datasetsQuery.error instanceof Error
? datasetsQuery.error.message
: jobsQuery.error instanceof Error
? jobsQuery.error.message
: linesQuery.error instanceof Error
? linesQuery.error.message
: "加载失败")}
/>
)}
<Card
title="高程数据集"
extra={(
@@ -21,6 +21,7 @@ import type { ColumnsType } from "antd/es/table";
import { useAuth } from "@/components/auth-provider";
import { Card } from "@/components/ui-antd";
import { useToastFeedback } from "@/hooks/use-toast-feedback";
import { readApiError } from "@/lib/api";
import type {
FaultRecurrenceAnalyzeResponse,
@@ -92,6 +93,11 @@ export default function AdminFaultRecurrencePage() {
|| hasPermission("tower.read")
|| hasPermission("tower.manage");
useToastFeedback({
errorMessage: error,
clearError: () => setError(""),
});
const analyzeMutation = useMutation({
mutationFn: async (file: File) => {
if (!canRead) {
+6 -12
View File
@@ -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() {
<div className="space-y-6">
{messageContextHolder}
{(listError || errorMessage) && (
<Alert
type="error"
showIcon
closable
message="操作失败"
description={listError || errorMessage}
onClose={() => setErrorMessage("")}
/>
)}
<Card className="shadow-sm" size="small">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
+24 -27
View File
@@ -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 (
<Space direction="vertical" size={16} className="w-full">
{(error || listError || sampleError || statsError || distributionError || towerBufferError || compareError || reportError) && (
<Alert
type="error"
showIcon
message="操作失败"
description={
error
|| listError
|| sampleError
|| statsError
|| distributionError
|| towerBufferError
|| compareError
|| reportError
}
/>
)}
{success && <Alert type="success" showIcon message="操作成功" description={success} />}
<Card title="线路参数准备">
<Space direction="vertical" size={12} className="w-full">
<Typography.Text type="secondary">
+6 -12
View File
@@ -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 (
<div className="space-y-6">
{error && (
<Alert
type="error"
showIcon
closable
message="操作失败"
description={<pre className="mb-0 whitespace-pre-wrap break-words">{error}</pre>}
onClose={() => setError("")}
/>
)}
<AntCard
title="菜单列表"
extra={
@@ -26,6 +26,7 @@ import { useAuth } from "@/components/auth-provider";
import { withBasePath } from "@/lib/base-path";
import { AtpX6Viewer } from "@/components/atp-x6-viewer";
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 { parseAtpTextToGraphJson, stringifyAtpGraphJson } from "@/lib/atp/parse-atp-text";
@@ -337,6 +338,18 @@ export default function PowerLinesAtpViewerPage() {
staleTime: 30_000,
});
const modelError = modelsQuery.error instanceof Error ? modelsQuery.error.message : "";
const versionError = versionsQuery.error instanceof Error ? versionsQuery.error.message : "";
const runError = runsQuery.error instanceof Error ? runsQuery.error.message : "";
const engineError = engineQuery.error instanceof Error ? engineQuery.error.message : "";
useToastFeedback({
errorMessage: error || modelError || versionError || runError || engineError,
successMessage: success,
clearError: () => 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 (
<Space direction="vertical" size={16} className="w-full">
{(error || modelError || versionError || runError || engineError) && (
<Alert type="error" showIcon message="操作失败" description={error || modelError || versionError || runError || engineError} />
)}
{success && <Alert type="success" showIcon message="操作成功" description={success} />}
<Card
title="ATP模型台账"
extra={(
+9 -8
View File
@@ -26,6 +26,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useAuth } from "@/components/auth-provider";
import { PowerLineCesiumMap } from "@/components/power-line-cesium-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 type {
@@ -654,6 +655,14 @@ export default function AdminPowerLinesPage() {
return (await response.json()) as TowerProfileDetail;
},
});
const lineError = linesQuery.error instanceof Error ? linesQuery.error.message : "";
const towerError = towersQuery.error instanceof Error ? towersQuery.error.message : "";
useToastFeedback({
errorMessage: error || lineError || towerError,
clearError: () => 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 (
<Card>
@@ -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 (
<>
<Space direction="vertical" size={16} className="w-full">
{(error || lineError || towerError) && (
<Alert type="error" showIcon message="操作失败" description={error || lineError || towerError} />
)}
<div
ref={panelScrollAnchorRef}
className="grid gap-4 xl:grid-cols-[360px_minmax(0,1fr)]"
+6 -12
View File
@@ -3,7 +3,6 @@
import Link from "next/link";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
Alert,
App,
Button,
Card,
@@ -24,6 +23,7 @@ import type { ColumnsType } from "antd/es/table";
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 type { MenuItem, RoleItem, RoleListResponse } from "@/types/auth";
@@ -66,6 +66,11 @@ export default function AdminRolesPage() {
const canRead = hasPermission("role.read") || hasPermission("role.manage");
const canManage = hasPermission("role.manage");
useToastFeedback({
errorMessage: error,
clearError: () => 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 (
<div className="space-y-6">
{error && (
<Alert
type="error"
showIcon
closable
message="操作失败"
description={<pre className="mb-0 whitespace-pre-wrap break-words">{error}</pre>}
onClose={() => setError("")}
/>
)}
<AntCard
title="角色列表"
extra={
+8 -23
View File
@@ -4,7 +4,6 @@ import Link from "next/link";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import {
Alert,
Button,
Empty,
Form,
@@ -20,6 +19,7 @@ import {
} from "antd";
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 type { SystemParamListResponse, SystemParamSummary } from "@/types/auth";
@@ -236,7 +236,13 @@ export default function AdminSystemParamsPage() {
const items = listQuery.data?.items ?? [];
const listError = listQuery.error instanceof Error ? listQuery.error.message : "";
const displayError = error || listError;
useToastFeedback({
errorMessage: error || listError,
successMessage: success,
clearError: () => setError(""),
clearSuccess: () => setSuccess(""),
});
const columns = useMemo<TableColumnsType<SystemParamSummary>>(() => {
const baseColumns: TableColumnsType<SystemParamSummary> = [
@@ -421,27 +427,6 @@ export default function AdminSystemParamsPage() {
return (
<div className="space-y-6">
{displayError && (
<Alert
type="error"
showIcon
closable={Boolean(error)}
message="操作失败"
description={<pre className="mb-0 whitespace-pre-wrap break-words">{displayError}</pre>}
onClose={() => setError("")}
/>
)}
{success && (
<Alert
type="success"
showIcon
closable
message="操作成功"
description={success}
onClose={() => setSuccess("")}
/>
)}
<div className="rounded-xl border border-[var(--gray-6)] bg-[var(--gray-1)] p-4 shadow-sm sm:p-5">
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<h2 className="text-base font-semibold text-[var(--gray-12)]"></h2>
+6 -5
View File
@@ -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 (
<Space direction="vertical" size={16} className="w-full">
{(error || listError) && (
<Alert type="error" showIcon message="操作失败" description={error || listError} />
)}
<Card
title="杆塔模型管理"
extra={canManage ? (
+12 -14
View File
@@ -2,7 +2,6 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
Alert,
Button,
Card,
Checkbox,
@@ -24,6 +23,7 @@ import Link from "next/link";
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type 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 type { RoleItem, RoleListResponse, UserListResponse, UserPublic } from "@/types/auth";
@@ -435,6 +435,13 @@ export default function AdminUsersPage() {
|| (rolesQuery.error instanceof Error ? rolesQuery.error.message : "");
const anyError = error || queryError;
useToastFeedback({
errorMessage: anyError,
successMessage: success,
clearError: () => 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 (
<div className="space-y-6">
{anyError && (
<Alert
type="error"
showIcon
closable
message="操作失败"
description={<pre className="mb-0 whitespace-pre-wrap break-words">{anyError}</pre>}
onClose={() => setError("")}
/>
)}
{success && <Alert type="success" showIcon closable message={success} onClose={() => setSuccess("")} />}
<AntCard
title="用户列表"
extra={(
+9 -3
View File
@@ -19,6 +19,7 @@ import type { ComponentType } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useAuth } from "@/components/auth-provider";
import { useToastFeedback } from "@/hooks/use-toast-feedback";
import { readApiError } from "@/lib/api";
type WineStatusResponse = {
@@ -160,6 +161,14 @@ export default function AdminWineRunnerPage() {
const canRead = hasPermission("wine.read") || hasPermission("wine.manage");
const canManage = hasPermission("wine.manage");
useToastFeedback({
errorMessage: error,
successMessage: success,
errorTitle: "执行失败",
clearError: () => setError(""),
clearSuccess: () => setSuccess(""),
});
const loadStatus = useCallback(async () => {
if (!user || !canRead) {
return;
@@ -308,9 +317,6 @@ export default function AdminWineRunnerPage() {
return (
<Space direction="vertical" size={16} style={{ width: "100%" }}>
{error ? <Alert type="error" showIcon message="操作失败" description={error} /> : null}
{success ? <Alert type="success" showIcon message={success} /> : null}
<Row gutter={[16, 16]}>
<Col xs={24} lg={8}>
<Card
+16
View File
@@ -171,6 +171,22 @@ body {
min-width: 0;
}
.ant-message {
width: auto !important;
max-width: min(420px, calc(100vw - 32px));
inset-inline: auto 16px !important;
}
.ant-message .ant-message-notice-wrapper {
padding-inline: 0;
text-align: end;
}
.ant-message .ant-message-notice-content {
max-width: min(420px, calc(100vw - 32px));
text-align: start;
}
@media (max-width: 767px) {
.admin-design-header {
padding-inline: 16px !important;
+6 -1
View File
@@ -565,7 +565,12 @@ export function Theme({
}}
>
<ConfigProvider locale={zhCN} theme={themeConfig}>
<AntApp>
<AntApp
message={{
top: 20,
duration: 3,
}}
>
<ThemeCssVarsScope>{children}</ThemeCssVarsScope>
</AntApp>
</ConfigProvider>
+56
View File
@@ -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]);
}