借款出借人改为从现有员工选择并复用资料(09-17-company-loan-principal-repayment)
- 新增员工来源预览、关联与显式刷新接口,受 loan.manage 与员工银行卡权限保护;服务端读取并加密保存,客户端不能另传姓名账号 - 同公司同员工唯一复用,请求键绑定来源;锁序出借人→员工,员工使用 FOR NO KEY UPDATE - 员工关联出借人禁止手工改账号;在途还款期间禁止刷新,刷新推进版本并使旧公司代表授权失效 - 电脑/手机共用对话框,写请求在卸载或身份切换时取消 - 无新增迁移;本地 126 组 HTTP、45 组浏览器、9 个变异通过,两名只读复核及 Claude 独立复核无阻断 Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -53,10 +53,21 @@ export function fetchLoanRepayments(page: number, signal?: AbortSignal) {
|
||||
return apiRequestData<{ items: LoanRepayment[]; total: number }>(`/api/loan-repayments?page=${page}&pageSize=30`, { signal });
|
||||
}
|
||||
export function fetchLoanIdentities(q: string, signal?: AbortSignal) {
|
||||
return apiRequestData<{ items: Array<{ id: string; name: string; employeeNo: string }> }>(`/api/loan-lenders/identity-options?q=${encodeURIComponent(q)}`, { signal });
|
||||
return apiRequestData<{ items: Array<{ id: string; name: string; employeeNo: string; legalEntityId: string | null }> }>(`/api/loan-lenders/identity-options?q=${encodeURIComponent(q)}`, { signal });
|
||||
}
|
||||
export function saveLoanAction<T>(path: string, body: unknown, method = "POST") {
|
||||
return apiRequestData<T>(`/api${path}`, { method, body: JSON.stringify(body) });
|
||||
export type LoanEmployeeSource = {
|
||||
employee: { id: string; name: string; employeeNo: string | null; legalEntityId: string | null; legalEntityName: string | null };
|
||||
legalEntityId: string; legalEntityName: string; sourceVersion: string;
|
||||
bankName: string; bankAccountMasked: string | null; problems: string[];
|
||||
linkState: "new" | "reusable" | "refresh_required" | "conflict";
|
||||
existingLender: LoanLender | null;
|
||||
};
|
||||
export function fetchLoanEmployeeSource(employeeId: string, legalEntityId: string, signal?: AbortSignal) {
|
||||
const query = new URLSearchParams({ legalEntityId });
|
||||
return apiRequestData<LoanEmployeeSource>(`/api/loan-lenders/employee-source/${encodeURIComponent(employeeId)}?${query}`, { signal });
|
||||
}
|
||||
export function saveLoanAction<T>(path: string, body: unknown, method = "POST", signal?: AbortSignal) {
|
||||
return apiRequestData<T>(`/api${path}`, { method, body: JSON.stringify(body), signal });
|
||||
}
|
||||
export function createLoanRepayment(body: { requestKey: string; lenderId: string; reason: string;
|
||||
allocations: Array<{ loanId: string; amount: string }> }) {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export const ERROR_REASON_MESSAGES: Record<string, string> = {
|
||||
"loan.employee_source_incomplete": "员工姓名或收款资料不完整、不可用或银行不受支持,请在员工资料中核对维护。",
|
||||
"loan.employee_source_changed": "员工资料已变化,请重新加载并核对后再保存。",
|
||||
"loan.employee_lender_conflict": "该员工在本公司存在重复、归档或已变化的出借人关联,请核对原记录,不能另建或自动合并。",
|
||||
"loan.employee_lender_refresh_required": "已有出借人资料与当前员工资料不同,请核对后使用“使用当前员工资料”更新。",
|
||||
"loan.employee_account_managed": "员工出借人的收款资料来自员工档案,请先维护员工资料,再使用“使用当前员工资料”更新。",
|
||||
"loan.related_authorization_required": "本次操作没有有效的关联方公司授权,请核对授权范围和当前付款或对账权限。",
|
||||
"loan.related_scope_invalid": "关联方授权、身份、借款范围或历史决定不一致,请刷新并核对;不能转换原审批模式。",
|
||||
"loan.related_actor_conflict": "关联方确认须由指定公司有权人完成,且不能兼任本批经办、上传、录入或提案人。",
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { useEffect, useRef, useState, type FormEvent } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAuth } from "../../auth/AuthContext";
|
||||
import { hasPermission } from "../../auth/permissions";
|
||||
import { fetchLegalEntities } from "../../api/masterData";
|
||||
import { fetchLoanEmployeeSource, fetchLoanIdentities, saveLoanAction, type LoanLender } from "../../api/companyLoans";
|
||||
import { resolveErrorMessage } from "../../lib/resolveErrorMessage";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../ui/dialog";
|
||||
|
||||
const selectClass = "min-h-11 min-w-0 w-full rounded-md border border-zinc-300 bg-white px-3 text-sm";
|
||||
const problemLabels: Record<string, string> = {
|
||||
name_invalid: "员工姓名未完整维护", bank_missing: "银行或开户行未完整维护", bank_unsupported: "当前银行不支持借款还本或已停用",
|
||||
account_missing: "员工收款账号未维护", account_invalid: "员工收款账号格式不符合要求", account_unavailable: "员工收款账号暂不可读取",
|
||||
};
|
||||
|
||||
export function LoanEmployeeLenderDialog({ lender, onClose, onExternal, onRefresh, onSaved }: {
|
||||
lender: LoanLender | null; onClose: () => void; onExternal: () => void;
|
||||
onRefresh: (lender: LoanLender) => void; onSaved: (lender: LoanLender) => void;
|
||||
}) {
|
||||
const { me, permissions } = useAuth();
|
||||
const cache = useQueryClient();
|
||||
const [search, setSearch] = useState("");
|
||||
const [employeeId, setEmployeeId] = useState(lender?.employeeId ?? "");
|
||||
const [companyId, setCompanyId] = useState(lender?.legalEntityId ?? "");
|
||||
const [companyChosen, setCompanyChosen] = useState(Boolean(lender));
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [reason, setReason] = useState("");
|
||||
const [requestKey] = useState(() => crypto.randomUUID());
|
||||
const alive = useRef(true);
|
||||
const pendingRequest = useRef<AbortController | null>(null);
|
||||
const actor = useRef(me?.id); actor.current = me?.id;
|
||||
useEffect(() => {
|
||||
alive.current = true;
|
||||
return () => { alive.current = false; pendingRequest.current?.abort(); };
|
||||
}, [me?.id]);
|
||||
const canReadBank = hasPermission(permissions, "employee.bank.read") || hasPermission(permissions, "employee.bank.manage");
|
||||
const canMaintain = hasPermission(permissions, "employee.bank.manage") && hasPermission(permissions, "system.users");
|
||||
const companies = useQuery({ queryKey: ["loan-companies", me?.id], queryFn: () => fetchLegalEntities({ enabled: true }), enabled: canReadBank && !lender });
|
||||
const people = useQuery({ queryKey: ["loan-identities", me?.id, search], queryFn: ({ signal }) => fetchLoanIdentities(search, signal), enabled: canReadBank && !lender });
|
||||
const source = useQuery({ queryKey: ["loan-employee-source", me?.id, employeeId, companyId],
|
||||
queryFn: ({ signal }) => fetchLoanEmployeeSource(employeeId, companyId, signal),
|
||||
enabled: canReadBank && Boolean(employeeId && companyId), refetchOnWindowFocus: false, refetchOnReconnect: false, staleTime: 0 });
|
||||
useEffect(() => { setChecked(false); }, [employeeId, companyId, source.dataUpdatedAt]);
|
||||
const data = canReadBank && source.isSuccess ? source.data : undefined;
|
||||
const existing = data?.existingLender;
|
||||
const blocked = !canReadBank || !data || source.isFetching || data.problems.length > 0 || data.linkState === "conflict" ||
|
||||
(lender ? existing?.id !== lender.id || existing.hasInFlightRepayment : data.linkState === "refresh_required");
|
||||
const mutation = useMutation({ mutationFn: async () => {
|
||||
if (blocked || !checked || !data) throw new Error("请先重新加载并核对员工资料。");
|
||||
const actorId = me?.id;
|
||||
const controller = new AbortController(); pendingRequest.current = controller;
|
||||
try {
|
||||
const result = lender && existing ? await saveLoanAction<LoanLender>(`/loan-lenders/${lender.id}/employee-refresh`, {
|
||||
expectedSourceVersion: data.sourceVersion, expectedIdentityVersion: existing.identityVersion,
|
||||
expectedAccountVersion: existing.accountVersion, detailsConfirmed: true, reason: reason.trim(),
|
||||
}, "POST", controller.signal) : (await saveLoanAction<{ lender: LoanLender; reused: boolean }>("/loan-lenders/from-employee", {
|
||||
requestKey, employeeId, legalEntityId: companyId, expectedSourceVersion: data.sourceVersion, detailsConfirmed: true,
|
||||
}, "POST", controller.signal)).lender;
|
||||
if (alive.current && actor.current === actorId) {
|
||||
await Promise.all(["loan-lenders", "company-loans", "loan-related-authorizations", "loan-employee-source"]
|
||||
.map((key) => cache.invalidateQueries({ queryKey: [key, actorId] })));
|
||||
if (alive.current && actor.current === actorId) onSaved(result);
|
||||
}
|
||||
} finally { if (pendingRequest.current === controller) pendingRequest.current = null; }
|
||||
} });
|
||||
const error = mutation.error ?? source.error ?? (!lender ? people.error ?? companies.error : null);
|
||||
function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); mutation.mutate(); }
|
||||
function chooseEmployee(id: string) {
|
||||
setEmployeeId(id); setChecked(false); mutation.reset();
|
||||
if (!companyChosen) setCompanyId(people.data?.items.find((person) => person.id === id)?.legalEntityId ?? "");
|
||||
}
|
||||
return <Dialog open onOpenChange={(open) => { if (!open && !mutation.isPending) onClose(); }}>
|
||||
<DialogContent className="max-h-[90dvh] overflow-y-auto"><DialogHeader>
|
||||
<DialogTitle>{lender ? "使用当前员工资料" : "从员工选择出借人"}</DialogTitle>
|
||||
<DialogDescription>{lender ? "更新出借人的姓名与收款资料后,原公司代表授权须重新登记确认。已经审批和付款的历史记录保留原资料。" : "选择已有员工,核对姓名和收款资料后关联。借款所属公司须单独确认。"}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{!canReadBank ? <p role="alert" className="text-sm">当前账号没有员工银行卡读取权限,请由具有该权限的财务办理。</p> : null}
|
||||
<form onSubmit={submit} className="grid gap-4">
|
||||
<fieldset disabled={mutation.isPending} className="grid min-w-0 gap-4">
|
||||
{!lender && canReadBank ? <>
|
||||
<label className="grid gap-1.5 text-sm">检索员工<Input value={search} onChange={(e) => { setSearch(e.target.value); chooseEmployee(""); }} placeholder="姓名或员工编号" maxLength={100} /></label>
|
||||
<label className="grid gap-1.5 text-sm">已有员工<select aria-label="已有员工" required className={selectClass} value={employeeId} onChange={(e) => chooseEmployee(e.target.value)} disabled={!people.isSuccess || people.isFetching}>
|
||||
<option value="">请选择已有员工</option>{people.data?.items.map((person) => <option key={person.id} value={person.id}>{person.name} · {person.employeeNo || "未编员工号"}</option>)}
|
||||
</select></label>
|
||||
{people.isSuccess && !people.data.items.length ? <p className="text-sm">未找到可选择的员工,请核对姓名或员工编号。</p> : null}
|
||||
<label className="grid gap-1.5 text-sm">借款所属公司<select aria-label="借款所属公司" required className={selectClass} value={companyId} disabled={!companies.isSuccess} onChange={(e) => { setCompanyId(e.target.value); setCompanyChosen(true); setChecked(false); mutation.reset(); }}>
|
||||
<option value="">请选择并核对借款所属公司</option>{companies.data?.items.filter((company) => company.currencyCode === "CNY").map((company) => <option key={company.id} value={company.id}>{company.name}</option>)}
|
||||
</select></label>
|
||||
<p className="text-sm text-zinc-600">公司默认值来自员工所属公司,请以实际借款关系核对。</p>
|
||||
</> : null}
|
||||
{source.isFetching ? <p role="status" className="text-sm">正在读取员工资料…</p> : null}
|
||||
{error ? <p role="alert" className="text-sm text-red-700">{resolveErrorMessage(error)}</p> : null}
|
||||
{data ? <>
|
||||
<dl className="grid gap-2 rounded border p-3 text-sm" aria-label="员工资料摘要">
|
||||
<div><dt className="text-zinc-600">姓名及账户名</dt><dd className="break-words">{data.employee.name || "待维护"}</dd></div>
|
||||
<div><dt className="text-zinc-600">员工所属公司</dt><dd className="break-words">{data.employee.legalEntityName ?? "未维护"}</dd></div>
|
||||
<div><dt className="text-zinc-600">借款所属公司</dt><dd className="break-words">{data.legalEntityName}</dd></div>
|
||||
<div><dt className="text-zinc-600">开户行</dt><dd className="break-words">{data.bankName || "待维护"}</dd></div>
|
||||
<div><dt className="text-zinc-600">收款账号</dt><dd className="break-all">{data.bankAccountMasked ?? "待维护或核对"}</dd></div>
|
||||
</dl>
|
||||
{data.problems.length ? <p role="alert" className="text-sm text-amber-800">{data.problems.map((problem) => problemLabels[problem] ?? "员工资料待核对").join(";")}。请在员工资料中维护后重新加载。</p> : null}
|
||||
{canMaintain ? <Link to={`/people/${encodeURIComponent(employeeId)}`} className="inline-flex min-h-11 items-center text-sm text-primary underline">维护员工资料</Link> : data.problems.length ? <p className="text-sm">请具有员工银行卡维护权限的人在员工档案中修正。</p> : null}
|
||||
{data.linkState === "conflict" ? <p role="alert" className="text-sm text-amber-800">本公司已有重复、归档或已变化的关联,请核对原出借人记录,不能自动合并。</p> : null}
|
||||
{!lender && data.linkState === "reusable" ? <p className="text-sm">该员工在本公司已有出借人登记,将使用已有记录。</p> : null}
|
||||
{existing && (lender || data.linkState === "refresh_required") ? <div className="grid gap-2 rounded border p-3 text-sm">
|
||||
<p className="break-words">已有出借人资料:{existing.name} · {existing.bankName} · {existing.bankAccountMasked ?? "账号暂不可用"}</p>
|
||||
{existing.hasInFlightRepayment ? <p role="alert">该出借人有还款正在办理,处理完成或安全取消后才能更新资料。</p> : null}
|
||||
{!lender ? <><p>已有资料与当前员工资料不同,请核对后显式更新。</p><Button type="button" variant="outline" disabled={existing.hasInFlightRepayment || Boolean(data.problems.length)} onClick={() => onRefresh(existing)}>使用当前员工资料</Button></> : null}
|
||||
</div> : null}
|
||||
<Button type="button" variant="outline" disabled={source.isFetching} onClick={() => { setChecked(false); mutation.reset(); void source.refetch(); }}>重新加载员工资料</Button>
|
||||
</> : null}
|
||||
{lender ? <label className="grid gap-1.5 text-sm">更新依据<Input required minLength={2} maxLength={500} value={reason} onChange={(e) => setReason(e.target.value)} /></label> : null}
|
||||
<label className="flex min-h-11 items-center gap-2 text-sm"><input type="checkbox" required disabled={blocked} checked={checked} onChange={(e) => setChecked(e.target.checked)} /><span>已核对借款所属公司、员工身份和收款资料</span></label>
|
||||
<Button type="submit" disabled={blocked || !checked || mutation.isPending}>{mutation.isPending ? "处理中…" : lender ? "确认更新出借人资料" : data?.linkState === "reusable" ? "使用已有出借人" : "确认关联员工"}</Button>
|
||||
{!lender ? <Button type="button" variant="outline" onClick={onExternal}>登记非员工出借人</Button> : null}
|
||||
</fieldset>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { fetchBankInstitutions } from "../api/paymentAccounts";
|
||||
import { createLoanRepayment, downloadLoanEvidence, fetchCompanyLoans, fetchLoanIdentities, fetchLoanLenders,
|
||||
fetchLoanRepayments, saveLoanAction, uploadLoanEvidence, type CompanyLoan, type LoanLender } from "../api/companyLoans";
|
||||
import { LoanRelatedAuthorizations } from "../components/common/LoanRelatedAuthorizations";
|
||||
import { LoanEmployeeLenderDialog } from "../components/common/LoanEmployeeLenderDialog";
|
||||
import { ListWorkspace, WorkspaceHeader, HeaderActionGroup } from "../components/patterns/ListWorkspace";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
@@ -20,7 +21,7 @@ import { downloadBlob } from "../lib/download";
|
||||
const evidenceLabels: Record<string, string> = { agreement: "协议或借款确认书", incoming: "原借入凭据",
|
||||
accounting_confirmation: "余额核对材料", adjustment: "系统外已还本金依据" };
|
||||
const stateLabels: Record<string, string> = { draft: "待付款复核", reviewed: "待付款", executed: "已付款", cancelled: "已取消", failed: "登记未完成" };
|
||||
type Mode = "lender" | "identity" | "account" | "loan" | "confirm" | "repay" | "download" | null;
|
||||
type Mode = "lender" | "employeeLender" | "employeeRefresh" | "identity" | "account" | "loan" | "confirm" | "repay" | "download" | null;
|
||||
const selectClass = "min-h-11 w-full rounded-md border border-zinc-300 bg-white px-3 text-sm";
|
||||
function Field({ label, children }: { label: string; children: ReactNode }) {
|
||||
const id = useId();
|
||||
@@ -103,7 +104,7 @@ export function CompanyLoansPage() {
|
||||
const setCurrentPage = tab === "loans" ? setPage : tab === "lenders" ? setLenderPage : setRepayPage;
|
||||
return <ListWorkspace>
|
||||
<WorkspaceHeader title="公司借款" subtitle="记录公司向个人借入及归还的人民币本金。余额以核对截止日及 OA 登记为依据,未核实不代表已结清。"
|
||||
actions={<HeaderActionGroup><Button variant="outline" onClick={() => open("lender")}>登记出借人</Button>
|
||||
actions={<HeaderActionGroup><Button variant="outline" onClick={() => open("employeeLender")}>登记出借人</Button>
|
||||
<Button onClick={() => open("loan")}>登记借入</Button><Button variant="outline" onClick={() => open("repay")}>申请还本</Button></HeaderActionGroup>} />
|
||||
<div className="flex flex-wrap gap-2" role="tablist" aria-label="借款台账视图">
|
||||
{([["loans", "借款台账"], ["lenders", "出借人"], ["repayments", "还款记录"], ["authorizations", "公司代表授权"]] as const).map(([key, label]) =>
|
||||
@@ -141,7 +142,7 @@ export function CompanyLoansPage() {
|
||||
<div className="flex flex-wrap gap-2"><Button variant="outline" disabled={lender.hasInFlightRepayment}
|
||||
aria-describedby={lender.hasInFlightRepayment ? `${lender.id}-in-flight` : undefined} onClick={() => open("identity", lender)}>核实身份关联</Button>
|
||||
<Button variant="outline" disabled={lender.hasInFlightRepayment}
|
||||
aria-describedby={lender.hasInFlightRepayment ? `${lender.id}-in-flight` : undefined} onClick={() => open("account", lender)}>更新收款资料</Button></div>
|
||||
aria-describedby={lender.hasInFlightRepayment ? `${lender.id}-in-flight` : undefined} onClick={() => open(lender.employeeId ? "employeeRefresh" : "account", lender)}>{lender.employeeId ? "使用当前员工资料" : "更新收款资料"}</Button></div>
|
||||
</section>) : null}
|
||||
{tab === "repayments" && repayments.isSuccess ? repayments.data.items.map((repayment) => <section key={repayment.id} className="card-std space-y-2 p-4">
|
||||
<h2 className="font-semibold break-all">{repayment.lenderName} · {repayment.repaymentNo}</h2>
|
||||
@@ -152,15 +153,19 @@ export function CompanyLoansPage() {
|
||||
</section>) : null}
|
||||
{tab !== "authorizations" ? <div className="flex items-center gap-3"><Button variant="outline" disabled={currentPage <= 1} onClick={() => setCurrentPage(currentPage - 1)}>上一页</Button>
|
||||
<span>第 {currentPage} 页</span><Button variant="outline" disabled={!current.data || currentPage * 30 >= current.data.total} onClick={() => setCurrentPage(currentPage + 1)}>下一页</Button></div> : null}
|
||||
<Dialog open={mode !== null} onOpenChange={(opened) => { if (!opened && !mutation.isPending) setMode(null); }}>
|
||||
{mode === "employeeLender" || mode === "employeeRefresh" ? <LoanEmployeeLenderDialog key={`${me?.id}-${requestKey}`}
|
||||
lender={mode === "employeeRefresh" ? selectedLender : null} onClose={() => setMode(null)} onExternal={() => open("lender")}
|
||||
onRefresh={(lender) => open("employeeRefresh", lender)} onSaved={() => { setMode(null); setTab("lenders"); setLenderPage(1); }} /> : null}
|
||||
<Dialog open={mode !== null && mode !== "employeeLender" && mode !== "employeeRefresh"} onOpenChange={(opened) => { if (!opened && !mutation.isPending) setMode(null); }}>
|
||||
<DialogContent className="max-h-[90dvh] overflow-y-auto"><DialogHeader><DialogTitle>
|
||||
{({ lender: "登记出借人", account: "更新收款资料", identity: "核实身份关联", loan: "登记借入", confirm: "确认期初余额", repay: "申请归还本金", download: "下载借款材料" } as Record<string, string>)[mode ?? ""]}
|
||||
{({ lender: "登记非员工出借人", account: "更新收款资料", identity: "核实身份关联", loan: "登记借入", confirm: "确认期初余额", repay: "申请归还本金", download: "下载借款材料" } as Record<string, string>)[mode ?? ""]}
|
||||
</DialogTitle><DialogDescription>{mode === "repay" ? "同一公司和出借人可合并多笔借款。系统按已确认的授权范围确定独立复核或公司代表关联方确认,提交后不能切换模式。" :
|
||||
mode === "confirm" ? "请根据凭据与代账核对余额。确认后不能覆盖修改,系统外已还本金须有依据。" :
|
||||
mode === "account" ? "更新后须重新核实身份关联。该出借人有还款正在办理时不能更新,须待处理完成或安全取消后再操作。" : "仅录入已核对的业务事实;敏感材料仅供有权人员查阅。"}</DialogDescription></DialogHeader>
|
||||
<form onSubmit={submit} className="grid gap-4">
|
||||
{error ? <p role="alert" className="text-sm text-red-700">{resolveErrorMessage(error)}</p> : null}
|
||||
{mode === "lender" ? <><Field label="公司主体"><select required name="company" className={selectClass}><option value="">请选择</option>{companies.data?.items.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}</select></Field>
|
||||
{mode === "lender" ? <><p className="text-sm text-zinc-600">仅用于非员工个人出借人;已有员工请从员工资料中选择。</p><Button type="button" variant="outline" onClick={() => open("employeeLender")}>从已有员工选择</Button>
|
||||
<Field label="公司主体"><select required name="company" className={selectClass}><option value="">请选择</option>{companies.data?.items.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}</select></Field>
|
||||
<Field label="出借人姓名"><Input name="name" required maxLength={120} /></Field></> : null}
|
||||
{mode === "lender" || mode === "account" ? <><Field label="收款银行"><select name="bank" required className={selectClass} defaultValue={selectedLender?.bankInstitutionId ?? ""}><option value="">请选择</option>{banks.data?.items.filter((b) => b.code === "CCB").map((b) => <option key={b.id} value={b.id}>{b.name}</option>)}</select></Field>
|
||||
<Field label="开户行名称"><Input name="bankName" required maxLength={200} defaultValue={selectedLender?.bankName} /></Field>
|
||||
|
||||
在新工单中引用
屏蔽一个用户