feat: 完成P6-P8前端能力闭环(V3,D2,D3,B3)

这个提交包含在:
2026-02-10 11:28:51 +08:00
父节点 6bc09f557b
当前提交 4e5a7793fe
共修改 14 个文件,包含 1151 行新增和 62 行删除
+83
查看文件
@@ -0,0 +1,83 @@
import { apiRequestData } from "./http";
export type BudgetPool = {
id: string;
code: string;
name: string;
departmentId: string | null;
status: "active" | "archived";
periodStart: string | null;
periodEnd: string | null;
allocatedAmount: string;
usedAmount: string;
remainingAmount: string;
warningThresholdPercent: number;
archivedAt: string | null;
createdAt: string | null;
updatedAt: string | null;
};
export type BudgetListResponse = {
page: number;
pageSize: number;
total: number;
items: BudgetPool[];
};
export type BudgetListQuery = {
status?: "active" | "archived";
departmentId?: string;
page?: number;
pageSize?: number;
};
function toQueryString(query: BudgetListQuery = {}) {
const params = new URLSearchParams();
if (query.status) params.set("status", query.status);
if (query.departmentId) params.set("departmentId", query.departmentId);
if (query.page != null) params.set("page", String(query.page));
if (query.pageSize != null) params.set("pageSize", String(query.pageSize));
const encoded = params.toString();
return encoded ? `?${encoded}` : "";
}
export async function fetchBudgets(query: BudgetListQuery = {}) {
return apiRequestData<BudgetListResponse>(`/api/budgets${toQueryString(query)}`);
}
export async function createBudget(payload: {
code: string;
name: string;
departmentId?: string | null;
periodStart: string;
periodEnd: string;
allocatedAmount: string;
warningThresholdPercent?: number;
}) {
return apiRequestData<BudgetPool>(`/api/budgets`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function updateBudget(
budgetId: string,
payload: Partial<{
name: string;
allocatedAmount: string;
warningThresholdPercent: number;
status: "active" | "archived";
}>,
) {
return apiRequestData<BudgetPool>(`/api/budgets/${budgetId}`, {
method: "PATCH",
body: JSON.stringify(payload),
});
}
export async function archiveBudget(budgetId: string) {
return apiRequestData<BudgetPool>(`/api/budgets/${budgetId}/archive`, {
method: "POST",
body: JSON.stringify({}),
});
}
+60
查看文件
@@ -0,0 +1,60 @@
import { apiRequestData } from "./http";
export type DashboardQuery = {
dateFrom?: string;
dateTo?: string;
departmentId?: string;
limit?: number;
};
function toQueryString(query: DashboardQuery = {}) {
const params = new URLSearchParams();
if (query.dateFrom) params.set("dateFrom", query.dateFrom);
if (query.dateTo) params.set("dateTo", query.dateTo);
if (query.departmentId) params.set("departmentId", query.departmentId);
if (query.limit != null) params.set("limit", String(query.limit));
const encoded = params.toString();
return encoded ? `?${encoded}` : "";
}
export type DashboardExpenseOverview = {
totalCount: number;
totalAmount: string;
paidCount: number;
paidAmount: string;
reviewingCount: number;
};
export type DashboardTrendPoint = {
date: string;
amount: string;
count: number;
};
export type DashboardTopCategory = {
code: string;
name: string;
amount: string;
count: number;
};
export type DashboardApprovalEfficiency = {
approvedCount: number;
averageHours: number;
};
export async function fetchDashboardOverview(query: DashboardQuery = {}) {
return apiRequestData<DashboardExpenseOverview>(`/api/dashboard/expense-overview${toQueryString(query)}`);
}
export async function fetchDashboardTrends(query: DashboardQuery = {}) {
return apiRequestData<{ points: DashboardTrendPoint[] }>(`/api/dashboard/trends${toQueryString(query)}`);
}
export async function fetchDashboardTopCategories(query: DashboardQuery = {}) {
return apiRequestData<{ items: DashboardTopCategory[] }>(`/api/dashboard/top-categories${toQueryString(query)}`);
}
export async function fetchDashboardApprovalEfficiency(query: DashboardQuery = {}) {
return apiRequestData<DashboardApprovalEfficiency>(`/api/dashboard/approval-efficiency${toQueryString(query)}`);
}
+29
查看文件
@@ -114,6 +114,35 @@ export function mapApiErrorMessage(
if (details?.ocr === "disabled") {
return "当前环境未开启 OCR,请联系管理员或切换到 mock/tencent。";
}
if (details?.verification === "disabled") {
return "当前环境未开启发票验真。";
}
if (details?.verification === "already_pending") {
return "已有验真任务正在处理中,请稍后查看结果。";
}
if (details?.verification === "too_frequent") {
return "验真触发过于频繁,请稍后重试。";
}
if (details?.verification === "attempts_exceeded") {
return "验真重试次数已达上限,请联系财务人工核验。";
}
if (details?.budget === "exceeded_block_mode") {
return "当前报销金额超出预算,系统已阻断提交。";
}
if (details?.integration === "erp_sync_disabled") {
return "ERP 同步功能未开启。";
}
if (details?.integration === "travel_sync_disabled") {
return "差旅同步功能未开启。";
}
}
return fallbackMessage || "请求失败,请稍后重试。";
+15 -1
查看文件
@@ -41,5 +41,19 @@ export async function exportAuditLogs(params: {
return apiRequestBlob(`/api/exports/audit-logs?${qs.toString()}`);
}
export type { ApiBlobResponse };
export async function exportDashboardReimbursements(params: {
format: "csv" | "xlsx";
dateFrom?: string;
dateTo?: string;
departmentId?: string;
}) {
const qs = new URLSearchParams();
qs.set("format", params.format);
if (params.dateFrom) qs.set("dateFrom", params.dateFrom);
if (params.dateTo) qs.set("dateTo", params.dateTo);
if (params.departmentId) qs.set("departmentId", params.departmentId);
return apiRequestBlob(`/api/exports/dashboard/reimbursements?${qs.toString()}`);
}
export type { ApiBlobResponse };
+62
查看文件
@@ -0,0 +1,62 @@
import { apiRequestData } from "./http";
export async function triggerErpSync(reimbursementId: string, runNow = true) {
return apiRequestData<{
outboxId: string;
status: string;
duplicated: boolean;
}>(`/api/integrations/erp/sync/${reimbursementId}?runNow=${runNow ? "1" : "0"}`, {
method: "POST",
body: JSON.stringify({}),
});
}
export async function importTravelOrders(payload: {
orders: Array<{
source: string;
externalOrderNo: string;
employeeId: string;
linkedReimbursementId?: string;
occurredAt?: string;
amount?: number;
currency?: string;
}>;
}) {
return apiRequestData<{
requested: number;
imported: number;
skipped: Array<{ source: string; externalOrderNo: string; reason: string }>;
items: Array<{
id: string;
source: string;
externalOrderNo: string;
employeeId: string;
linkedReimbursementId: string | null;
}>;
}>(`/api/travel/orders/import`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function testNotificationChannel(payload: {
channelType: string;
name: string;
}) {
return apiRequestData<{
channel: {
id: string;
channelType: string;
name: string;
enabled: boolean;
};
testResult: {
ok: boolean;
code: string;
response?: Record<string, unknown>;
};
}>(`/api/notifications/channels/test`, {
method: "POST",
body: JSON.stringify(payload),
});
}
+44
查看文件
@@ -2,6 +2,22 @@ import { apiRequestData } from "./http";
export type InvoicePoolStatus = "pending_ocr" | "ocr_failed" | "available" | "bound" | "archived";
export type InvoiceVerificationRecord = {
id: string;
provider: string;
status: string;
resultCode: string | null;
resultMessage: string | null;
errorCode: string | null;
errorMessage: string | null;
durationMs: number | null;
requestedBy: string | null;
requestedAt: string | null;
completedAt: string | null;
retryOfId: string | null;
attempts: number;
};
export type InvoicePoolListItem = {
id: string;
status: InvoicePoolStatus;
@@ -21,6 +37,7 @@ export type InvoicePoolListItem = {
originalName: string;
mimeType: string;
fileSize: number;
verification: InvoiceVerificationRecord | null;
owner: {
id: string;
employeeNo: string;
@@ -75,6 +92,7 @@ export type InvoicePoolDetail = {
overrideRefFormNumber: string | null;
overrideReason: string | null;
overriddenBy: string | null;
verification: InvoiceVerificationRecord | null;
createdAt: string | null;
updatedAt: string | null;
};
@@ -193,3 +211,29 @@ export async function overrideInvoiceDuplicate(invoiceId: string, payload: { ref
body: JSON.stringify(payload),
});
}
export async function triggerInvoiceVerification(invoiceId: string) {
return apiRequestData<{ invoiceId: string; verificationId: string; provider: string; triggerMode: string }>(
`/api/invoice-pool/${invoiceId}/verify`,
{
method: "POST",
body: JSON.stringify({}),
},
);
}
export async function fetchInvoiceVerifications(invoiceId: string, limit = 20) {
return apiRequestData<{ invoiceId: string; items: InvoiceVerificationRecord[] }>(
`/api/invoice-pool/${invoiceId}/verification?limit=${limit}`,
);
}
export async function retryInvoiceVerification(invoiceId: string) {
return apiRequestData<{ invoiceId: string; verificationId: string; status: string }>(
`/api/invoice-pool/${invoiceId}/verification/retry`,
{
method: "POST",
body: JSON.stringify({}),
},
);
}
+34 -1
查看文件
@@ -59,6 +59,22 @@ export type ReimbursementListResponse = {
items: ReimbursementListItem[];
};
export type ReimbursementBudgetHint = {
budgetId: string;
budgetCode: string;
budgetName: string;
controlMode: "warn" | "block";
warningThresholdPercent: number;
allocatedAmount: string;
usedAmount: string;
remainingAmount: string;
currentAmount: string;
nextUsedAmount: string;
nextUsagePercent: number;
exceedsThreshold: boolean;
exceedsBudget: boolean;
};
export type ReimbursementDetail = {
id: string;
formNumber: string | null;
@@ -86,6 +102,7 @@ export type ReimbursementDetail = {
invoiceNumber: string | null;
totalAmount: string | null;
}>;
budgetHint: ReimbursementBudgetHint | null;
pendingWorkflow: {
instanceId: string;
stepId: string;
@@ -135,6 +152,22 @@ export type ReimbursementItemDeleteResponse = {
totalAmount: string;
};
export type ReimbursementSubmitResponse = {
reimbursementId: string;
formNumber: string;
workflowInstanceId: string;
currentTaskId: string;
status: "reviewing";
budget: {
budgetId: string;
budgetCode: string;
controlMode: "warn" | "block";
usagePercent: number;
exceededThreshold: boolean;
exceededBudget: boolean;
} | null;
};
function toQueryString(query: Record<string, string | number | undefined>) {
const params = new URLSearchParams();
@@ -171,7 +204,7 @@ export async function createReimbursement(payload: {
}
export async function submitReimbursement(reimbursementId: string) {
return apiRequestData<Record<string, unknown>>(`/api/reimbursements/${reimbursementId}/submit`, {
return apiRequestData<ReimbursementSubmitResponse>(`/api/reimbursements/${reimbursementId}/submit`, {
method: "POST",
body: JSON.stringify({}),
});