feat: 凭证生成、成本分析、AI问答、前端页面、集成测试与E2E测试
- 后端: 凭证生成引擎、金蝶导出器、凭证模板服务 - 后端: 成本分析服务、AI问答服务 - 后端: 科目映射CRUD API、分析API、QA API - 后端: 集成测试(认证/任务/凭证) 49个测试全部通过 - 前端: 凭证管理、成本分析、导出中心、知识库、系统设置页面 - 前端: AuthGuard认证守卫、Dashboard AI聊天功能 - 前端: Playwright E2E测试 16 passed, 1 skipped - 基础设施: Docker Compose、Nginx反向代理、.env.example - 文档: 用户手册、管理员手册、发布检查清单
This commit is contained in:
@@ -0,0 +1,581 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { motion } from "motion/react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Receipt,
|
||||
Download,
|
||||
Loader2,
|
||||
CheckCircle,
|
||||
FileText,
|
||||
Settings,
|
||||
Plus,
|
||||
Pencil,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { api } from "@/lib/api/client";
|
||||
|
||||
interface VoucherEntry {
|
||||
account_code: string;
|
||||
account_name: string;
|
||||
debit_amount: number;
|
||||
credit_amount: number;
|
||||
summary: string;
|
||||
department?: string;
|
||||
}
|
||||
|
||||
interface Voucher {
|
||||
id: number;
|
||||
voucher_number: string;
|
||||
voucher_date: string;
|
||||
period: string;
|
||||
summary: string;
|
||||
entries: VoucherEntry[];
|
||||
total_debit: number;
|
||||
total_credit: number;
|
||||
status: string;
|
||||
confirmed_by?: number;
|
||||
confirmed_at?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface AccountMapping {
|
||||
id: number;
|
||||
standard_field: string;
|
||||
debit_account: string;
|
||||
debit_account_name: string;
|
||||
credit_account: string;
|
||||
credit_account_name: string;
|
||||
cost_center?: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
const statusConfig: Record<string, { label: string; color: string }> = {
|
||||
DRAFT: { label: "草稿", color: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300" },
|
||||
CONFIRMED: { label: "已确认", color: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300" },
|
||||
EXPORTED: { label: "已导出", color: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300" },
|
||||
};
|
||||
|
||||
function formatCurrency(value: number): string {
|
||||
return new Intl.NumberFormat("zh-CN", {
|
||||
style: "currency",
|
||||
currency: "CNY",
|
||||
minimumFractionDigits: 2,
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
export default function VouchersPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const [voucher, setVoucher] = useState<Voucher | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [taskId, setTaskId] = useState(searchParams.get("task_id") || "");
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
|
||||
const [mappings, setMappings] = useState<AccountMapping[]>([]);
|
||||
const [mappingDialogOpen, setMappingDialogOpen] = useState(false);
|
||||
const [editingMapping, setEditingMapping] = useState<AccountMapping | null>(null);
|
||||
const [mappingForm, setMappingForm] = useState({
|
||||
standard_field: "",
|
||||
debit_account: "",
|
||||
debit_account_name: "",
|
||||
credit_account: "",
|
||||
credit_account_name: "",
|
||||
cost_center: "",
|
||||
});
|
||||
|
||||
const loadVoucher = useCallback(async () => {
|
||||
if (!taskId) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await api.get<Voucher>(`/api/vouchers/task/${taskId}`);
|
||||
setVoucher(res.data);
|
||||
} catch (error) {
|
||||
console.error("加载凭证失败:", error);
|
||||
setVoucher(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [taskId]);
|
||||
|
||||
const loadMappings = useCallback(async () => {
|
||||
try {
|
||||
const res = await api.get<AccountMapping[]>(`/api/vouchers/account-mappings/list`);
|
||||
setMappings(res.data || []);
|
||||
} catch (error) {
|
||||
console.error("加载科目映射失败:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadVoucher();
|
||||
loadMappings();
|
||||
}, [loadVoucher, loadMappings]);
|
||||
|
||||
async function handleGenerate() {
|
||||
if (!taskId) return;
|
||||
setGenerating(true);
|
||||
try {
|
||||
const res = await api.post<Voucher>(`/api/vouchers/generate`, {
|
||||
task_id: Number(taskId),
|
||||
});
|
||||
setVoucher(res.data);
|
||||
} catch (error) {
|
||||
console.error("生成凭证失败:", error);
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
if (!voucher) return;
|
||||
setConfirming(true);
|
||||
try {
|
||||
const userId = Number(localStorage.getItem("user_id") || "1");
|
||||
const res = await api.post<Voucher>(`/api/vouchers/${voucher.id}/confirm`, {
|
||||
user_id: userId,
|
||||
});
|
||||
setVoucher(res.data);
|
||||
setConfirmOpen(false);
|
||||
} catch (error) {
|
||||
console.error("确认凭证失败:", error);
|
||||
} finally {
|
||||
setConfirming(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleExport(format: string) {
|
||||
if (!taskId) return;
|
||||
const token = localStorage.getItem("auth_token");
|
||||
const companyId = localStorage.getItem("company_id");
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/vouchers/task/${taskId}/export?format=${format}`;
|
||||
fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-Company-ID": companyId || "",
|
||||
},
|
||||
})
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `voucher_${taskId}.${format === "excel" ? "xlsx" : "csv"}`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
});
|
||||
}
|
||||
|
||||
function openMappingDialog(mapping?: AccountMapping) {
|
||||
if (mapping) {
|
||||
setEditingMapping(mapping);
|
||||
setMappingForm({
|
||||
standard_field: mapping.standard_field,
|
||||
debit_account: mapping.debit_account,
|
||||
debit_account_name: mapping.debit_account_name,
|
||||
credit_account: mapping.credit_account,
|
||||
credit_account_name: mapping.credit_account_name,
|
||||
cost_center: mapping.cost_center || "",
|
||||
});
|
||||
} else {
|
||||
setEditingMapping(null);
|
||||
setMappingForm({
|
||||
standard_field: "",
|
||||
debit_account: "",
|
||||
debit_account_name: "",
|
||||
credit_account: "",
|
||||
credit_account_name: "",
|
||||
cost_center: "",
|
||||
});
|
||||
}
|
||||
setMappingDialogOpen(true);
|
||||
}
|
||||
|
||||
async function saveMapping() {
|
||||
try {
|
||||
if (editingMapping) {
|
||||
await api.put(`/api/vouchers/account-mappings/${editingMapping.id}`, mappingForm);
|
||||
} else {
|
||||
await api.post(`/api/vouchers/account-mappings`, mappingForm);
|
||||
}
|
||||
setMappingDialogOpen(false);
|
||||
loadMappings();
|
||||
} catch (error) {
|
||||
console.error("保存科目映射失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMapping(id: number) {
|
||||
try {
|
||||
await api.delete(`/api/vouchers/account-mappings/${id}`);
|
||||
loadMappings();
|
||||
} catch (error) {
|
||||
console.error("删除科目映射失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-full p-6 lg:p-8 max-w-7xl mx-auto space-y-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div>
|
||||
<h1 className="text-heading-1 text-foreground">凭证管理</h1>
|
||||
<p className="text-muted-foreground mt-1">生成、预览和导出会计凭证</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => openMappingDialog()} className="btn-press">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
科目映射
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
<div className="flex-1 min-w-[180px]">
|
||||
<label className="text-caption text-muted-foreground mb-1.5 block">任务ID</label>
|
||||
<Input
|
||||
placeholder="输入对账任务ID"
|
||||
value={taskId}
|
||||
onChange={(e) => setTaskId(e.target.value)}
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={loadVoucher} variant="outline" className="h-9 btn-press">
|
||||
查询凭证
|
||||
</Button>
|
||||
<Button onClick={handleGenerate} disabled={generating || !taskId} className="h-9 btn-press">
|
||||
{generating ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Receipt className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
生成凭证
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||
</div>
|
||||
) : !voucher ? (
|
||||
<Card>
|
||||
<CardContent className="p-8 text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-muted mx-auto mb-4 flex items-center justify-center">
|
||||
<Receipt className="w-6 h-6 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-body text-muted-foreground mb-2">
|
||||
{taskId ? "该任务暂无凭证,请点击「生成凭证」" : "请输入任务ID查询凭证"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<CardTitle className="text-lg font-medium">
|
||||
{voucher.voucher_number}
|
||||
</CardTitle>
|
||||
<Badge className={statusConfig[voucher.status]?.color || statusConfig.DRAFT.color}>
|
||||
{statusConfig[voucher.status]?.label || voucher.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => handleExport("csv")} className="btn-press">
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
金蝶CSV
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => handleExport("excel")} className="btn-press">
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
Excel
|
||||
</Button>
|
||||
{voucher.status === "DRAFT" && (
|
||||
<Button size="sm" onClick={() => setConfirmOpen(true)} className="btn-press">
|
||||
<CheckCircle className="w-4 h-4 mr-1" />
|
||||
确认凭证
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div>
|
||||
<p className="text-caption text-muted-foreground">凭证日期</p>
|
||||
<p className="font-medium">{voucher.voucher_date}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-caption text-muted-foreground">会计期间</p>
|
||||
<p className="font-medium">{voucher.period}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-caption text-muted-foreground">借方合计</p>
|
||||
<p className="font-mono font-semibold text-foreground">{formatCurrency(voucher.total_debit)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-caption text-muted-foreground">贷方合计</p>
|
||||
<p className="font-mono font-semibold text-foreground">{formatCurrency(voucher.total_credit)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<p className="text-caption text-muted-foreground">摘要</p>
|
||||
<p className="text-body">{voucher.summary}</p>
|
||||
</div>
|
||||
|
||||
{voucher.confirmed_at && (
|
||||
<div className="mb-4 p-3 rounded-lg bg-emerald-50 dark:bg-emerald-950/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-emerald-600" />
|
||||
<span className="text-sm text-emerald-700 dark:text-emerald-400">
|
||||
已于 {new Date(voucher.confirmed_at).toLocaleString("zh-CN")} 确认
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, delay: 0.1 }}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-medium">凭证分录</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead className="w-[60px]">序号</TableHead>
|
||||
<TableHead>科目代码</TableHead>
|
||||
<TableHead>科目名称</TableHead>
|
||||
<TableHead>摘要</TableHead>
|
||||
<TableHead className="text-right">借方金额</TableHead>
|
||||
<TableHead className="text-right">贷方金额</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{voucher.entries.map((entry, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell className="text-muted-foreground">{i + 1}</TableCell>
|
||||
<TableCell className="font-mono">{entry.account_code}</TableCell>
|
||||
<TableCell>{entry.account_name}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{entry.summary}</TableCell>
|
||||
<TableCell className="text-right font-mono">
|
||||
{entry.debit_amount > 0 ? formatCurrency(entry.debit_amount) : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono">
|
||||
{entry.credit_amount > 0 ? formatCurrency(entry.credit_amount) : "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow className="border-t-2 font-semibold">
|
||||
<TableCell colSpan={4} className="text-right">合计</TableCell>
|
||||
<TableCell className="text-right font-mono">{formatCurrency(voucher.total_debit)}</TableCell>
|
||||
<TableCell className="text-right font-mono">{formatCurrency(voucher.total_credit)}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{mappings.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, delay: 0.2 }}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg font-medium">科目映射配置</CardTitle>
|
||||
<Button variant="outline" size="sm" onClick={() => openMappingDialog()} className="btn-press">
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
新增映射
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead>标准字段</TableHead>
|
||||
<TableHead>借方科目</TableHead>
|
||||
<TableHead>贷方科目</TableHead>
|
||||
<TableHead>成本中心</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead className="w-[100px]">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{mappings.map((m) => (
|
||||
<TableRow key={m.id} className="group">
|
||||
<TableCell className="font-medium">{m.standard_field}</TableCell>
|
||||
<TableCell>
|
||||
<span className="font-mono text-sm">{m.debit_account}</span>
|
||||
<span className="text-muted-foreground ml-2">{m.debit_account_name}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="font-mono text-sm">{m.credit_account}</span>
|
||||
<span className="text-muted-foreground ml-2">{m.credit_account_name}</span>
|
||||
</TableCell>
|
||||
<TableCell>{m.cost_center || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={m.is_active ? "default" : "secondary"}>
|
||||
{m.is_active ? "启用" : "禁用"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={() => openMappingDialog(m)}>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={() => deleteMapping(m.id)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* 确认对话框 */}
|
||||
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>确认凭证</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-body text-muted-foreground">
|
||||
确认后凭证将不能修改。确认凭证后将可以导出金蝶格式文件。
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setConfirmOpen(false)}>取消</Button>
|
||||
<Button onClick={handleConfirm} disabled={confirming} className="btn-press">
|
||||
{confirming ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <CheckCircle className="w-4 h-4 mr-2" />}
|
||||
确认
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 科目映射对话框 */}
|
||||
<Dialog open={mappingDialogOpen} onOpenChange={setMappingDialogOpen}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingMapping ? "编辑科目映射" : "新增科目映射"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-caption text-muted-foreground mb-1.5 block">标准字段</label>
|
||||
<Input
|
||||
value={mappingForm.standard_field}
|
||||
onChange={(e) => setMappingForm({ ...mappingForm, standard_field: e.target.value })}
|
||||
placeholder="如:基本工资"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-caption text-muted-foreground mb-1.5 block">借方科目代码</label>
|
||||
<Input
|
||||
value={mappingForm.debit_account}
|
||||
onChange={(e) => setMappingForm({ ...mappingForm, debit_account: e.target.value })}
|
||||
placeholder="如:6601.01"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-caption text-muted-foreground mb-1.5 block">借方科目名称</label>
|
||||
<Input
|
||||
value={mappingForm.debit_account_name}
|
||||
onChange={(e) => setMappingForm({ ...mappingForm, debit_account_name: e.target.value })}
|
||||
placeholder="如:管理费用-工资"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-caption text-muted-foreground mb-1.5 block">贷方科目代码</label>
|
||||
<Input
|
||||
value={mappingForm.credit_account}
|
||||
onChange={(e) => setMappingForm({ ...mappingForm, credit_account: e.target.value })}
|
||||
placeholder="如:2211.01"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-caption text-muted-foreground mb-1.5 block">贷方科目名称</label>
|
||||
<Input
|
||||
value={mappingForm.credit_account_name}
|
||||
onChange={(e) => setMappingForm({ ...mappingForm, credit_account_name: e.target.value })}
|
||||
placeholder="如:应付职工薪酬-工资"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-caption text-muted-foreground mb-1.5 block">成本中心(可选)</label>
|
||||
<Input
|
||||
value={mappingForm.cost_center}
|
||||
onChange={(e) => setMappingForm({ ...mappingForm, cost_center: e.target.value })}
|
||||
placeholder="如:管理部"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setMappingDialogOpen(false)}>取消</Button>
|
||||
<Button onClick={saveMapping} className="btn-press">保存</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user