diff --git a/client/src/pages/ApprovalRulesPage.tsx b/client/src/pages/ApprovalRulesPage.tsx index 4cc8d54d..2f5dd21f 100644 --- a/client/src/pages/ApprovalRulesPage.tsx +++ b/client/src/pages/ApprovalRulesPage.tsx @@ -16,6 +16,9 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D import { Input } from "../components/ui/input"; import { Label } from "../components/ui/label"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; +import { toast } from "sonner"; +import { TableSkeleton } from "../components/common/TableSkeleton"; +import { ConfirmDialog } from "../components/common/ConfirmDialog"; const ROLE_OPTIONS: Array<{ code: ApprovalRoleCode; label: string }> = [ { code: "manager", label: "manager" }, @@ -45,6 +48,7 @@ export function ApprovalRulesPage() { const [editOpen, setEditOpen] = useState(false); const [editRule, setEditRule] = useState(null); + const [confirmToggleRule, setConfirmToggleRule] = useState(null); const listQuery = useQuery({ queryKey: ["approval-rules"], @@ -79,13 +83,12 @@ export function ApprovalRulesPage() { setLevel3Role(""); setEnabled(true); setError(""); + toast.success("规则已创建"); }, onError: (err) => { - if (isApiError(err)) { - setError(`${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`); - return; - } - setError(err instanceof Error ? err.message : "创建审批规则失败"); + const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "创建审批规则失败"; + setError(msg); + toast.error(msg); }, }); @@ -94,13 +97,12 @@ export function ApprovalRulesPage() { onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["approval-rules"] }); setError(""); + toast.success("状态已更新"); }, onError: (err) => { - if (isApiError(err)) { - setError(`${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`); - return; - } - setError(err instanceof Error ? err.message : "更新审批规则失败"); + const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "更新审批规则失败"; + setError(msg); + toast.error(msg); }, }); @@ -117,13 +119,12 @@ export function ApprovalRulesPage() { setEditOpen(false); setEditRule(null); setError(""); + toast.success("规则已更新"); }, onError: (err) => { - if (isApiError(err)) { - setError(`${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`); - return; - } - setError(err instanceof Error ? err.message : "更新审批规则失败"); + const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "更新审批规则失败"; + setError(msg); + toast.error(msg); }, }); @@ -283,7 +284,7 @@ export function ApprovalRulesPage() {

规则列表

- {listQuery.isLoading ?

加载中...

: null} + {listQuery.isLoading ? : null} {listQuery.isError ? (

@@ -348,10 +349,7 @@ export function ApprovalRulesPage() { variant="secondary" type="button" disabled={toggleEnabledMutation.isPending} - onClick={() => { - if (!window.confirm(`确认${rule.enabled ? "停用" : "启用"}该审批规则?`)) return; - toggleEnabledMutation.mutate(rule); - }} + onClick={() => setConfirmToggleRule(rule)} > {rule.enabled ? "停用" : "启用"} @@ -372,6 +370,13 @@ export function ApprovalRulesPage() { ) : null} + { if (!open) setConfirmToggleRule(null); }} + title={`确认${confirmToggleRule?.enabled ? "停用" : "启用"}该审批规则?`} + onConfirm={() => { if (confirmToggleRule) toggleEnabledMutation.mutate(confirmToggleRule); }} + /> +

diff --git a/client/src/pages/AuditLogsPage.tsx b/client/src/pages/AuditLogsPage.tsx index 81ad6927..bdcfc6e5 100644 --- a/client/src/pages/AuditLogsPage.tsx +++ b/client/src/pages/AuditLogsPage.tsx @@ -6,6 +6,7 @@ import { PageHeader } from "../components/common/PageHeader"; import { Button } from "../components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu"; import { downloadBlob } from "../lib/download"; +import { TableSkeleton } from "../components/common/TableSkeleton"; const DEFAULT_PAGE_SIZE = 20; @@ -190,8 +191,8 @@ export function AuditLogsPage() { {loading ? ( - - 加载中... + + ) : null} diff --git a/client/src/pages/BudgetsPage.tsx b/client/src/pages/BudgetsPage.tsx index 9adfcd53..5508cb32 100644 --- a/client/src/pages/BudgetsPage.tsx +++ b/client/src/pages/BudgetsPage.tsx @@ -10,6 +10,11 @@ import { PageHeader } from "../components/common/PageHeader"; import { Button } from "../components/ui/button"; import { Card } from "../components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; +import { toast } from "sonner"; +import { TableSkeleton } from "../components/common/TableSkeleton"; +import { StatusBadge } from "../components/common/StatusBadge"; +import { ConfirmDialog } from "../components/common/ConfirmDialog"; +import { Pagination } from "../components/common/Pagination"; const PAGE_SIZE = 20; @@ -42,6 +47,7 @@ export function BudgetsPage() { const [warningThresholdPercent, setWarningThresholdPercent] = useState("80"); const [formDepartmentId, setFormDepartmentId] = useState(""); const [error, setError] = useState(""); + const [confirmArchiveId, setConfirmArchiveId] = useState<{ id: string; code: string } | null>(null); const queryInput = useMemo( () => ({ @@ -87,9 +93,11 @@ export function BudgetsPage() { setWarningThresholdPercent("80"); setFormDepartmentId(""); setError(""); + toast.success("预算池已创建"); }, onError: (mutationError) => { setError(resolveError(mutationError)); + toast.error(resolveError(mutationError)); }, }); @@ -98,9 +106,11 @@ export function BudgetsPage() { onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["budgets"] }); setError(""); + toast.success("已归档"); }, onError: (mutationError) => { setError(resolveError(mutationError)); + toast.error(resolveError(mutationError)); }, }); @@ -246,89 +256,81 @@ export function BudgetsPage() {

预算池列表

-
- - - - 编码 - 名称 - 状态 - 期间 - 额度 - 预警阈值 - 操作 - - - - {(budgetsQuery.data?.items ?? []).length ? ( - (budgetsQuery.data?.items ?? []).map((item) => ( - - {item.code} - {item.name} - {item.status} - - {(item.periodStart ?? "-")} ~ {(item.periodEnd ?? "-")} - - - {item.usedAmount} / {item.allocatedAmount} -
剩余 {item.remainingAmount}
-
- {item.warningThresholdPercent}% - - {canManageBudget && item.status !== "archived" ? ( - - ) : ( - - - )} - -
- )) - ) : ( - - - 暂无预算池 - - - )} -
-
-
+ {budgetsQuery.isLoading ? : null} -
- - 第 {page} / {totalPages} 页 - -
- - -
-
+ {!budgetsQuery.isLoading ? ( + <> +
+ + + + 编码 + 名称 + 状态 + 期间 + 额度 + 预警阈值 + 操作 + + + + {(budgetsQuery.data?.items ?? []).length ? ( + (budgetsQuery.data?.items ?? []).map((item) => ( + + {item.code} + {item.name} + + + {(item.periodStart ?? "-")} ~ {(item.periodEnd ?? "-")} + + + {item.usedAmount} / {item.allocatedAmount} +
剩余 {item.remainingAmount}
+
+ {item.warningThresholdPercent}% + + {canManageBudget && item.status !== "archived" ? ( + + ) : ( + - + )} + +
+ )) + ) : ( + + + 暂无预算池 + + + )} +
+
+
+ + + + ) : null}
+ + { + if (confirmArchiveId) archiveMutation.mutate(confirmArchiveId.id); + }} + onOpenChange={(open) => { if (!open) setConfirmArchiveId(null); }} + /> ); } diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index b2d63592..64137cb2 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -11,6 +11,7 @@ import { } from "../api/dashboard"; import { fetchDepartments } from "../api/departments"; import { isApiError } from "../api/types"; +import { toast } from "sonner"; import { useAuth } from "../auth/AuthContext"; import { hasPermission } from "../auth/permissions"; import { PageHeader } from "../components/common/PageHeader"; @@ -104,6 +105,10 @@ export function DashboardPage() { }), onSuccess: (response, format) => { downloadBlob(response.blob, response.filename ?? `dashboard-reimbursements.${format}`); + toast.success("导出完成"); + }, + onError: (err) => { + toast.error(err instanceof Error ? err.message : "导出失败"); }, }); diff --git a/client/src/pages/DelegationsPage.tsx b/client/src/pages/DelegationsPage.tsx index 76ba9951..31f37729 100644 --- a/client/src/pages/DelegationsPage.tsx +++ b/client/src/pages/DelegationsPage.tsx @@ -1,10 +1,13 @@ import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; import { mapApiErrorMessage } from "../api/errorMessages"; import { isApiError } from "../api/types"; import { createDelegation, fetchMyDelegations, revokeDelegation } from "../api/delegations"; import { fetchWorkflowApprovers } from "../api/workflow"; import { PageHeader } from "../components/common/PageHeader"; +import { TableSkeleton } from "../components/common/TableSkeleton"; +import { StatusBadge } from "../components/common/StatusBadge"; import { Button } from "../components/ui/button"; import { Card } from "../components/ui/card"; import { Input } from "../components/ui/input"; @@ -46,13 +49,12 @@ export function DelegationsPage() { onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["delegations"] }); setError(""); + toast.success("委派已创建"); }, onError: (err) => { - if (isApiError(err)) { - setError(`${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`); - return; - } - setError(err instanceof Error ? err.message : "创建委派失败"); + const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "创建委派失败"; + setError(msg); + toast.error(msg); }, }); @@ -61,13 +63,12 @@ export function DelegationsPage() { onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["delegations"] }); setError(""); + toast.success("已撤销"); }, onError: (err) => { - if (isApiError(err)) { - setError(`${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`); - return; - } - setError(err instanceof Error ? err.message : "撤销委派失败"); + const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "撤销委派失败"; + setError(msg); + toast.error(msg); }, }); @@ -158,7 +159,7 @@ export function DelegationsPage() { - {listQuery.isLoading ?

加载中...

: null} + {listQuery.isLoading ? : null} {listQuery.isError ? (

{isApiError(listQuery.error) @@ -193,7 +194,7 @@ export function DelegationsPage() { {item.id} {item.delegateEmployeeId} {item.scope} - {item.status} + @@ -236,6 +237,17 @@ export function DepartmentsPage() { ) : null} + + { + if (confirmArchive) archiveMutation.mutate(confirmArchive.id); + }} + onOpenChange={(open) => { if (!open) setConfirmArchive(null); }} + /> ); } diff --git a/client/src/pages/FeatureFlagsPage.tsx b/client/src/pages/FeatureFlagsPage.tsx index eb4e5cbe..ea57386e 100644 --- a/client/src/pages/FeatureFlagsPage.tsx +++ b/client/src/pages/FeatureFlagsPage.tsx @@ -8,9 +8,11 @@ import { type FeatureFlagOverrideMode, type FeatureFlagRolloutType, } from "../api/featureFlags"; +import { toast } from "sonner"; import { mapApiErrorMessage } from "../api/errorMessages"; import { isApiError } from "../api/types"; import { PageHeader } from "../components/common/PageHeader"; +import { TableSkeleton } from "../components/common/TableSkeleton"; import { Button } from "../components/ui/button"; import { Card } from "../components/ui/card"; import { Label } from "../components/ui/label"; @@ -89,13 +91,12 @@ export function FeatureFlagsPage() { onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["feature-flags"] }); setError(""); + toast.success("开关已更新"); }, onError: (err) => { - if (isApiError(err)) { - setError(`${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`); - return; - } - setError(err instanceof Error ? err.message : "更新开关失败"); + const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "更新开关失败"; + setError(msg); + toast.error(msg); }, }); @@ -141,7 +142,7 @@ export function FeatureFlagsPage() {

开关列表

- {flagsQuery.isLoading ?

加载中...

: null} + {flagsQuery.isLoading ? : null} {flagsQuery.isError ? (

diff --git a/client/src/pages/MobileApprovalsPage.tsx b/client/src/pages/MobileApprovalsPage.tsx index d117febd..5a409209 100644 --- a/client/src/pages/MobileApprovalsPage.tsx +++ b/client/src/pages/MobileApprovalsPage.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; import { mapApiErrorMessage } from "../api/errorMessages"; import { isApiError } from "../api/types"; import { @@ -13,6 +14,7 @@ import { } from "../api/workflow"; import { PageHeader } from "../components/common/PageHeader"; import { Button } from "../components/ui/button"; +import { Spinner } from "../components/ui/spinner"; import { Card } from "../components/ui/card"; import { Textarea } from "../components/ui/textarea"; @@ -85,14 +87,12 @@ export function MobileApprovalsPage() { setTransferReason(""); setTransferToApproverId(""); setError(""); + toast.success("操作成功"); }, onError: (err) => { - if (isApiError(err)) { - setError(`${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`); - return; - } - - setError(err instanceof Error ? err.message : "动作执行失败"); + const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "动作执行失败"; + setError(msg); + toast.error(msg); }, }); @@ -110,7 +110,7 @@ export function MobileApprovalsPage() {

待办任务

- {tasksQuery.isLoading ?

加载中...

: null} + {tasksQuery.isLoading ?
加载中...
: null} {!tasksQuery.isLoading && pendingTasks.length === 0 ? (

暂无待办任务。

diff --git a/client/src/pages/MobileCapturePage.tsx b/client/src/pages/MobileCapturePage.tsx index 8b3d381c..39908f5f 100644 --- a/client/src/pages/MobileCapturePage.tsx +++ b/client/src/pages/MobileCapturePage.tsx @@ -1,10 +1,12 @@ import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; import { uploadInvoice } from "../api/invoicePool"; import { mapApiErrorMessage } from "../api/errorMessages"; import { isApiError } from "../api/types"; import { PageHeader } from "../components/common/PageHeader"; import { Button } from "../components/ui/button"; +import { Spinner } from "../components/ui/spinner"; import { Card } from "../components/ui/card"; import { enqueueInvoiceUpload, @@ -47,15 +49,20 @@ export function MobileCapturePage() { onSuccess: () => { setSelectedFile(null); setError(""); - setMessage("上传成功"); + setMessage(""); + toast.success("上传成功"); }, onError: (err) => { if (isApiError(err)) { - setError(`${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`); + const msg = `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`; + setError(msg); + toast.error(msg); return; } - setError(err instanceof Error ? err.message : "上传失败"); + const msg = err instanceof Error ? err.message : "上传失败"; + setError(msg); + toast.error(msg); }, }); @@ -80,10 +87,13 @@ export function MobileCapturePage() { onSuccess: async (result) => { await queryClient.invalidateQueries({ queryKey: ["mobile", "invoice-upload-queue"] }); setError(""); - setMessage(`队列同步完成:成功 ${result.succeeded},失败 ${result.failed}`); + setMessage(""); + toast.success(`同步完成:成功 ${result.succeeded},失败 ${result.failed}`); }, onError: (err) => { - setError(err instanceof Error ? err.message : "队列同步失败"); + const msg = err instanceof Error ? err.message : "队列同步失败"; + setError(msg); + toast.error(msg); }, }); @@ -210,7 +220,7 @@ export function MobileCapturePage() { {queuedCountLabel} - {queueQuery.isLoading ?

加载中...

: null} + {queueQuery.isLoading ?
加载中...
: null} {!queueQuery.isLoading && queuedItems.length === 0 ? (

暂无待上传文件。

diff --git a/client/src/pages/NotificationsPage.tsx b/client/src/pages/NotificationsPage.tsx index 353d24f5..1b415aff 100644 --- a/client/src/pages/NotificationsPage.tsx +++ b/client/src/pages/NotificationsPage.tsx @@ -4,6 +4,8 @@ import { fetchNotifications, markNotificationRead } from "../api/notifications"; import { isApiError } from "../api/types"; import { mapApiErrorMessage } from "../api/errorMessages"; import { PageHeader } from "../components/common/PageHeader"; +import { TableSkeleton } from "../components/common/TableSkeleton"; +import { Pagination } from "../components/common/Pagination"; import { Button } from "../components/ui/button"; import { Card } from "../components/ui/card"; import { @@ -105,7 +107,7 @@ export function NotificationsPage() { - {listQuery.isLoading ?

加载中...

: null} + {listQuery.isLoading ? : null} {listQuery.isError ? (

@@ -185,38 +187,12 @@ export function NotificationsPage() { ) : null} -

- - 第 {page} / {totalPages} 页 - -
- - -
-
+ setSearchParams({ unreadOnly: unreadOnly ? "1" : "0", page: String(p) })} + testIdPrefix="notifications" + />
); diff --git a/client/src/pages/WorkflowTaskDetailPage.tsx b/client/src/pages/WorkflowTaskDetailPage.tsx index c76f432f..73a3f4f3 100644 --- a/client/src/pages/WorkflowTaskDetailPage.tsx +++ b/client/src/pages/WorkflowTaskDetailPage.tsx @@ -11,9 +11,12 @@ import { returnWorkflowTask, transferWorkflowTask, } from "../api/workflow"; +import { toast } from "sonner"; import { useAuth } from "../auth/AuthContext"; import { hasPermission } from "../auth/permissions"; +import { CardSkeleton } from "../components/common/CardSkeleton"; import { PageHeader } from "../components/common/PageHeader"; +import { StatusBadge } from "../components/common/StatusBadge"; import { Button } from "../components/ui/button"; import { Card } from "../components/ui/card"; import { Textarea } from "../components/ui/textarea"; @@ -70,27 +73,26 @@ export function WorkflowTaskDetailPage() { return transferWorkflowTask(taskId, selectedApprover, transferReason.trim()); }, - onSuccess: async () => { + onSuccess: async (_data, action) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["workflow", "tasks"] }), queryClient.invalidateQueries({ queryKey: ["workflow", "task", taskId] }), ]); setActionError(""); + const messages: Record = { approve: "已通过", reject: "已驳回", return: "已退回", transfer: "已转审" }; + toast.success(messages[action] ?? "操作成功"); }, onError: (error) => { + let msg: string; if (error instanceof Error && !isApiError(error)) { - setActionError(error.message); - return; + msg = error.message; + } else if (isApiError(error)) { + msg = `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`; + } else { + msg = "动作执行失败,请稍后重试"; } - - if (isApiError(error)) { - setActionError( - `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`, - ); - return; - } - - setActionError("动作执行失败,请稍后重试"); + setActionError(msg); + toast.error(msg); }, }); @@ -104,9 +106,7 @@ export function WorkflowTaskDetailPage() { if (detailQuery.isLoading) { return ( - -

加载任务详情中...

-
+ ); } @@ -148,7 +148,7 @@ export function WorkflowTaskDetailPage() { /> -
状态:{detail.task.status}
+
状态:
实例:{detail.workflowInstance?.id ?? "-"}
报销单号:{detail.reimbursement?.formNumber ?? "-"}
报销金额:{detail.reimbursement?.totalAmount ?? "-"}
diff --git a/client/src/pages/WorkflowTasksPage.tsx b/client/src/pages/WorkflowTasksPage.tsx index b127dc39..fa6dccac 100644 --- a/client/src/pages/WorkflowTasksPage.tsx +++ b/client/src/pages/WorkflowTasksPage.tsx @@ -4,6 +4,9 @@ import { isApiError } from "../api/types"; import { mapApiErrorMessage } from "../api/errorMessages"; import { fetchWorkflowTasks } from "../api/workflow"; import { PageHeader } from "../components/common/PageHeader"; +import { Pagination } from "../components/common/Pagination"; +import { StatusBadge } from "../components/common/StatusBadge"; +import { TableSkeleton } from "../components/common/TableSkeleton"; import { Button } from "../components/ui/button"; import { Card } from "../components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; @@ -60,16 +63,16 @@ export function WorkflowTasksPage() { - {query.isLoading ?

加载中...

: null} + {query.isLoading ? : null} {query.isError ? (

{isApiError(query.error) ? `${query.error.code}: ${mapApiErrorMessage( - query.error.code, - query.error.details, - query.error.message, - )}` + query.error.code, + query.error.details, + query.error.message, + )}` : "加载待办失败,请稍后重试"}

) : null} @@ -97,7 +100,7 @@ export function WorkflowTasksPage() { {task.workflowInstance?.entityId} - {task.status} + {new Date(task.createdAt).toLocaleString()} - - - + setSearchParams({ status, page: String(p) })} + testIdPrefix="workflow-tasks" + />
);