[fix]:[FL-53][系统中所有的成功失败提示优化]
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -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` 组装。
|
||||
|
||||
@@ -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`,属于有意设计,不是遗漏。
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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={(
|
||||
|
||||
@@ -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)]"
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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={(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -565,7 +565,12 @@ export function Theme({
|
||||
}}
|
||||
>
|
||||
<ConfigProvider locale={zhCN} theme={themeConfig}>
|
||||
<AntApp>
|
||||
<AntApp
|
||||
message={{
|
||||
top: 20,
|
||||
duration: 3,
|
||||
}}
|
||||
>
|
||||
<ThemeCssVarsScope>{children}</ThemeCssVarsScope>
|
||||
</AntApp>
|
||||
</ConfigProvider>
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
Reference in New Issue
Block a user