import { Search } from "@element-plus/icons-vue";
|
import dayjs from "dayjs";
|
import { userListNoPageByTenantId } from "@/api/system/user.js";
|
import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue";
|
import {
|
EXPENSE_SUBJECT_OPTIONS,
|
expenseSubjectLabel,
|
statusLabel,
|
statusTagType,
|
detectTravelTier,
|
getTravelStandardByTier,
|
computeTravelDays,
|
createEmptyExpenseDetail,
|
createEmptyForm,
|
initApprovalFlowNodes,
|
advanceApprovalFlow,
|
rejectApprovalFlow,
|
mockDeptBudget,
|
normalizeImportedRow,
|
} from "./travelReimburseUtils.js";
|
|
function unwrapArray(payload) {
|
if (Array.isArray(payload)) return payload;
|
if (payload?.data && Array.isArray(payload.data)) return payload.data;
|
if (payload?.rows && Array.isArray(payload.rows)) return payload.rows;
|
return [];
|
}
|
|
function isActiveUser(u) {
|
if (u.delFlag === "2" || u.delFlag === 2) return false;
|
if (u.status == null) return true;
|
return String(u.status) === "0";
|
}
|
|
function demoFlowNodes(names = ["部门主管", "财务审核"]) {
|
return names.map((name, i) => ({
|
approverId: `mock_${i + 1}`,
|
approverName: name,
|
sortOrder: i + 1,
|
nodeOrder: i + 1,
|
nodeStatus: i === 0 ? "process" : "wait",
|
approveOpinion: "",
|
approveTime: "",
|
}));
|
}
|
|
export function useTravelReimburse() {
|
const { proxy } = getCurrentInstance();
|
|
const allRows = ref([
|
{
|
id: "1",
|
reimburseNo: "TR202605090001",
|
applicantId: "mock_1",
|
employeeNo: "zhangsan",
|
employeeName: "张三",
|
applicantNo: "zhangsan",
|
applicantName: "张三",
|
reimburseReason: "赴上海参加行业展会及客户拜访。",
|
travelStartTime: "2026-05-10 08:00:00",
|
travelEndTime: "2026-05-13 18:00:00",
|
travelDays: 4,
|
departurePlace: "杭州",
|
destination: "上海",
|
hotelStandard: 600,
|
hotelDays: 3,
|
livingSubsidy: 400,
|
applyAmount: 4580,
|
payee: "张三",
|
expenseDetails: [
|
{ id: "d1", invoiceDate: "2026-05-10", expenseSubject: "transport", amount: 553, description: "高铁往返" },
|
{ id: "d2", invoiceDate: "2026-05-11", expenseSubject: "hotel", amount: 1680, description: "酒店住宿" },
|
],
|
attachmentList: [{ name: "高铁票.pdf", url: "/mock/invoice1.pdf" }],
|
invoiceAttachments: [{ name: "高铁票.pdf", url: "/mock/invoice1.pdf" }],
|
approvalFlowNodes: demoFlowNodes(),
|
currentNodeIndex: 0,
|
approvalResult: "pending",
|
rejectReason: "",
|
approvalRecords: [],
|
needSpecialApproval: false,
|
deptId: "101",
|
deptName: "销售部",
|
travelTier: "tier1",
|
createTime: "2026-05-09 10:20:00",
|
},
|
{
|
id: "2",
|
reimburseNo: "TR202605080002",
|
applicantId: "mock_2",
|
employeeNo: "lisi",
|
employeeName: "李四",
|
applicantNo: "lisi",
|
applicantName: "李四",
|
reimburseReason: "成都分公司技术支持。",
|
travelStartTime: "2026-05-05 09:00:00",
|
travelEndTime: "2026-05-07 17:00:00",
|
travelDays: 3,
|
departurePlace: "武汉",
|
destination: "成都",
|
hotelStandard: 450,
|
hotelDays: 2,
|
livingSubsidy: 240,
|
applyAmount: 2100,
|
payee: "李四",
|
expenseDetails: [{ id: "d3", invoiceDate: "2026-05-06", expenseSubject: "meal", amount: 180, description: "工作餐" }],
|
attachmentList: [],
|
invoiceAttachments: [],
|
approvalFlowNodes: demoFlowNodes().map((n, i) => ({ ...n, nodeStatus: "finish", approveOpinion: "同意", approveTime: "2026-05-08 11:00:00" })),
|
currentNodeIndex: 1,
|
approvalResult: "approved",
|
rejectReason: "",
|
approvalRecords: [{ operatorName: "部门主管", result: "approved", opinion: "同意", time: "2026-05-08 10:00:00" }],
|
needSpecialApproval: false,
|
deptId: "102",
|
deptName: "技术部",
|
travelTier: "tier2",
|
createTime: "2026-05-07 16:00:00",
|
},
|
]);
|
|
const searchForm = reactive({ applicantKeyword: "", travelStartFrom: "", travelEndTo: "" });
|
const tableLoading = ref(false);
|
const page = reactive({ current: 1, size: 10, total: 0 });
|
const importInputRef = ref(null);
|
const allUsersCache = ref([]);
|
const applicantFormSearchLoading = ref(false);
|
const applicantFormOptions = ref([]);
|
const formRef = ref();
|
const form = reactive(createEmptyForm());
|
const formDialog = reactive({ visible: false, title: "", mode: "add", readonly: false });
|
const detailDialog = reactive({ visible: false });
|
const detailRow = ref({});
|
const approveDialog = reactive({ visible: false, row: null });
|
const approveOpinion = ref("");
|
|
const filteredList = computed(() => {
|
let list = [...allRows.value];
|
const kw = (searchForm.applicantKeyword || "").trim().toLowerCase();
|
if (kw) {
|
list = list.filter((r) => {
|
const name = (r.applicantName || r.employeeName || "").toLowerCase();
|
const no = (r.applicantNo || r.employeeNo || "").toLowerCase();
|
return name.includes(kw) || no.includes(kw);
|
});
|
}
|
if (searchForm.travelStartFrom) {
|
list = list.filter((r) => !r.travelStartTime || r.travelStartTime.slice(0, 10) >= searchForm.travelStartFrom);
|
}
|
if (searchForm.travelEndTo) {
|
list = list.filter((r) => !r.travelEndTime || r.travelEndTime.slice(0, 10) <= searchForm.travelEndTo);
|
}
|
return list.sort((a, b) => (String(a.createTime) < String(b.createTime) ? 1 : -1));
|
});
|
|
watch(
|
filteredList,
|
(list) => {
|
page.total = list.length;
|
const maxPage = Math.max(1, Math.ceil(list.length / page.size) || 1);
|
if (page.current > maxPage) page.current = maxPage;
|
},
|
{ immediate: true }
|
);
|
|
const tableData = computed(() => {
|
const start = (page.current - 1) * page.size;
|
return filteredList.value.slice(start, start + page.size);
|
});
|
|
const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser));
|
|
const travelDaysDisplay = computed(() => {
|
const d = computeTravelDays(form.travelStartTime, form.travelEndTime);
|
return d == null ? "" : String(d);
|
});
|
|
const travelTierLabel = computed(() => {
|
const std = getTravelStandardByTier(form.travelTier || detectTravelTier(form.destination));
|
return `按${std.label}标准`;
|
});
|
|
const suggestedLivingSubsidy = computed(() => {
|
const days = computeTravelDays(form.travelStartTime, form.travelEndTime) || form.hotelDays || 0;
|
const std = getTravelStandardByTier(form.travelTier);
|
return Math.round(std.mealPerDay * days * 100) / 100;
|
});
|
|
const suggestedTransportSubsidy = computed(() => {
|
const days = computeTravelDays(form.travelStartTime, form.travelEndTime) || 0;
|
const std = getTravelStandardByTier(form.travelTier);
|
return Math.round(std.transportPerDay * days * 100) / 100;
|
});
|
|
const suggestedHotelLimit = computed(() => {
|
const nights = form.hotelDays || 0;
|
const perNight = form.hotelStandard ?? getTravelStandardByTier(form.travelTier).hotelPerNight;
|
return Math.round(perNight * nights * 100) / 100;
|
});
|
|
const detailTotalAmount = computed(() => {
|
const sum = (form.expenseDetails || []).reduce((s, d) => s + (Number(d.amount) || 0), 0);
|
return Math.round(sum * 100) / 100;
|
});
|
|
const overBudgetWarnings = computed(() => buildOverBudgetWarnings(form, detailTotalAmount.value, suggestedHotelLimit.value, suggestedTransportSubsidy.value, suggestedLivingSubsidy.value));
|
|
const budgetHint = computed(() => {
|
if (!form.deptId) return { visible: false };
|
const b = mockDeptBudget(form.deptId);
|
const apply = Number(form.applyAmount) || detailTotalAmount.value || 0;
|
const after = b.remainingAmount - apply;
|
return {
|
visible: true,
|
type: after < 0 ? "error" : "info",
|
title: `部门预算联动(${form.deptName || b.deptId})`,
|
description: `年度预算 ${b.totalBudget} 元,已用 ${b.usedAmount} 元,剩余 ${b.remainingAmount} 元;本单申请后预计剩余 ${Math.round(after * 100) / 100} 元。`,
|
};
|
});
|
|
const tableColumn = ref([
|
{ label: "报销单号", prop: "reimburseNo", width: 150 },
|
{ label: "申请人编号", prop: "applicantNo", width: 110 },
|
{ label: "申请人", prop: "applicantName", minWidth: 90 },
|
{ label: "出差开始", prop: "travelStartTime", width: 165 },
|
{ label: "出差结束", prop: "travelEndTime", width: 165 },
|
{ label: "创建时间", prop: "createTime", width: 165 },
|
{
|
label: "状态",
|
prop: "approvalResult",
|
width: 100,
|
dataType: "tag",
|
formatData: (v) => statusLabel(v),
|
formatType: (v) => statusTagType(v),
|
},
|
{
|
dataType: "action",
|
label: "操作",
|
align: "center",
|
fixed: "right",
|
width: 200,
|
operation: [
|
{ name: "编辑", type: "text", disabled: (row) => row.approvalResult === "pending" || row.approvalResult === "approved", clickFun: (row) => openFormDialog("edit", row) },
|
{ name: "详情", type: "text", clickFun: (row) => openDetail(row) },
|
{ name: "审批", type: "text", disabled: (row) => row.approvalResult !== "pending", clickFun: (row) => openApprove(row) },
|
],
|
},
|
]);
|
|
const formRules = {
|
applicantId: [{ required: true, message: "请选择员工", trigger: "change" }],
|
reimburseReason: [{ required: true, message: "请填写报销原因", trigger: "blur" }],
|
travelStartTime: [{ required: true, message: "请选择出差开始时间", trigger: "change" }],
|
travelEndTime: [
|
{ required: true, message: "请选择出差结束时间", trigger: "change" },
|
{
|
validator: (_r, val, cb) => {
|
if (!form.travelStartTime || !val) { cb(); return; }
|
if (computeTravelDays(form.travelStartTime, val) == null) cb(new Error("结束时间须晚于开始时间"));
|
else cb();
|
},
|
trigger: "change",
|
},
|
],
|
departurePlace: [{ required: true, message: "请填写出差地", trigger: "blur" }],
|
destination: [{ required: true, message: "请填写目的地", trigger: "blur" }],
|
applyAmount: [{ required: true, message: "请填写申请金额", trigger: "blur" }],
|
payee: [{ required: true, message: "请填写收款人", trigger: "blur" }],
|
approvalFlowNodes: [
|
{
|
validator: (_r, _v, cb) => {
|
const nodes = form.approvalFlowNodes || [];
|
if (!nodes.length) { cb(new Error("请至少配置一个审批节点")); return; }
|
if (nodes.some((n) => n.approverId == null || n.approverId === "")) { cb(new Error("每个节点须选择审批人")); return; }
|
cb();
|
},
|
trigger: "change",
|
},
|
],
|
};
|
|
function buildOverBudgetWarnings(f, detailTotal, hotelLimit, transportLimit, mealLimit) {
|
const warnings = [];
|
const bySubject = { transport: 0, hotel: 0, meal: 0, other: 0 };
|
(f.expenseDetails || []).forEach((d) => {
|
const key = d.expenseSubject || "other";
|
bySubject[key] = (bySubject[key] || 0) + (Number(d.amount) || 0);
|
});
|
if (bySubject.transport > transportLimit && transportLimit > 0) {
|
warnings.push(`交通费 ${bySubject.transport} 元超出标准 ${transportLimit} 元`);
|
}
|
if (bySubject.hotel > hotelLimit && hotelLimit > 0) {
|
warnings.push(`住宿费 ${bySubject.hotel} 元超出限额 ${hotelLimit} 元`);
|
}
|
if (bySubject.meal > mealLimit && mealLimit > 0) {
|
warnings.push(`餐饮费 ${bySubject.meal} 元超出生活补贴建议 ${mealLimit} 元`);
|
}
|
const std = getTravelStandardByTier(f.travelTier);
|
if (f.hotelStandard > std.hotelPerNight) {
|
warnings.push(`酒店标准 ${f.hotelStandard} 元/晚高于${std.label}标准 ${std.hotelPerNight} 元/晚`);
|
}
|
const apply = Number(f.applyAmount) || detailTotal;
|
const standardTotal = transportLimit + hotelLimit + mealLimit;
|
if (apply > standardTotal && standardTotal > 0) {
|
warnings.push(`申请总额 ${apply} 元高于差旅标准合计约 ${standardTotal} 元`);
|
}
|
return warnings;
|
}
|
|
async function loadUserPool() {
|
try {
|
allUsersCache.value = unwrapArray(await userListNoPageByTenantId());
|
} catch {
|
allUsersCache.value = [];
|
}
|
}
|
|
function userSelectLabel(u) {
|
const nick = u.nickName || "";
|
const name = u.userName || "";
|
if (nick && name && nick !== name) return `${nick}(${name})`;
|
return nick || name || `用户${u.userId ?? u.id ?? ""}`;
|
}
|
|
function userById(id) {
|
return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id));
|
}
|
|
function employeeNoFromUser(u) {
|
if (!u) return "";
|
return u.userName ?? u.userCode ?? u.jobNumber ?? u.workNo ?? (u.userId != null ? String(u.userId) : "");
|
}
|
|
function filterUsersByQuery(query) {
|
const list = allUsersCache.value.filter(isActiveUser);
|
const q = (query || "").trim().toLowerCase();
|
if (!q) return [...list];
|
return list.filter((u) => {
|
const nick = (u.nickName || "").toLowerCase();
|
const uname = (u.userName || "").toLowerCase();
|
return nick.includes(q) || uname.includes(q);
|
});
|
}
|
|
async function remoteSearchApplicantForm(query) {
|
applicantFormSearchLoading.value = true;
|
try {
|
if (!allUsersCache.value.length) await loadUserPool();
|
applicantFormOptions.value = filterUsersByQuery(query);
|
} finally {
|
applicantFormSearchLoading.value = false;
|
}
|
}
|
|
function onApplicantChange(uid) {
|
const u = userById(uid);
|
if (u) {
|
form.employeeName = u.nickName || u.userName || "";
|
form.employeeNo = employeeNoFromUser(u);
|
form.payee = form.payee || form.employeeName;
|
form.deptId = String(u.deptId ?? u.sysDeptId ?? "");
|
form.deptName = u.dept?.deptName ?? u.deptName ?? "";
|
} else {
|
form.employeeName = "";
|
form.employeeNo = "";
|
}
|
}
|
|
function recalcTravelStandards() {
|
form.travelTier = detectTravelTier(form.destination);
|
const std = getTravelStandardByTier(form.travelTier);
|
if (form.hotelStandard == null || form.hotelStandard === 0) form.hotelStandard = std.hotelPerNight;
|
const days = computeTravelDays(form.travelStartTime, form.travelEndTime);
|
if (days != null) {
|
form.travelDays = days;
|
if (form.hotelDays == null) form.hotelDays = Math.max(0, days - 1);
|
if (form.livingSubsidy == null || form.livingSubsidy === 0) form.livingSubsidy = suggestedLivingSubsidy.value;
|
}
|
form.needSpecialApproval = buildOverBudgetWarnings(form, detailTotalAmount.value, suggestedHotelLimit.value, suggestedTransportSubsidy.value, suggestedLivingSubsidy.value).length > 0;
|
}
|
|
function onTravelRangeChange() {
|
recalcTravelStandards();
|
nextTick(() => formRef.value?.validateField?.("travelEndTime"));
|
}
|
|
function onDetailAmountChange() {
|
recalcTravelStandards();
|
}
|
|
function onApprovalFlowChange() {
|
nextTick(() => formRef.value?.validateField?.("approvalFlowNodes"));
|
}
|
|
function addExpenseDetail() {
|
form.expenseDetails.push(createEmptyExpenseDetail());
|
}
|
|
function removeExpenseDetail(index) {
|
form.expenseDetails.splice(index, 1);
|
recalcTravelStandards();
|
}
|
|
function mapAttachmentList(list) {
|
return (list || []).map((f, i) => ({
|
id: f.id ?? f.uid ?? `inv_${Date.now()}_${i}`,
|
name: f.name || f.fileName || f.originalFilename || "未命名",
|
url: f.url || f.downloadURL || f.previewURL || f.previewUrl || "",
|
}));
|
}
|
|
function syncApplyAmountFromDetails() {
|
form.applyAmount = detailTotalAmount.value;
|
recalcTravelStandards();
|
}
|
|
function handleQuery() {
|
page.current = 1;
|
tableLoading.value = true;
|
setTimeout(() => { tableLoading.value = false; }, 150);
|
}
|
|
function resetSearch() {
|
searchForm.applicantKeyword = "";
|
searchForm.travelStartFrom = "";
|
searchForm.travelEndTo = "";
|
handleQuery();
|
}
|
|
function pagination(obj) {
|
page.current = obj.page;
|
page.size = obj.limit;
|
}
|
|
function openDetail(row) {
|
detailRow.value = { ...row };
|
detailDialog.visible = true;
|
}
|
|
function openApprove(row) {
|
approveDialog.row = { ...row };
|
approveDialog.visible = true;
|
}
|
|
function approvalActionLabel(v) {
|
if (v === "approved") return "通过";
|
if (v === "rejected") return "驳回";
|
return "提交";
|
}
|
|
async function openFormDialog(mode, row) {
|
formDialog.mode = mode;
|
formDialog.readonly = false;
|
formDialog.title = mode === "add" ? "新增差旅报销" : "编辑差旅报销";
|
if (!allUsersCache.value.length) await loadUserPool();
|
Object.assign(form, createEmptyForm());
|
if (mode === "edit" && row) {
|
Object.assign(form, {
|
...JSON.parse(JSON.stringify(row)),
|
attachmentList: JSON.parse(JSON.stringify(row.attachmentList || row.invoiceAttachments || [])),
|
approvalFlowNodes: JSON.parse(JSON.stringify(row.approvalFlowNodes || [])),
|
expenseDetails: JSON.parse(JSON.stringify(row.expenseDetails || [])),
|
});
|
const u = userById(row.applicantId);
|
applicantFormOptions.value = u ? [u] : [{ userId: row.applicantId, nickName: row.employeeName, userName: row.employeeNo }];
|
} else {
|
form.approvalFlowNodes = [{ approverId: null, approverName: "", sortOrder: 1, nodeOrder: 1 }];
|
remoteSearchApplicantForm("");
|
}
|
formDialog.visible = true;
|
nextTick(() => {
|
formRef.value?.clearValidate?.();
|
recalcTravelStandards();
|
});
|
}
|
|
function onFormClosed() {
|
formRef.value?.resetFields?.();
|
}
|
|
async function submitForm() {
|
try {
|
await formRef.value?.validate?.();
|
} catch {
|
return;
|
}
|
if (!(form.expenseDetails || []).length) {
|
proxy?.$modal?.msgWarning?.("请至少添加一条报销明细");
|
return;
|
}
|
recalcTravelStandards();
|
if (form.needSpecialApproval) {
|
try {
|
await proxy.$modal.confirm("存在超支项,提交后将标记为需特批,是否继续?");
|
} catch {
|
return;
|
}
|
}
|
const days = computeTravelDays(form.travelStartTime, form.travelEndTime);
|
const payload = {
|
reimburseNo: form.reimburseNo || `TR${dayjs().format("YYYYMMDDHHmmss")}`,
|
applicantId: form.applicantId,
|
employeeNo: form.employeeNo,
|
employeeName: form.employeeName,
|
applicantNo: form.employeeNo,
|
applicantName: form.employeeName,
|
reimburseReason: form.reimburseReason,
|
travelStartTime: form.travelStartTime,
|
travelEndTime: form.travelEndTime,
|
travelDays: days,
|
departurePlace: form.departurePlace,
|
destination: form.destination,
|
hotelStandard: form.hotelStandard,
|
hotelDays: form.hotelDays,
|
livingSubsidy: form.livingSubsidy,
|
applyAmount: form.applyAmount,
|
payee: form.payee,
|
expenseDetails: JSON.parse(JSON.stringify(form.expenseDetails)),
|
attachmentList: JSON.parse(JSON.stringify(form.attachmentList || [])),
|
invoiceAttachments: mapAttachmentList(form.attachmentList),
|
approvalFlowNodes: initApprovalFlowNodes(form.approvalFlowNodes),
|
currentNodeIndex: 0,
|
needSpecialApproval: form.needSpecialApproval,
|
deptId: form.deptId,
|
deptName: form.deptName,
|
travelTier: form.travelTier,
|
};
|
if (formDialog.mode === "add") {
|
allRows.value.unshift({
|
id: `local_${Date.now()}`,
|
...payload,
|
approvalResult: "pending",
|
rejectReason: "",
|
approvalRecords: [],
|
createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
|
});
|
proxy?.$modal?.msgSuccess?.("提交成功,已进入审批(本地模拟)");
|
} else {
|
const idx = allRows.value.findIndex((r) => r.id === form.id);
|
if (idx !== -1) {
|
const prev = allRows.value[idx];
|
allRows.value[idx] = {
|
...prev,
|
...payload,
|
id: form.id,
|
approvalResult: prev.approvalResult === "rejected" ? "pending" : prev.approvalResult,
|
approvalFlowNodes: initApprovalFlowNodes(form.approvalFlowNodes),
|
currentNodeIndex: 0,
|
createTime: prev.createTime,
|
};
|
}
|
proxy?.$modal?.msgSuccess?.("保存成功(本地模拟)");
|
}
|
formDialog.visible = false;
|
handleQuery();
|
}
|
|
async function submitApprove(result) {
|
const row = approveDialog.row;
|
if (!row) return;
|
if (result === "rejected" && !(approveOpinion.value || "").trim()) {
|
proxy?.$modal?.msgWarning?.("驳回须填写审批意见");
|
return;
|
}
|
const idx = allRows.value.findIndex((r) => r.id === row.id);
|
if (idx === -1) return;
|
const cur = allRows.value[idx];
|
const operatorName = "当前审批人";
|
const record = {
|
operatorName,
|
result,
|
opinion: approveOpinion.value || (result === "approved" ? "同意" : "驳回"),
|
time: dayjs().format("YYYY-MM-DD HH:mm:ss"),
|
};
|
const records = [...(cur.approvalRecords || []), record];
|
let flowUpdate;
|
if (result === "approved") {
|
flowUpdate = advanceApprovalFlow(cur, approveOpinion.value);
|
} else {
|
flowUpdate = rejectApprovalFlow(cur, approveOpinion.value);
|
}
|
allRows.value[idx] = {
|
...cur,
|
approvalFlowNodes: flowUpdate.nodes,
|
currentNodeIndex: flowUpdate.currentNodeIndex,
|
approvalResult: flowUpdate.approvalResult,
|
rejectReason: flowUpdate.rejectReason ?? cur.rejectReason,
|
approvalRecords: records,
|
};
|
proxy?.$modal?.msgSuccess?.(result === "approved" ? "已通过" : "已驳回");
|
approveDialog.visible = false;
|
handleQuery();
|
}
|
|
function handleExport() {
|
const data = filteredList.value;
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json;charset=utf-8" });
|
const url = URL.createObjectURL(blob);
|
const a = document.createElement("a");
|
a.href = url;
|
a.download = `差旅报销导出_${dayjs().format("YYYYMMDDHHmmss")}.json`;
|
a.click();
|
URL.revokeObjectURL(url);
|
proxy?.$modal?.msgSuccess?.(`已导出 ${data.length} 条`);
|
}
|
|
function handleImportClick() {
|
importInputRef.value?.click?.();
|
}
|
|
function onImportFile(e) {
|
const file = e.target.files?.[0];
|
e.target.value = "";
|
if (!file) return;
|
const reader = new FileReader();
|
reader.onload = () => {
|
try {
|
const parsed = JSON.parse(String(reader.result || ""));
|
const arr = Array.isArray(parsed) ? parsed : parsed?.rows || parsed?.data;
|
if (!Array.isArray(arr) || !arr.length) {
|
proxy?.$modal?.msgWarning?.("导入格式须为差旅报销 JSON 数组");
|
return;
|
}
|
arr.forEach((raw, i) => allRows.value.unshift(normalizeImportedRow(raw, i)));
|
proxy?.$modal?.msgSuccess?.(`成功导入 ${arr.length} 条`);
|
handleQuery();
|
} catch {
|
proxy?.$modal?.msgError?.("解析失败");
|
}
|
};
|
reader.readAsText(file, "utf-8");
|
}
|
|
onMounted(() => loadUserPool());
|
|
return {
|
Search,
|
EXPENSE_SUBJECT_OPTIONS,
|
expenseSubjectLabel,
|
searchForm,
|
tableLoading,
|
page,
|
tableData,
|
tableColumn,
|
importInputRef,
|
formRef,
|
form,
|
formDialog,
|
formRules,
|
detailDialog,
|
detailRow,
|
approveDialog,
|
approveOpinion,
|
applicantFormSearchLoading,
|
applicantFormOptions,
|
flowUserOptions,
|
travelDaysDisplay,
|
travelTierLabel,
|
suggestedLivingSubsidy,
|
suggestedTransportSubsidy,
|
suggestedHotelLimit,
|
detailTotalAmount,
|
overBudgetWarnings,
|
budgetHint,
|
handleQuery,
|
resetSearch,
|
pagination,
|
remoteSearchApplicantForm,
|
userSelectLabel,
|
onApplicantChange,
|
recalcTravelStandards,
|
onTravelRangeChange,
|
onDetailAmountChange,
|
onApprovalFlowChange,
|
addExpenseDetail,
|
removeExpenseDetail,
|
syncApplyAmountFromDetails,
|
openFormDialog,
|
onFormClosed,
|
submitForm,
|
openDetail,
|
openApprove,
|
approvalActionLabel,
|
submitApprove,
|
handleExport,
|
handleImportClick,
|
onImportFile,
|
};
|
}
|