feat(08-05-interaction-pattern-unification): 统一前端交互与反馈模式
这个提交包含在:
@@ -1,9 +1,9 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { searchGlobal, type GlobalSearchHit } from "../api/search";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog";
|
||||
@@ -21,7 +21,7 @@ function entityLabel(entityType: string) {
|
||||
if (entityType === "employee") return "人员";
|
||||
if (entityType === "announcement") return "公告";
|
||||
if (entityType === "weekly_report") return "周报";
|
||||
if (entityType === "todo") return "待办";
|
||||
if (entityType === "todo") return "我的事项";
|
||||
if (entityType === "reimbursement") return "报销";
|
||||
if (entityType === "leave") return "请假";
|
||||
if (entityType === "purchase_request") return "采购";
|
||||
@@ -116,12 +116,12 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp
|
||||
}, [searchQuery.data?.items]);
|
||||
|
||||
const message = useMemo(() => {
|
||||
if (!trimmed) return "输入姓名、公告、项目、合同、资产、请假、报销、周报或待办关键字开始搜索。";
|
||||
if (!trimmed) return "输入姓名、公告、项目、合同、资产、请假、报销、周报或事项关键字开始搜索。";
|
||||
if (trimmed.length < 2) return "至少输入 2 个字符。";
|
||||
if (searchQuery.isLoading) return "搜索中...";
|
||||
if (searchQuery.isError) {
|
||||
return isApiError(searchQuery.error)
|
||||
? `${searchQuery.error.code}: ${mapApiErrorMessage(searchQuery.error.code, searchQuery.error.details, searchQuery.error.message)}`
|
||||
? resolveUserErrorMessage(searchQuery.error)
|
||||
: "搜索失败,请稍后重试";
|
||||
}
|
||||
if (!groupedItems.length) return "未找到匹配结果。";
|
||||
@@ -137,7 +137,7 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder="搜索人员、项目、合同、公告、资产、请假、报销、周报、待办"
|
||||
placeholder="搜索人员、项目、合同、公告、资产、请假、报销、周报、事项"
|
||||
value={keyword}
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
/>
|
||||
|
||||
@@ -47,7 +47,7 @@ export function StatusBadge({
|
||||
status: string;
|
||||
label?: string;
|
||||
}) {
|
||||
const config = STATUS_MAP[status] || { label: status, className: "badge-neutral" };
|
||||
const config = STATUS_MAP[status] || { label: "未知状态", className: "badge-neutral" };
|
||||
|
||||
return (
|
||||
<span className={cn(config.className, className)} {...props}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../../lib/resolveErrorMessage";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { mapApiErrorMessage } from "../../api/errorMessages";
|
||||
import {
|
||||
archiveHolidayCalendar,
|
||||
createHolidayCalendar,
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
type HolidayCalendarDayType,
|
||||
type LegalEntityItem,
|
||||
} from "../../api/masterData";
|
||||
import { isApiError } from "../../api/types";
|
||||
import { ConfirmDialog } from "../common/ConfirmDialog";
|
||||
import { StatusBadge } from "../common/StatusBadge";
|
||||
import { TableSkeleton } from "../common/TableSkeleton";
|
||||
@@ -42,13 +41,7 @@ type DraftDay = {
|
||||
};
|
||||
|
||||
function resolveError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "操作失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "操作失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function toEnabledFilter(value: "" | "true" | "false") {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { mapApiErrorMessage } from "../../api/errorMessages";
|
||||
import {
|
||||
archiveSupplier,
|
||||
createSupplier,
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
type LegalEntityItem,
|
||||
type SupplierItem,
|
||||
} from "../../api/masterData";
|
||||
import { isApiError } from "../../api/types";
|
||||
import { ConfirmDialog } from "../common/ConfirmDialog";
|
||||
import { StatusBadge } from "../common/StatusBadge";
|
||||
import { TableSkeleton } from "../common/TableSkeleton";
|
||||
@@ -26,13 +25,7 @@ type SupplierTabProps = {
|
||||
};
|
||||
|
||||
function resolveError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "操作失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "操作失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function toEnabledFilter(value: "" | "true" | "false") {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { resolveErrorMessage } from "../../lib/resolveErrorMessage";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
type BarcodeScannerPanelProps = {
|
||||
@@ -22,13 +23,6 @@ type BrowserWithBarcodeDetector = Window & {
|
||||
|
||||
const SCAN_FORMATS = ["qr_code", "code_128", "code_39", "ean_13", "ean_8", "upc_a", "upc_e"];
|
||||
|
||||
function resolveErrorMessage(error: unknown) {
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message;
|
||||
}
|
||||
return "无法启动相机扫码,请改用手工输入。";
|
||||
}
|
||||
|
||||
export function BarcodeScannerPanel({ disabled = false, onDetected }: BarcodeScannerPanelProps) {
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const mediaStreamRef = useRef<MediaStream | null>(null);
|
||||
@@ -166,9 +160,9 @@ export function BarcodeScannerPanel({ disabled = false, onDetected }: BarcodeSca
|
||||
await startWithBarcodeDetector();
|
||||
setIsStarting(false);
|
||||
return;
|
||||
} catch (detectorError) {
|
||||
} catch {
|
||||
stopScanner();
|
||||
setError(`BarcodeDetector 启动失败,已回退到 ZXing:${resolveErrorMessage(detectorError)}`);
|
||||
setError("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +170,7 @@ export function BarcodeScannerPanel({ disabled = false, onDetected }: BarcodeSca
|
||||
await startWithZxing();
|
||||
} catch (fallbackError) {
|
||||
stopScanner();
|
||||
setError(resolveErrorMessage(fallbackError));
|
||||
setError(resolveErrorMessage(fallbackError, "无法启动相机扫码,请改用手工输入。"));
|
||||
}
|
||||
|
||||
setIsStarting(false);
|
||||
|
||||
@@ -32,7 +32,7 @@ export const NAV_ITEMS: NavItem[] = [
|
||||
{ to: "/announcements", label: "公告", icon: "campaign" },
|
||||
{ to: "/projects", label: "项目", icon: "workspaces", anyOf: ["project.view_own", "project.view_all", "project.manage"] },
|
||||
{ to: "/system/settings", label: "管理", icon: "settings", anyOf: MANAGEMENT_ROOT_PERMISSIONS },
|
||||
{ to: "/workflow/tasks", label: "待我处理", icon: "fact_check", anyOf: ["expense.approve", "leave.approve", "attendance.approve", "procurement.approve", "travel.approve"] },
|
||||
{ to: "/workflow/tasks", label: "待我审批", icon: "fact_check", anyOf: ["expense.approve", "leave.approve", "attendance.approve", "procurement.approve", "travel.approve"] },
|
||||
{ to: "/contracts-registry", label: "合同", icon: "description", anyOf: ["project.view_all", "project.manage", "system.settings"] },
|
||||
{ to: "/purchase-requests", label: "采购", icon: "shopping_cart", anyOf: ["procurement.create", "procurement.view_own", "procurement.view_all", "procurement.manage"] },
|
||||
{ to: "/travel-requests", label: "差旅申请", icon: "flight", anyOf: ["travel.view_all", "travel.manage", "travel.payment.manage"] },
|
||||
@@ -46,10 +46,14 @@ export const NAV_ITEMS: NavItem[] = [
|
||||
|
||||
const ACCESSIBLE_LABELS: Record<string, string> = {
|
||||
"/reimbursements": "我的报销",
|
||||
"/workflow/tasks": "待我处理",
|
||||
"/workflow/tasks": "待我审批",
|
||||
"/assets": "资产",
|
||||
};
|
||||
|
||||
const PRODUCT_LABELS: Record<string, string> = {
|
||||
"/workflow/tasks": "待我审批",
|
||||
};
|
||||
|
||||
const TEAM_NAVIGATION_ROLE_CODES = ["manager", "ceo", "finance_mgr", "hr", "admin"];
|
||||
const PAYMENT_REVIEWER_ROLE_CODES = ["manager", "ceo"];
|
||||
|
||||
@@ -177,7 +181,12 @@ function orderItems(items: ResolvedNavItem[], order: string[]) {
|
||||
}
|
||||
|
||||
function toResolved(items: NavigationItem[]) {
|
||||
return items.map((item) => ({ key: item.key, href: item.href, label: item.label, icon: item.icon }));
|
||||
return items.map((item) => ({
|
||||
key: item.key,
|
||||
href: item.href,
|
||||
label: PRODUCT_LABELS[item.href] ?? item.label,
|
||||
icon: item.icon,
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeSections(sections: { main: NavigationItem[]; admin: NavigationItem[]; backup: NavigationItem[] }) {
|
||||
|
||||
@@ -9,38 +9,19 @@ import {
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
|
||||
type WelcomeLinkItem = {
|
||||
export type WelcomeLinkItem = {
|
||||
icon: string;
|
||||
label: string;
|
||||
description: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
const WELCOME_LINKS: WelcomeLinkItem[] = [
|
||||
{
|
||||
icon: "fact_check",
|
||||
label: "处理待办",
|
||||
description: "先看今天需要审批或跟进的事项",
|
||||
to: "/workflow/tasks",
|
||||
},
|
||||
{
|
||||
icon: "request_quote",
|
||||
label: "提交报销",
|
||||
description: "录入票据和报销说明,查看进度",
|
||||
to: "/reimbursements",
|
||||
},
|
||||
{
|
||||
icon: "event_available",
|
||||
label: "请假记录",
|
||||
description: "日常请假走企业微信,OA 查看记录和状态",
|
||||
to: "/leave-requests",
|
||||
},
|
||||
];
|
||||
|
||||
export function WelcomeDialog({
|
||||
links,
|
||||
open,
|
||||
onClose,
|
||||
}: {
|
||||
links: WelcomeLinkItem[];
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
@@ -50,30 +31,32 @@ export function WelcomeDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>常用入口</DialogTitle>
|
||||
<DialogDescription>
|
||||
你的账号已就绪。先从工作台处理待办,也可以直接进入常用业务。
|
||||
你的账号已就绪。下面只展示当前账号可用的常用业务入口。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
{WELCOME_LINKS.map((item) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
to={item.to}
|
||||
onClick={onClose}
|
||||
className="rounded-lg border border-zinc-200 bg-white px-4 py-4 shadow-sm transition-colors hover:bg-zinc-50"
|
||||
>
|
||||
<div className="flex h-full flex-col gap-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-md bg-primary/10 text-primary">
|
||||
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
|
||||
{links.length ? (
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
{links.map((item) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
to={item.to}
|
||||
onClick={onClose}
|
||||
className="rounded-lg border border-zinc-200 bg-white px-4 py-4 shadow-sm transition-colors hover:bg-zinc-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2"
|
||||
>
|
||||
<div className="flex h-full flex-col gap-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-md bg-primary/10 text-primary">
|
||||
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium text-zinc-900">{item.label}</div>
|
||||
<div className="mt-1 text-sm leading-6 text-zinc-500">{item.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium text-zinc-900">{item.label}</div>
|
||||
<div className="mt-1 text-sm leading-6 text-zinc-500">{item.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm text-zinc-600">
|
||||
关闭后不再显示这个提示;所有功能都可以从左侧菜单进入。
|
||||
|
||||
@@ -3,6 +3,10 @@ import { Link } from "react-router-dom";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Button } from "../ui/button";
|
||||
import { WorkspaceHeader } from "./ListWorkspace";
|
||||
import {
|
||||
MOBILE_STICKY_ACTION_PAGE_CLASS,
|
||||
MobileStickyActionBar,
|
||||
} from "./MobileStickyActionBar";
|
||||
import { SectionPanel } from "./SectionPanel";
|
||||
|
||||
export function BusinessEditTemplate({
|
||||
@@ -45,7 +49,15 @@ export function BusinessEditTemplate({
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className={cn("page-scaffold", bottomActions && "pb-24 md:pb-0", className)} data-testid={testId}>
|
||||
<div
|
||||
className={cn(
|
||||
"page-scaffold",
|
||||
bottomActions && MOBILE_STICKY_ACTION_PAGE_CLASS,
|
||||
bottomActions && "md:pb-0",
|
||||
className,
|
||||
)}
|
||||
data-testid={testId}
|
||||
>
|
||||
<WorkspaceHeader eyebrow={eyebrow} title={title} meta={meta} actions={headerActions} />
|
||||
{validation ? <BusinessEditValidationSummary>{validation}</BusinessEditValidationSummary> : null}
|
||||
{children}
|
||||
@@ -149,14 +161,5 @@ export function BusinessEditActionBar({
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mobile-sticky-action-surface fixed inset-x-0 bottom-0 z-50 border-t border-zinc-200 bg-white/95 shadow-[0_-10px_24px_rgba(15,23,42,0.10)] backdrop-blur md:static md:inset-auto md:mt-5 md:rounded-lg md:border md:bg-white md:p-3 md:shadow-none",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="mobile-sticky-action-grid md:flex md:justify-end md:gap-2">{children}</div>
|
||||
</div>
|
||||
);
|
||||
return <MobileStickyActionBar className={className} inlineFromMedium>{children}</MobileStickyActionBar>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export const MOBILE_STICKY_ACTION_PAGE_CLASS =
|
||||
"mobile-fixed-action-page pb-[calc(var(--mobile-sticky-action-height,78px)+env(safe-area-inset-bottom,0px)+1.25rem)]";
|
||||
|
||||
export function MobileStickyActionBar({
|
||||
children,
|
||||
className,
|
||||
contentClassName,
|
||||
inlineFromMedium = false,
|
||||
testId,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
inlineFromMedium?: boolean;
|
||||
testId?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mobile-sticky-action-surface fixed inset-x-0 bottom-0 z-50 border-t border-zinc-200 bg-white/95 shadow-[0_-10px_24px_rgba(15,23,42,0.10)] backdrop-blur",
|
||||
inlineFromMedium && "md:static md:inset-auto md:mt-5 md:rounded-lg md:border md:bg-white md:p-3 md:shadow-none",
|
||||
className,
|
||||
)}
|
||||
data-testid={testId}
|
||||
>
|
||||
<div className={cn("mobile-sticky-action-grid", inlineFromMedium && "md:flex md:justify-end md:gap-2", contentClassName)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../../lib/resolveErrorMessage";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -5,8 +6,6 @@ import {
|
||||
type PerformanceEmployeeReviewTimeline,
|
||||
type PerformanceReviewDimensionType,
|
||||
} from "../../api/performance";
|
||||
import { mapApiErrorMessage } from "../../api/errorMessages";
|
||||
import { isApiError } from "../../api/types";
|
||||
import { StatusBadge } from "../common/StatusBadge";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
@@ -57,10 +56,7 @@ function buildFormState(review: PerformanceEmployeeReviewTimeline["items"][numbe
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown, fallback: string) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
function cycleStatusLabel(status: string) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../../lib/resolveErrorMessage";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -8,8 +9,6 @@ import {
|
||||
type EmployeeSalaryProfileSnapshot,
|
||||
type Payslip,
|
||||
} from "../../api/payroll";
|
||||
import { mapApiErrorMessage } from "../../api/errorMessages";
|
||||
import { isApiError } from "../../api/types";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
|
||||
@@ -17,10 +16,7 @@ import { toast } from "sonner";
|
||||
import { StatusBadge } from "../common/StatusBadge";
|
||||
|
||||
function errorMessage(error: unknown, fallback: string) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
function todayDateString() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../../lib/resolveErrorMessage";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { updateReimbursement } from "../../api/reimbursements";
|
||||
import { isApiError } from "../../api/types";
|
||||
import { mapApiErrorMessage } from "../../api/errorMessages";
|
||||
import { useUnsavedChangesGuard } from "../common/UnsavedChangesGuard";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
@@ -58,7 +58,7 @@ export function ReimbursementHeaderEditor({
|
||||
},
|
||||
onError: (mutationError) => {
|
||||
setError(isApiError(mutationError)
|
||||
? mapApiErrorMessage(mutationError.code, mutationError.details, mutationError.message)
|
||||
? resolveUserErrorMessage(mutationError)
|
||||
: "保存报销信息失败,请重试");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { mapApiErrorMessage } from "../../api/errorMessages";
|
||||
import {
|
||||
bindInvoice,
|
||||
fetchInvoicePoolList,
|
||||
type InvoicePoolListItem,
|
||||
} from "../../api/invoicePool";
|
||||
import { isApiError } from "../../api/types";
|
||||
import {
|
||||
fetchTravelPaymentTrack,
|
||||
markTravelOrderPaid,
|
||||
@@ -22,13 +21,7 @@ import { Button } from "../ui/button";
|
||||
import { TravelOrderMarkPaidDialog } from "./TravelOrderMarkPaidDialog";
|
||||
|
||||
function resolveError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function freezeReasonFromAnomaly(code: string): "amount_mismatch" | "currency_mismatch" | "missing_order" | "duplicate" | "manual_review" {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../../lib/resolveErrorMessage";
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { mapApiErrorMessage } from '../../api/errorMessages';
|
||||
import { useAuth } from '../../auth/AuthContext';
|
||||
import { hasPermission } from '../../auth/permissions';
|
||||
import { fetchCostCenters, fetchLegalEntities } from '../../api/masterData';
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
type TravelReconciliationBatchStatus,
|
||||
type TravelReconciliationItem,
|
||||
} from '../../api/travelReconciliation';
|
||||
import { isApiError } from '../../api/types';
|
||||
import { TravelReconciliationBatchCard } from './TravelReconciliationBatchCard';
|
||||
import { TravelReconciliationItemTable } from './TravelReconciliationItemTable';
|
||||
import { CardSkeleton } from '../common/CardSkeleton';
|
||||
@@ -33,13 +32,7 @@ import { Textarea } from '../ui/textarea';
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
function resolveError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return '请求失败,请稍后重试。';
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function currentMonthRange() {
|
||||
|
||||
在新工单中引用
屏蔽一个用户