fix: 修复和完善 Excel 功能。

This commit is contained in:
c
2026-03-19 16:58:10 +08:00
parent bf83b5e3b6
commit d88701f4ee
15 changed files with 546 additions and 220 deletions

View File

@@ -2,7 +2,7 @@
"name": "teek-design-vue3-template", "name": "teek-design-vue3-template",
"version": "2.0.0", "version": "2.0.0",
"private": true, "private": true,
"description": "Teek Design Vue3 后台管理系统", "description": "牛安后台管理系统",
"author": "Teeker <2456019588@qq.com>", "author": "Teeker <2456019588@qq.com>",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",

8
pnpm-lock.yaml generated
View File

@@ -47,9 +47,6 @@ importers:
qs: qs:
specifier: ^6.14.0 specifier: ^6.14.0
version: 6.14.0 version: 6.14.0
remixicon:
specifier: ^4.9.1
version: 4.9.1
sortablejs: sortablejs:
specifier: ^1.15.6 specifier: ^1.15.6
version: 1.15.6 version: 1.15.6
@@ -4260,9 +4257,6 @@ packages:
resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
remixicon@4.9.1:
resolution: {integrity: sha512-36gLSoujkabnCFZFDyP17VNh9piuBA/rsXUb4auSJWLGsHVXtmxLj/EM5FjaEAGnk8oIAj1Azob/DZ2N+90lAQ==}
repeat-element@1.1.4: repeat-element@1.1.4:
resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -9675,8 +9669,6 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
set-function-name: 2.0.2 set-function-name: 2.0.2
remixicon@4.9.1: {}
repeat-element@1.1.4: {} repeat-element@1.1.4: {}
repeat-string@1.6.1: {} repeat-string@1.6.1: {}

View File

@@ -80,6 +80,11 @@ export default {
yes: "Yes", yes: "Yes",
no: "No", no: "No",
}, },
_component: {
baseFormWithTable: {
uploadTip: "Only xlsx/xls/csv files are allowed, max 10MB",
},
},
_prop: { _prop: {
common: { common: {
tel: "Tel", tel: "Tel",
@@ -339,6 +344,8 @@ export default {
formStatus: "Audit Status", formStatus: "Audit Status",
customerName: "Customer Name", customerName: "Customer Name",
customerId: "Customer", customerId: "Customer",
storeNo: "Warehouse",
storeName: "Warehouse",
partNumber: "Part Number", partNumber: "Part Number",
productSpecs: "Product Specs", productSpecs: "Product Specs",
saleCount: "Sale Count", saleCount: "Sale Count",
@@ -754,6 +761,27 @@ export default {
}, },
sale: { sale: {
saleorder: { saleorder: {
select_customerId: "Please select customer",
select_storeNo: "Please select warehouse",
input_formName: "Please enter form name",
input_formMark: "Please enter form remark",
input_formCode: "Please enter form code",
input_partNumber: "Please enter part number",
input_saleCount: "Please enter sale count",
input_price: "Please enter price",
input_saleMark: "Please enter remark",
approve_confirm: "Confirm approval",
approve_success: "Approval success",
approve_fail: "Approval failed",
reject_confirm: "Confirm reject",
reject_success: "Reject success",
reject_fail: "Reject failed",
already_approved: "Sale order already approved",
not_approved: "Sale order not approved",
no_sale_order_items: "Sale order has no items",
cannot_unapprove_with_shipped: "Sale order already shipped, cannot unapprove",
import_success: "Import success",
import_fail: "Import failed",
delete_message: "Delete Order", delete_message: "Delete Order",
}, },
}, },
@@ -830,14 +858,17 @@ export default {
add: "Add Transfer", add: "Add Transfer",
edit: "Edit Transfer", edit: "Edit Transfer",
stock_check_failed: "Stock check failed", stock_check_failed: "Stock check failed",
templateFileName: "Transfer Order Template",
}, },
warehousereceipt: { warehousereceipt: {
add: "Add Receipt", add: "Add Receipt",
edit: "Edit Receipt", edit: "Edit Receipt",
templateFileName: "Warehouse Receipt Template",
}, },
inventorycount: { inventorycount: {
add: "Add Count", add: "Add Count",
edit: "Edit Count", edit: "Edit Count",
templateFileName: "Inventory Count Template",
}, },
}, },
production: { production: {
@@ -847,6 +878,7 @@ export default {
showItem: "BOM Items", showItem: "BOM Items",
baseTitle: "BOM Info", baseTitle: "BOM Info",
tableTitle: "BOM Details", tableTitle: "BOM Details",
templateFileName: "BOM Template",
}, },
production_plan: { production_plan: {
add: "Add Plan", add: "Add Plan",
@@ -868,10 +900,12 @@ export default {
baseTitle: "Receipt Basic Info", baseTitle: "Receipt Basic Info",
tableTitle: "Product Details", tableTitle: "Product Details",
outstockDialog: "Product Outstock", outstockDialog: "Product Outstock",
templateFileName: "Finished Product Receipt Template",
}, },
finishedproductshipment: { finishedproductshipment: {
add: "Add Shipment", add: "Add Shipment",
edit: "Edit Shipment", edit: "Edit Shipment",
templateFileName: "Finished Product Shipment Template",
}, },
}, },
purchase: { purchase: {
@@ -881,6 +915,7 @@ export default {
showItem: "Plan Items", showItem: "Plan Items",
baseTitle: "Purchase Plan Basic Info", baseTitle: "Purchase Plan Basic Info",
tableTitle: "Purchase Details", tableTitle: "Purchase Details",
templateFileName: "Purchase Plan Template",
}, },
purchaseorder: { purchaseorder: {
add: "Add Purchase Order", add: "Add Purchase Order",
@@ -888,6 +923,7 @@ export default {
showItem: "Order Items", showItem: "Order Items",
baseTitle: "Purchase Order Basic Info", baseTitle: "Purchase Order Basic Info",
tableTitle: "Order Details", tableTitle: "Order Details",
templateFileName: "Purchase Order Template",
}, },
}, },
sale: { sale: {
@@ -897,6 +933,7 @@ export default {
showItem: "Sale Details", showItem: "Sale Details",
baseTitle: "Sale Order Basic Info", baseTitle: "Sale Order Basic Info",
tableTitle: "Sale Details", tableTitle: "Sale Details",
templateFileName: "Sale Order Template",
}, },
repairrecord: { repairrecord: {
add: "Add Repair Record", add: "Add Repair Record",

View File

@@ -80,6 +80,11 @@ export default {
yes: "是", yes: "是",
no: "否", no: "否",
}, },
_component: {
baseFormWithTable: {
uploadTip: "只能上传 xlsx/xls/csv 文件,且不超过 10MB",
},
},
_prop: { _prop: {
common: { common: {
tel: "电话", tel: "电话",
@@ -339,6 +344,8 @@ export default {
formStatus: "审核状态", formStatus: "审核状态",
customerName: "客户名称", customerName: "客户名称",
customerId: "客户", customerId: "客户",
storeNo: "出货仓库",
storeName: "出货仓库",
partNumber: "物料编号", partNumber: "物料编号",
productSpecs: "物料型号", productSpecs: "物料型号",
saleCount: "销售数量", saleCount: "销售数量",
@@ -809,6 +816,7 @@ export default {
sale: { sale: {
saleorder: { saleorder: {
select_customerId: "请选择客户", select_customerId: "请选择客户",
select_storeNo: "请选择出货仓库",
input_formName: "请输入单据名称", input_formName: "请输入单据名称",
input_formMark: "请输入单据备注", input_formMark: "请输入单据备注",
input_formCode: "请输入单据编号", input_formCode: "请输入单据编号",
@@ -928,6 +936,7 @@ export default {
baseTitle: "调拨单基本信息", baseTitle: "调拨单基本信息",
tableTitle: "调拨明细", tableTitle: "调拨明细",
stock_check_failed: "库存检测失败", stock_check_failed: "库存检测失败",
templateFileName: "调拨单模板",
}, },
warehousereceipt: { warehousereceipt: {
add: "添加入库单", add: "添加入库单",
@@ -935,6 +944,7 @@ export default {
showItem: "入库明细", showItem: "入库明细",
baseTitle: "入库单基本信息", baseTitle: "入库单基本信息",
tableTitle: "入库明细", tableTitle: "入库明细",
templateFileName: "入库单模板",
}, },
inventorycount: { inventorycount: {
add: "添加盘点单", add: "添加盘点单",
@@ -942,6 +952,7 @@ export default {
showItem: "盘点明细", showItem: "盘点明细",
baseTitle: "盘点单基本信息", baseTitle: "盘点单基本信息",
tableTitle: "盘点明细", tableTitle: "盘点明细",
templateFileName: "盘点单模板",
}, },
}, },
production: { production: {
@@ -951,6 +962,7 @@ export default {
showItem: "BOM 明细", showItem: "BOM 明细",
baseTitle: "BOM 基本信息", baseTitle: "BOM 基本信息",
tableTitle: "BOM 明细", tableTitle: "BOM 明细",
templateFileName: "BOM模板",
}, },
production_plan: { production_plan: {
add: "新增生产计划", add: "新增生产计划",
@@ -972,6 +984,7 @@ export default {
baseTitle: "成品入库单基本信息", baseTitle: "成品入库单基本信息",
tableTitle: "成品明细", tableTitle: "成品明细",
outstockDialog: "成品出货", outstockDialog: "成品出货",
templateFileName: "成品入库单模板",
}, },
finishedproductshipment: { finishedproductshipment: {
add: "添加成品出货单", add: "添加成品出货单",
@@ -979,6 +992,7 @@ export default {
showItem: "成品明细", showItem: "成品明细",
baseTitle: "成品出货单基本信息", baseTitle: "成品出货单基本信息",
tableTitle: "成品明细", tableTitle: "成品明细",
templateFileName: "成品出货单模板",
}, },
saleorder: { saleorder: {
add: "添加销售订单", add: "添加销售订单",
@@ -986,6 +1000,7 @@ export default {
showItem: "销售明细", showItem: "销售明细",
baseTitle: "销售订单基本信息", baseTitle: "销售订单基本信息",
tableTitle: "销售明细", tableTitle: "销售明细",
templateFileName: "销售订单模板",
}, },
}, },
purchase: { purchase: {
@@ -1001,6 +1016,7 @@ export default {
unitPrice: "单价", unitPrice: "单价",
totalPrice: "总价", totalPrice: "总价",
purchaseStatus: "采购状态", purchaseStatus: "采购状态",
templateFileName: "采购计划模板",
}, },
purchase_order: { purchase_order: {
add: "新建采购订单", add: "新建采购订单",
@@ -1009,6 +1025,7 @@ export default {
qrcode: "二维码打印", qrcode: "二维码打印",
baseTitle: "采购订单基本信息", baseTitle: "采购订单基本信息",
tableTitle: "采购明细", tableTitle: "采购明细",
templateFileName: "采购订单模板",
}, },
}, },
sale: { sale: {
@@ -1018,6 +1035,7 @@ export default {
showItem: "销售明细", showItem: "销售明细",
baseTitle: "销售订单基本信息", baseTitle: "销售订单基本信息",
tableTitle: "销售明细", tableTitle: "销售明细",
templateFileName: "销售订单模板",
}, },
repairrecord: { repairrecord: {
add: "添加维修记录", add: "添加维修记录",

View File

@@ -3,56 +3,104 @@ import { Upload, Download } from "@icon-park/vue-next";
import BaseForm from "../base-form/BaseForm.vue"; import BaseForm from "../base-form/BaseForm.vue";
import { ElMessage, type UploadInstance, type UploadUserFile } from "element-plus"; import { ElMessage, type UploadInstance, type UploadUserFile } from "element-plus";
import * as XLSX from "xlsx"; import * as XLSX from "xlsx";
import { type ExcelDataMapping } from "./type"; import { type ExcelColumnConfig, type FieldMappingConfig, type DetailLoadConfig } from "./type";
import { Delete } from "@element-plus/icons-vue"; import { Delete } from "@element-plus/icons-vue";
import { get } from "@/common/http/request";
import { $t } from "@/common/languages";
interface Emits { interface Emits {
(e: "update:mappedData", data: any[]): void;
(e: "error", msg: string): void; (e: "error", msg: string): void;
} }
const props = defineProps({ const props = defineProps({
baseTitle: String, baseTitle: String,
tableTitle: String, tableTitle: String,
uploadDesc: String,
itemArrayName: { type: String, required: true }, itemArrayName: { type: String, required: true },
mappingConfig: { type: Object, required: true }, mappingConfig: { type: Object as () => FieldMappingConfig, required: true },
templateFileName: { type: String, default: "模板" }, templateFileName: { type: String, required: true },
/**
* 明细数据加载配置
* 传入此配置后,组件会在编辑模式下自动加载明细数据
*/
detailConfig: { type: Object as () => DetailLoadConfig | null, default: null },
/**
* 模板列配置
* 用于控制模板中显示哪些列,如果不传则使用 mappingConfig 中的所有列
* 可以设置 include 或 exclude 来控制列的显示
* include: 只包含指定的列
* exclude: 排除指定的列
*/
templateColumns: {
type: Object as () => {
include?: string[];
exclude?: string[];
} | null,
default: null,
},
}); });
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
const form = defineModel<any>("form"); const form = defineModel<any>("form");
const visible = defineModel<boolean>("visible");
const uploadRef = ref<UploadInstance>(); const uploadRef = ref<UploadInstance>();
const baseFormComponentRef = ref<InstanceType<typeof BaseForm> | null>(null); const baseFormRef = ref<InstanceType<typeof BaseForm> | null>(null);
const getValue = (row: any, config: ExcelDataMapping) => { // 加载状态
const { sourceKey, defaultValue, transform } = config; const loading = ref(false);
/**
* 获取 Excel 列配置
* 统一处理为数组形式
*/
const getSourceKeys = (config: ExcelColumnConfig): string[] => {
const { sourceKey } = config;
if (Array.isArray(sourceKey)) {
return sourceKey;
}
return [sourceKey];
};
/**
* 获取表头文本
* 优先使用 header如果没有则使用 sourceKey 的第一个值
*/
const getHeaderText = (config: ExcelColumnConfig): string => {
if (config.header) {
return config.header;
}
const sourceKeys = getSourceKeys(config);
return sourceKeys[0] || "";
};
/**
* 从行数据中获取值
*/
const getValue = (row: any, config: ExcelColumnConfig) => {
const sourceKeys = getSourceKeys(config);
const { defaultValue, transform } = config;
let rawValue: any; let rawValue: any;
// 1. 处理 sourceKey 是数组的情况 (多对一 / 降级策略) // 1. 处理 sourceKey 是数组的情况 (多对一 / 降级策略)
if (Array.isArray(sourceKey)) { for (const key of sourceKeys) {
for (const key of sourceKey) { const val = row[key];
const val = row[key]; // 只要找到非 undefined 且非 null 的值,就立即停止查找
// 只要找到非 undefined 且非 null 的值,就立即停止查找 if (val !== undefined && val !== null && val !== "") {
if (val !== undefined && val !== null) { rawValue = val;
rawValue = val; break;
break;
}
} }
} }
// 2. 处理 sourceKey 是字符串的情况 (一对一)
else if (typeof sourceKey === "string") {
rawValue = row[sourceKey];
}
// 3. 如果没找到值,使用默认值 // 2. 如果没找到值,使用默认值
if (rawValue === undefined || rawValue === null) { if (rawValue === undefined || rawValue === null) {
rawValue = defaultValue; rawValue = defaultValue;
} }
// 4. 执行转换函数 (如果有) // 3. 执行转换函数 (如果有)
if (transform && typeof transform === "function") { if (transform && typeof transform === "function") {
try { try {
return transform(rawValue); return transform(rawValue);
} catch (e) { } catch (e) {
console.warn(`Transform error for key ${Array.isArray(sourceKey) ? sourceKey.join("/") : sourceKey}:`, e); console.warn(`Transform error for key ${sourceKeys.join("/")}:`, e);
return defaultValue; return defaultValue;
} }
} }
@@ -60,6 +108,9 @@ const getValue = (row: any, config: ExcelDataMapping) => {
return rawValue; return rawValue;
}; };
/**
* 转换 Excel 数据
*/
const convertData = (jsonData: any) => { const convertData = (jsonData: any) => {
if (!Array.isArray(jsonData)) { if (!Array.isArray(jsonData)) {
emit("error", "jsonData 必须是数组"); emit("error", "jsonData 必须是数组");
@@ -69,20 +120,67 @@ const convertData = (jsonData: any) => {
const result = jsonData.map(row => { const result = jsonData.map(row => {
const newRow: any = {}; const newRow: any = {};
// Object.entries 返回的是 [string, any][],我们需要断言为正确的类型
// 或者直接在循环内部使用 config 变量
for (const [targetKey, config] of Object.entries(props.mappingConfig)) { for (const [targetKey, config] of Object.entries(props.mappingConfig)) {
// 显式断言 config 的类型,防止 entries 推断丢失泛型信息 const fieldConfig = config as ExcelColumnConfig;
const fieldConfig = config as ExcelDataMapping;
newRow[targetKey] = getValue(row, fieldConfig); newRow[targetKey] = getValue(row, fieldConfig);
} }
return newRow; return newRow;
}); });
emit("update:mappedData", result);
return result; return result;
}; };
/**
* 加载明细数据
*/
const loadDetailData = async () => {
if (!props.detailConfig || !form.value?.id) return;
const { url, paramName, dataPath = "data" } = props.detailConfig;
loading.value = true;
try {
const res = await get(url, { [paramName]: form.value.id });
// 根据 dataPath 获取数据
const keys = dataPath.split(".");
let items = res;
for (const key of keys) {
items = items?.[key];
if (items === undefined || items === null) break;
}
form.value[props.itemArrayName] = items || [];
} catch (error) {
console.error("加载明细数据失败:", error);
ElMessage.error("加载明细数据失败");
form.value[props.itemArrayName] = [];
} finally {
loading.value = false;
}
};
/**
* 监听 visible 变化
* 当弹窗打开且是编辑模式(有 id自动加载明细数据
*/
watch(
() => visible.value,
newVisible => {
if (newVisible && props.detailConfig && form.value?.id) {
// 编辑模式,自动加载明细
loadDetailData();
}
}
);
// 存储当前上传的文件信息,用于显示和移除
const currentFile = ref<UploadUserFile | null>(null);
/**
* 清空所有数据(文件 + 表格)
*/
const clearAllData = () => { const clearAllData = () => {
// 1. 清空文件引用 // 1. 清空文件引用
currentFile.value = null; currentFile.value = null;
@@ -96,10 +194,14 @@ const clearAllData = () => {
} }
// 4. 清空验证 // 4. 清空验证
if (baseFormComponentRef.value) { if (baseFormRef.value) {
baseFormComponentRef.value.clearValidate(); baseFormRef.value.clearValidate();
} }
}; };
/**
* 处理文件变化
*/
const handleFileChange = (file: any) => { const handleFileChange = (file: any) => {
// 文件大小校验 // 文件大小校验
const maxSize = 10 * 1024 * 1024; // 10MB const maxSize = 10 * 1024 * 1024; // 10MB
@@ -114,6 +216,10 @@ const handleFileChange = (file: any) => {
// 解析 Excel追加模式不清空原有数据 // 解析 Excel追加模式不清空原有数据
parseExcel(file.raw); parseExcel(file.raw);
}; };
/**
* 解析 Excel 文件
*/
const parseExcel = (file: any) => { const parseExcel = (file: any) => {
const reader = new FileReader(); const reader = new FileReader();
@@ -164,10 +270,9 @@ const parseExcel = (file: any) => {
reader.readAsArrayBuffer(file); reader.readAsArrayBuffer(file);
}; };
// 【新增】存储当前上传的文件信息,用于显示和移除 /**
const currentFile = ref<UploadUserFile | null>(null); * 只清理表格数据(保留基本表单数据)
*/
// 【新增】只清理表格数据(保留基本表单数据)
const clearTableData = () => { const clearTableData = () => {
// 1. 清空表格数据 // 1. 清空表格数据
if (form.value && Array.isArray(form.value[props.itemArrayName])) { if (form.value && Array.isArray(form.value[props.itemArrayName])) {
@@ -175,14 +280,16 @@ const clearTableData = () => {
} }
// 2. 清空表单验证(仅表格相关) // 2. 清空表单验证(仅表格相关)
if (baseFormComponentRef.value) { if (baseFormRef.value) {
baseFormComponentRef.value.clearValidate(); baseFormRef.value.clearValidate();
} }
ElMessage.success("表格数据已清空"); ElMessage.success("表格数据已清空");
}; };
// 【新增】深度清理函数(清理表格+文件,保留基本表单数据) /**
* 深度清理函数(清理表格+文件,保留基本表单数据)
*/
const resetFormData = () => { const resetFormData = () => {
// 1. 清空表格数据 // 1. 清空表格数据
if (form.value && Array.isArray(form.value[props.itemArrayName])) { if (form.value && Array.isArray(form.value[props.itemArrayName])) {
@@ -194,12 +301,14 @@ const resetFormData = () => {
uploadRef.value?.clearFiles(); uploadRef.value?.clearFiles();
// 3. 清空表单验证 // 3. 清空表单验证
if (baseFormComponentRef.value) { if (baseFormRef.value) {
baseFormComponentRef.value.clearValidate(); baseFormRef.value.clearValidate();
} }
}; };
// 【新增】增加一行 /**
* 增加一行
*/
const addRow = () => { const addRow = () => {
if (!form.value) form.value = {}; if (!form.value) form.value = {};
if (!Array.isArray(form.value[props.itemArrayName])) { if (!Array.isArray(form.value[props.itemArrayName])) {
@@ -208,39 +317,72 @@ const addRow = () => {
// 创建一个空行,根据 mappingConfig 生成默认结构 // 创建一个空行,根据 mappingConfig 生成默认结构
const newRow: any = {}; const newRow: any = {};
for (const [key, config] of Object.entries(props.mappingConfig)) { for (const [key, config] of Object.entries(props.mappingConfig)) {
const fieldConfig = config as ExcelDataMapping; const fieldConfig = config as ExcelColumnConfig;
newRow[key] = fieldConfig.defaultValue !== undefined ? fieldConfig.defaultValue : ""; newRow[key] = fieldConfig.defaultValue !== undefined ? fieldConfig.defaultValue : "";
} }
form.value[props.itemArrayName].push(newRow); form.value[props.itemArrayName].push(newRow);
}; };
// 【新增】删除指定行 /**
* 删除指定行
*/
const deleteRow = (index: number) => { const deleteRow = (index: number) => {
if (form.value && Array.isArray(form.value[props.itemArrayName])) { if (form.value && Array.isArray(form.value[props.itemArrayName])) {
form.value[props.itemArrayName].splice(index, 1); form.value[props.itemArrayName].splice(index, 1);
} }
}; };
// 【新增】移除文件 /**
* 移除文件
*/
const removeFile = () => { const removeFile = () => {
clearAllData(); clearAllData();
ElMessage.success("文件已移除,表格数据已清空"); ElMessage.success("文件已移除,表格数据已清空");
}; };
// 【新增】下载模板文件 /**
* 获取模板列配置
* 根据 templateColumns 配置过滤 mappingConfig
*/
const getTemplateColumns = (): [string, ExcelColumnConfig][] => {
const allEntries = Object.entries(props.mappingConfig) as [string, ExcelColumnConfig][];
if (!props.templateColumns) {
return allEntries;
}
const { include, exclude } = props.templateColumns;
if (include && include.length > 0) {
// 只包含指定的列
return allEntries.filter(([key]) => include.includes(key));
}
if (exclude && exclude.length > 0) {
// 排除指定的列
return allEntries.filter(([key]) => !exclude.includes(key));
}
return allEntries;
};
/**
* 下载模板文件
*/
const downloadTemplate = () => { const downloadTemplate = () => {
// 1. 根据 mappingConfig 生成表头 // 1. 根据模板列配置生成表头
const headers: string[] = []; const headers: string[] = [];
for (const [targetKey, config] of Object.entries(props.mappingConfig)) { const colWidths: { wch: number }[] = [];
const fieldConfig = config as ExcelDataMapping;
// 使用 sourceKey 作为表头,如果是数组则使用第一个 const templateColumns = getTemplateColumns();
let header: string;
if (Array.isArray(fieldConfig.sourceKey)) { for (const [targetKey, config] of templateColumns) {
header = fieldConfig.sourceKey[0]; // 使用 header 或 sourceKey 的第一个值作为表头
} else { const header = getHeaderText(config);
header = fieldConfig.sourceKey;
}
headers.push(header); headers.push(header);
// 使用配置的列宽,默认 20
colWidths.push({ wch: config.width || 20 });
} }
// 2. 创建工作簿 // 2. 创建工作簿
@@ -250,7 +392,6 @@ const downloadTemplate = () => {
const ws = XLSX.utils.aoa_to_sheet([headers]); const ws = XLSX.utils.aoa_to_sheet([headers]);
// 4. 设置列宽 // 4. 设置列宽
const colWidths = headers.map(() => ({ wch: 20 }));
ws["!cols"] = colWidths; ws["!cols"] = colWidths;
// 5. 将工作表添加到工作簿 // 5. 将工作表添加到工作簿
@@ -263,21 +404,55 @@ const downloadTemplate = () => {
ElMessage.success("模板文件下载成功"); ElMessage.success("模板文件下载成功");
}; };
// 监听 BaseForm 发出的 reset 事件 /**
* 计算属性:确保表格数据始终是一个数组
*/
const tableData = computed(() => {
if (!form.value) return [];
const data = form.value[props.itemArrayName];
return Array.isArray(data) ? data : [];
});
/**
* 监听 form 变化,确保 itemArrayName 对应的属性是数组
*/
watch(
() => form.value,
newVal => {
if (newVal && !Array.isArray(newVal[props.itemArrayName])) {
newVal[props.itemArrayName] = [];
}
},
{ immediate: true }
);
/**
* 监听 BaseForm 发出的 reset 事件
*/
const handleBaseFormReset = () => { const handleBaseFormReset = () => {
resetFormData(); resetFormData();
}; };
defineExpose({ defineExpose({
innerFormRef: computed(() => baseFormComponentRef.value?.formRef), innerFormRef: computed(() => baseFormRef.value?.formRef),
baseFormRef: computed(() => baseFormRef.value?.formRef),
resetFormData, resetFormData,
addRow, addRow,
deleteRow, deleteRow,
clearTableData, clearTableData,
downloadTemplate, downloadTemplate,
loadDetailData,
}); });
</script> </script>
<template> <template>
<BaseForm ref="baseFormRef" v-bind="$attrs" class="form-dialog" v-model:form="form" @reset="handleBaseFormReset"> <BaseForm
ref="baseFormRef"
v-bind="$attrs"
class="form-dialog"
v-model:form="form"
v-model:visible="visible"
@reset="handleBaseFormReset"
>
<template #form-items> <template #form-items>
<el-divider content-position="left" v-if="props.baseTitle"> <el-divider content-position="left" v-if="props.baseTitle">
<el-icon><Document /></el-icon> <el-icon><Document /></el-icon>
@@ -301,15 +476,11 @@ defineExpose({
:on-change="handleFileChange" :on-change="handleFileChange"
:show-file-list="false" :show-file-list="false"
> >
<el-button type="primary"> <el-button type="primary" :loading="loading">
<el-icon><Upload /></el-icon> <el-icon><Upload /></el-icon>
选择 Excel 文件 选择 Excel 文件
</el-button> </el-button>
<span class="upload-tip-side"> <span class="upload-tip-side">{{ $t("_component.baseFormWithTable.uploadTip") }}</span>
只能上传 xlsx/xls/csv 文件且不超过 10MB
<br v-if="props.uploadDesc" />
<span class="tip-desc" v-if="props.uploadDesc">{{ uploadDesc }}</span>
</span>
</el-upload> </el-upload>
<el-button type="success" @click="downloadTemplate"> <el-button type="success" @click="downloadTemplate">
<el-icon><Download /></el-icon> <el-icon><Download /></el-icon>
@@ -318,10 +489,17 @@ defineExpose({
</div> </div>
</el-form-item> </el-form-item>
<el-table :data="form[itemArrayName]" border style="width: 100%" max-height="400" class="form-item-table"> <el-table
:data="tableData"
border
style="width: 100%"
max-height="400"
class="form-item-table"
v-loading="loading"
>
<slot name="form-table-columns"></slot> <slot name="form-table-columns"></slot>
<!-- 新增固定操作列删除行 --> <!-- 固定操作列删除行 -->
<el-table-column label="操作" width="80" align="center"> <el-table-column label="操作" width="80" align="center">
<template #default="{ $index }"> <template #default="{ $index }">
<el-button type="danger" link size="small" @click="deleteRow($index)"> <el-button type="danger" link size="small" @click="deleteRow($index)">
@@ -331,7 +509,7 @@ defineExpose({
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 新增表格下方操作栏 --> <!-- 表格下方操作栏 -->
<div class="table-actions"> <div class="table-actions">
<el-button type="primary" link @click="addRow"> <el-button type="primary" link @click="addRow">
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>

View File

@@ -1,12 +1,26 @@
/** /**
* 用于映射 Excel 中的数据 * Excel 列配置
* 用于映射 Excel 中的数据以及生成模板文件
*/ */
export interface ExcelDataMapping { export interface ExcelColumnConfig {
/** /**
* Excel 中的列名 * Excel 中的列名(支持多个,按顺序匹配)
* 例如:["物料编号", "partNumber", "商品编号"]
*/ */
sourceKey: string | string[]; sourceKey: string | string[];
/**
* 模板表头显示文本
* 如果不设置,默认使用 sourceKey 的第一个值
*/
header?: string;
/**
* 模板列宽(字符数)
* 默认 20
*/
width?: number;
/** /**
* 默认值 * 默认值
*/ */
@@ -23,4 +37,25 @@ export interface ExcelDataMapping {
/** /**
* 映射配置表 目标 key -> 规则 * 映射配置表 目标 key -> 规则
*/ */
export type FieldMappingConfig = Record<string, ExcelDataMapping>; export type FieldMappingConfig = Record<string, ExcelColumnConfig>;
/**
* 明细数据加载配置
*/
export interface DetailLoadConfig {
/**
* 加载明细的接口地址
*/
url: string;
/**
* 参数名,如 'orderId'、'id'
*/
paramName: string;
/**
* 响应数据路径,如 'data'、'data.items'
* 默认 'data'
*/
dataPath?: string;
}

View File

@@ -7,7 +7,7 @@ import { ElMessage, type FormInstance, type FormItemRule, type FormRules } from
import BaseItemDialog from "@/components/base/base-item-dialog/BaseItemDialog.vue"; import BaseItemDialog from "@/components/base/base-item-dialog/BaseItemDialog.vue";
import BaseFormWithTable from "@/components/base/base-form-with-table/BaseFormWithTable.vue"; import BaseFormWithTable from "@/components/base/base-form-with-table/BaseFormWithTable.vue";
import { get } from "@/common/http/request"; import { get } from "@/common/http/request";
import { type FieldMappingConfig } from "@/components/base/base-form-with-table/type"; import { type FieldMappingConfig, type DetailLoadConfig } from "@/components/base/base-form-with-table/type";
import ExpandablePageableTable from "@/components/base/expandable-pageable-table/ExpandablePageableTable.vue"; import ExpandablePageableTable from "@/components/base/expandable-pageable-table/ExpandablePageableTable.vue";
/** /**
@@ -129,55 +129,56 @@ const operateButtonClick = (eventName: string, row: any) => {
}; };
const mappingConfig: FieldMappingConfig = { const mappingConfig: FieldMappingConfig = {
// partNumber <- "商品编号" (默认空字符串)
partNumber: { partNumber: {
sourceKey: "商品编号", sourceKey: ["商品编号", "partNumber"],
header: $t("_prop.production.bom_item.partNumber"),
width: 20,
defaultValue: "", defaultValue: "",
}, },
// itemPosition <- "位号" (默认空字符串)
itemPosition: { itemPosition: {
sourceKey: "位号", sourceKey: ["位号", "itemPosition"],
header: $t("_prop.production.bom_item.itemPosition"),
width: 20,
defaultValue: "", defaultValue: "",
}, },
// manufactureCount <- "用量" (默认 0且需要转为数字)
manufactureCount: { manufactureCount: {
sourceKey: "用量", sourceKey: ["用量", "manufactureCount"],
header: $t("_prop.production.bom_item.manufactureCount"),
width: 15,
defaultValue: 0, defaultValue: 0,
transform: (val: any) => { transform: (val: any) => {
const num = Number(val); const num = Number(val);
return isNaN(num) ? 0 : num; return isNaN(num) ? 0 : num;
}, },
}, },
// productMark <- "备注" (默认空字符串)
productMark: { productMark: {
sourceKey: "备注", sourceKey: ["备注", "productMark"],
header: $t("_prop.production.bom_item.productMark"),
width: 20,
defaultValue: "", defaultValue: "",
}, },
// sameUseNum1 <- "替换料1" (默认空字符串)
sameUseNum1: { sameUseNum1: {
sourceKey: "替换料1", sourceKey: ["替换料1", "sameUseNum1"],
header: $t("_prop.production.bom_item.sameUseNum") + "1",
width: 20,
defaultValue: "", defaultValue: "",
}, },
// sameUseNum2 <- "替换料2" (默认空字符串)
sameUseNum2: { sameUseNum2: {
sourceKey: "替换料2", sourceKey: ["替换料2", "sameUseNum2"],
header: $t("_prop.production.bom_item.sameUseNum") + "2",
width: 20,
defaultValue: "", defaultValue: "",
}, },
// sameUseNum3 <- "替换料3" (默认空字符串)
sameUseNum3: { sameUseNum3: {
sourceKey: "替换料3", sourceKey: ["替换料3", "sameUseNum3"],
header: $t("_prop.production.bom_item.sameUseNum") + "3",
width: 20,
defaultValue: "", defaultValue: "",
}, },
// sameUseCount <- "替换料数量" (默认 0且需要转为数字)
sameUseCount: { sameUseCount: {
sourceKey: "替换料数量", sourceKey: ["替换料数量", "sameUseCount"],
header: $t("_prop.production.bom_item.sameUseCount"),
width: 15,
defaultValue: 0, defaultValue: 0,
transform: (val: any) => { transform: (val: any) => {
const num = Number(val); const num = Number(val);
@@ -185,6 +186,12 @@ const mappingConfig: FieldMappingConfig = {
}, },
}, },
}; };
const detailConfig: DetailLoadConfig = {
url: itemUrl,
paramName: "bomId",
dataPath: "data",
};
const validateManufactureCount = (index: number) => { const validateManufactureCount = (index: number) => {
return [ return [
{ {
@@ -404,8 +411,9 @@ const handleItemPositionBlur = (index: number) => {
:base-title="$t('_title.production.bom.baseTitle')" :base-title="$t('_title.production.bom.baseTitle')"
:table-title="$t('_title.production.bom.tableTitle')" :table-title="$t('_title.production.bom.tableTitle')"
item-array-name="bomItems" item-array-name="bomItems"
upload-desc="物料信息"
:mapping-config="mappingConfig" :mapping-config="mappingConfig"
:template-file-name="$t('_title.production.bom.templateFileName')"
:detail-config="detailConfig"
> >
<template #form-items> <template #form-items>
<el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item> <el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item>

View File

@@ -14,7 +14,7 @@ import BaseTableForm from "@/components/base/base-table-form/BaseTableForm.vue";
import { generateDucumentNo } from "@/common/utils/document-no-generator/document-no-generator"; import { generateDucumentNo } from "@/common/utils/document-no-generator/document-no-generator";
import { get, post } from "@/common/http/request"; import { get, post } from "@/common/http/request";
import { useStatus } from "@/common/languages/mapping/base-info-mapping"; import { useStatus } from "@/common/languages/mapping/base-info-mapping";
import { type FieldMappingConfig } from "@/components/base/base-form-with-table/type"; import { type FieldMappingConfig, type DetailLoadConfig } from "@/components/base/base-form-with-table/type";
/** /**
* 必须要的变量 * 必须要的变量
@@ -137,51 +137,52 @@ const handleOutstockSelectionChange = (selection: any[]) => {
// Excel导入映射配置 // Excel导入映射配置
const mappingConfig: FieldMappingConfig = { const mappingConfig: FieldMappingConfig = {
// productType <- "型号" (默认空字符串)
productType: { productType: {
sourceKey: "型号", sourceKey: ["型号", "productType"],
header: $t("_prop.production.finishedproductreceipt.productType"),
width: 15,
defaultValue: "", defaultValue: "",
}, },
// productSn <- "SN号" (默认空字符串)
productSn: { productSn: {
sourceKey: "SN号", sourceKey: ["SN号", "productSn"],
header: $t("_prop.production.finishedproductreceipt.productSn"),
width: 20,
defaultValue: "", defaultValue: "",
}, },
// mac <- "MAC地址" (默认空字符串)
mac: { mac: {
sourceKey: "MAC地址", sourceKey: ["MAC地址", "mac"],
header: $t("_prop.production.finishedproductreceipt.mac"),
width: 18,
defaultValue: "", defaultValue: "",
}, },
// serialNum <- "序列号" (默认空字符串)
serialNum: { serialNum: {
sourceKey: "序列号", sourceKey: ["序列号", "serialNum"],
header: $t("_prop.production.finishedproductreceipt.serialNum"),
width: 20,
defaultValue: "", defaultValue: "",
}, },
// softVersion <- "软件版本" (默认空字符串)
softVersion: { softVersion: {
sourceKey: "软件版本", sourceKey: ["软件版本", "softVersion"],
header: $t("_prop.production.finishedproductreceipt.softVersion"),
width: 15,
defaultValue: "", defaultValue: "",
}, },
// alVersion <- "算法版本" (默认空字符串)
alVersion: { alVersion: {
sourceKey: "算法版本", sourceKey: ["算法版本", "alVersion"],
header: $t("_prop.production.finishedproductreceipt.alVersion"),
width: 15,
defaultValue: "", defaultValue: "",
}, },
// alNum <- "算法标志" (默认空字符串)
alNum: { alNum: {
sourceKey: "算法标志", sourceKey: ["算法标志", "alNum"],
header: $t("_prop.production.finishedproductreceipt.alNum"),
width: 15,
defaultValue: "", defaultValue: "",
}, },
// alTxt <- "激活状态" (默认"未激活")
alTxt: { alTxt: {
sourceKey: "激活状态", sourceKey: ["激活状态", "alTxt"],
header: $t("_prop.production.finishedproductreceipt.alTxt"),
width: 12,
defaultValue: "未激活", defaultValue: "未激活",
transform: (val: any) => { transform: (val: any) => {
if (val === "已激活" || val === "Activated" || val === true || val === "1") return "已激活"; if (val === "已激活" || val === "Activated" || val === true || val === "1") return "已激活";
@@ -189,19 +190,25 @@ const mappingConfig: FieldMappingConfig = {
return "未激活"; return "未激活";
}, },
}, },
// createDate <- "生产日期" (默认空字符串)
createDate: { createDate: {
sourceKey: "生产日期", sourceKey: ["生产日期", "createDate"],
header: $t("_prop.production.finishedproductreceipt.createDate"),
width: 15,
defaultValue: "", defaultValue: "",
}, },
// mark <- "备注" (默认空字符串)
mark: { mark: {
sourceKey: "备注", sourceKey: ["备注", "mark"],
header: $t("_prop.production.finishedproductreceipt.mark"),
width: 20,
defaultValue: "", defaultValue: "",
}, },
}; };
const detailConfig: DetailLoadConfig = {
url: getDetailUrl,
paramName: "id",
dataPath: "data",
};
/** /**
* 可以自定义的变量 * 可以自定义的变量
*/ */
@@ -425,8 +432,9 @@ const handleReset = () => {
:base-title="$t('_title.production.finishedproductreceipt.baseTitle')" :base-title="$t('_title.production.finishedproductreceipt.baseTitle')"
:table-title="$t('_title.production.finishedproductreceipt.tableTitle')" :table-title="$t('_title.production.finishedproductreceipt.tableTitle')"
:item-array-name="itemArrayName" :item-array-name="itemArrayName"
upload-desc="设备信息"
:mapping-config="mappingConfig" :mapping-config="mappingConfig"
:template-file-name="$t('_title.production.finishedproductreceipt.templateFileName')"
:detail-config="detailConfig"
@reset="handleReset" @reset="handleReset"
> >
<template #form-items> <template #form-items>

View File

@@ -12,7 +12,7 @@ import BaseSelect from "@/components/base/base-select/BaseSelect.vue";
import { generateDucumentNo } from "@/common/utils/document-no-generator/document-no-generator"; import { generateDucumentNo } from "@/common/utils/document-no-generator/document-no-generator";
import { useStatus } from "@/common/languages/mapping/base-info-mapping"; import { useStatus } from "@/common/languages/mapping/base-info-mapping";
import ExpandablePageableTable from "@/components/base/expandable-pageable-table/ExpandablePageableTable.vue"; import ExpandablePageableTable from "@/components/base/expandable-pageable-table/ExpandablePageableTable.vue";
import { type FieldMappingConfig } from "@/components/base/base-form-with-table/type"; import { type FieldMappingConfig, type DetailLoadConfig } from "@/components/base/base-form-with-table/type";
import { watch } from "vue"; import { watch } from "vue";
/** /**
@@ -65,11 +65,21 @@ const outStockTypeOptions = [
const mappingConfig: FieldMappingConfig = { const mappingConfig: FieldMappingConfig = {
partNumber: { partNumber: {
sourceKey: "物料编号", sourceKey: ["物料编号", "partNumber"],
header: $t("_prop.production.finishedproductshipment.partNumber"),
width: 20,
defaultValue: "",
},
productSpecs: {
sourceKey: ["型号", "物料型号", "productSpecs", "productSpec"],
header: $t("_prop.production.finishedproductshipment.productSpecs"),
width: 20,
defaultValue: "", defaultValue: "",
}, },
productCount: { productCount: {
sourceKey: "数量", sourceKey: ["数量", "productCount"],
header: $t("_prop.production.finishedproductshipment.productCount"),
width: 15,
defaultValue: 0, defaultValue: 0,
transform: (val: any) => { transform: (val: any) => {
const num = Number(val); const num = Number(val);
@@ -77,11 +87,19 @@ const mappingConfig: FieldMappingConfig = {
}, },
}, },
productMark: { productMark: {
sourceKey: "备注", sourceKey: ["备注", "productMark"],
header: $t("_prop.production.finishedproductshipment.productMark"),
width: 20,
defaultValue: "", defaultValue: "",
}, },
}; };
const detailConfig: DetailLoadConfig = {
url: getDetailUrl,
paramName: "id",
dataPath: "data",
};
/** /**
* 基本不变通用变量 * 基本不变通用变量
*/ */
@@ -288,8 +306,9 @@ const handleReset = () => {
:base-title="$t('_title.production.finishedproductshipment.baseTitle')" :base-title="$t('_title.production.finishedproductshipment.baseTitle')"
:table-title="$t('_title.production.finishedproductshipment.tableTitle')" :table-title="$t('_title.production.finishedproductshipment.tableTitle')"
:item-array-name="itemArrayName" :item-array-name="itemArrayName"
upload-desc="物料信息"
:mapping-config="mappingConfig" :mapping-config="mappingConfig"
:template-file-name="$t('_title.production.finishedproductshipment.templateFileName')"
:detail-config="detailConfig"
@reset="handleReset" @reset="handleReset"
> >
<template #form-items> <template #form-items>

View File

@@ -97,7 +97,7 @@ const inboundForm = reactive({
orderCode: "", orderCode: "",
vendorName: "", vendorName: "",
formCode: "", formCode: "",
storeNo: null as number | null, storeNo: null as number,
storeName: "", storeName: "",
formMark: "", formMark: "",
items: [] as any[], items: [] as any[],
@@ -426,8 +426,9 @@ const getFormStatusTagType = (code: number | null): string => {
:base-title="$t('_title.purchase.purchase_order.baseTitle')" :base-title="$t('_title.purchase.purchase_order.baseTitle')"
:table-title="$t('_title.purchase.purchase_order.tableTitle')" :table-title="$t('_title.purchase.purchase_order.tableTitle')"
item-array-name="items" item-array-name="items"
upload-desc="采购明细"
:mapping-config="mappingConfig" :mapping-config="mappingConfig"
:template-file-name="$t('_title.purchase.purchase_order.templateFileName')"
:template-columns="{ exclude: ['purchaseMark'] }"
> >
<template #form-items> <template #form-items>
<el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item> <el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item>

View File

@@ -429,8 +429,8 @@ const mappingConfig: FieldMappingConfig = {
:base-title="$t('_title.purchase.purchase_plan.baseTitle')" :base-title="$t('_title.purchase.purchase_plan.baseTitle')"
:table-title="$t('_title.purchase.purchase_plan.tableTitle')" :table-title="$t('_title.purchase.purchase_plan.tableTitle')"
item-array-name="planItems" item-array-name="planItems"
upload-desc="采购明细"
:mapping-config="mappingConfig" :mapping-config="mappingConfig"
:template-file-name="$t('_title.purchase.purchase_plan.templateFileName')"
> >
<template #form-items> <template #form-items>
<el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item> <el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item>

View File

@@ -8,9 +8,10 @@ import { formatDate } from "@/common/utils/format-utils";
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus"; import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
import BaseFormWithTable from "@/components/base/base-form-with-table/BaseFormWithTable.vue"; import BaseFormWithTable from "@/components/base/base-form-with-table/BaseFormWithTable.vue";
import BaseItemDialog from "@/components/base/base-item-dialog/BaseItemDialog.vue"; import BaseItemDialog from "@/components/base/base-item-dialog/BaseItemDialog.vue";
import BaseSelect from "@/components/base/base-select/BaseSelect.vue";
import ExpandablePageableTable from "@/components/base/expandable-pageable-table/ExpandablePageableTable.vue"; import ExpandablePageableTable from "@/components/base/expandable-pageable-table/ExpandablePageableTable.vue";
import { get, post } from "@/common/http/request"; import { get, post } from "@/common/http/request";
import { type FieldMappingConfig } from "@/components/base/base-form-with-table/type"; import { type FieldMappingConfig, type DetailLoadConfig } from "@/components/base/base-form-with-table/type";
import { useStatus } from "@/common/languages/mapping/base-info-mapping"; import { useStatus } from "@/common/languages/mapping/base-info-mapping";
/** /**
@@ -26,6 +27,7 @@ const rejectUrl = "/sale/saleorder/unapproveSaleOrder";
const itemUrl = "/sale/saleorder/getSaleOrderItemList"; const itemUrl = "/sale/saleorder/getSaleOrderItemList";
const keyAccountSelectUrl = "/sys/keyaccount/getKeyAccountSelectList"; const keyAccountSelectUrl = "/sys/keyaccount/getKeyAccountSelectList";
const partNumberExistsUrl = "/warehouse/warehouseitem/existsWarehouseItem"; const partNumberExistsUrl = "/warehouse/warehouseitem/existsWarehouseItem";
const warehouseSelectUrl = "/warehouse/warehouse/getWarehouseSelectList";
const searchers = [{ name: "searchCode", type: "text" as const, placeholder: $t("_prop.sale.saleorder.searchCode") }]; const searchers = [{ name: "searchCode", type: "text" as const, placeholder: $t("_prop.sale.saleorder.searchCode") }];
@@ -34,6 +36,7 @@ const rules = reactive<FormRules>({
formName: [{ required: true, message: $t("_message.sale.saleorder.input_formName"), trigger: "blur" }], formName: [{ required: true, message: $t("_message.sale.saleorder.input_formName"), trigger: "blur" }],
customerId: [{ required: true, message: $t("_message.sale.saleorder.select_customerId"), trigger: "change" }], customerId: [{ required: true, message: $t("_message.sale.saleorder.select_customerId"), trigger: "change" }],
customerName: [{ required: true, message: $t("_message.sale.saleorder.select_customerId"), trigger: "change" }], customerName: [{ required: true, message: $t("_message.sale.saleorder.select_customerId"), trigger: "change" }],
storeNo: [{ required: true, message: $t("_message.sale.saleorder.select_storeNo"), trigger: "change" }],
partNumber: [ partNumber: [
{ {
required: true, required: true,
@@ -77,10 +80,11 @@ const getDetailUrl = "/sale/saleorder/getSaleOrderItemList";
* 基本不变通用变量 * 基本不变通用变量
*/ */
const tableRef = ref<InstanceType<typeof ExpandablePageableTable> | null>(null); const tableRef = ref<InstanceType<typeof ExpandablePageableTable> | null>(null);
const { useAdd, useEdit, useRemove, useGeneralPageRef } = usePage(tableRef); const { useAdd, useEdit, useRemove, useApprove, useReject, useGeneralPageRef } = usePage(tableRef);
const { title, visible, formType, form, itemVisible, itemParentId } = useGeneralPageRef(); const { title, visible, formType, form, itemVisible, itemParentId } = useGeneralPageRef();
const { getFormStatusLabel } = useStatus(); const { getFormStatusLabel } = useStatus();
const baseFormWithTableRef = ref<InstanceType<typeof BaseFormWithTable>>(); const baseFormWithTableRef = ref<InstanceType<typeof BaseFormWithTable>>();
const warehouseSelectRef = ref<InstanceType<typeof BaseSelect>>();
/** /**
* 可以自定义的变量 * 可以自定义的变量
@@ -123,6 +127,14 @@ const handleCustomerChange = (value: number) => {
} }
}; };
// 监听仓库选择变化,同步更新仓库名称
const handleWarehouseChange = () => {
const label = warehouseSelectRef.value?.getLabel();
if (label) {
form.value.storeName = label;
}
};
const add = () => { const add = () => {
form.value = { form.value = {
formCode: generateFormCode(), formCode: generateFormCode(),
@@ -134,21 +146,19 @@ const add = () => {
const edit = (row: any) => { const edit = (row: any) => {
title.value = "_title.sale.saleorder.edit"; title.value = "_title.sale.saleorder.edit";
form.value = { form.value = { ...row };
...row,
saleOrderItems: [],
};
visible.value = true; visible.value = true;
formType.value = true; formType.value = true;
// 加载明细数据 // 延迟设置仓库名称,等待 BaseSelect 加载完成
get(getDetailUrl, { saleOrderId: row.id }) if (row.storeNo) {
.then(res => { setTimeout(() => {
form.value.saleOrderItems = res.data || []; const label = warehouseSelectRef.value?.getLabel();
}) if (label && !form.value.storeName) {
.catch(err => { form.value.storeName = label;
console.error("Failed to load sale order items:", err); }
}); }, 300);
}
}; };
const remove = (row: any) => { const remove = (row: any) => {
@@ -156,35 +166,11 @@ const remove = (row: any) => {
}; };
const approve = (row: any) => { const approve = (row: any) => {
ElMessageBox.confirm($t("_message.sale.saleorder.approve_confirm"), $t("_level.warning"), { useApprove(approveUrl, row.id, "_message.sale.saleorder.approve_confirm");
confirmButtonText: $t("_button.confirm"),
cancelButtonText: $t("_button.cancel"),
type: "warning",
}).then(async () => {
try {
await post(approveUrl, { id: row.id });
ElMessage.success($t("_message.sale.saleorder.approve_success"));
tableRef.value?.reload();
} catch (err: any) {
ElMessage.error($t("_message.sale.saleorder.approve_fail") + err);
}
});
}; };
const reject = (row: any) => { const reject = (row: any) => {
ElMessageBox.confirm($t("_message.sale.saleorder.reject_confirm"), $t("_level.warning"), { useReject(rejectUrl, row.id, "_message.sale.saleorder.reject_confirm");
confirmButtonText: $t("_button.confirm"),
cancelButtonText: $t("_button.cancel"),
type: "warning",
}).then(async () => {
try {
await post(rejectUrl, { id: row.id });
ElMessage.success($t("_message.sale.saleorder.reject_success"));
tableRef.value?.reload();
} catch (err: any) {
ElMessage.error($t("_message.sale.saleorder.reject_fail") + err);
}
});
}; };
const showItem = (row: any) => { const showItem = (row: any) => {
@@ -236,26 +222,32 @@ const operateButtonClick = (eventName: string, row: any) => {
}; };
const authShowFunc = (row: any, button: globalThis.ButtonProp) => { const authShowFunc = (row: any, button: globalThis.ButtonProp) => {
// 已审核(status=1)时不显示审核按钮,显示反审按钮 // 已审核(formStatus=1)时不显示审核按钮,显示反审按钮
// 未审核(status=0)时显示审核按钮,不显示反审按钮 // 未审核(formStatus=0)时显示审核按钮,不显示反审按钮
if (row.status === 0 && button.eventName === "reject") return false; if (row.formStatus === 0 && button.eventName === "reject") return false;
if (row.status === 1 && button.eventName === "approve") return false; if (row.formStatus === 1 && button.eventName === "approve") return false;
// 已审核后不能编辑和删除 // 已审核后不能编辑和删除
if (row.status === 1 && (button.eventName === "edit" || button.eventName === "remove")) return false; if (row.formStatus === 1 && (button.eventName === "edit" || button.eventName === "remove")) return false;
return true; return true;
}; };
const mappingConfig: FieldMappingConfig = { const mappingConfig: FieldMappingConfig = {
partNumber: { partNumber: {
sourceKey: "物料编号", sourceKey: ["物料编号", "partNumber"],
header: $t("_prop.sale.saleorder.partNumber"),
width: 20,
defaultValue: "", defaultValue: "",
}, },
productSpecs: { productSpecs: {
sourceKey: "物料型号", sourceKey: ["物料型号", "productSpecs"],
header: $t("_prop.sale.saleorder.productSpecs"),
width: 20,
defaultValue: "", defaultValue: "",
}, },
saleCount: { saleCount: {
sourceKey: "数量", sourceKey: ["数量", "saleCount"],
header: $t("_prop.sale.saleorder.saleCount"),
width: 15,
defaultValue: 0, defaultValue: 0,
transform: (val: any) => { transform: (val: any) => {
const num = Number(val); const num = Number(val);
@@ -263,7 +255,9 @@ const mappingConfig: FieldMappingConfig = {
}, },
}, },
price: { price: {
sourceKey: "单价", sourceKey: ["单价", "price"],
header: $t("_prop.sale.saleorder.price"),
width: 15,
defaultValue: 0, defaultValue: 0,
transform: (val: any) => { transform: (val: any) => {
const num = Number(val); const num = Number(val);
@@ -271,11 +265,19 @@ const mappingConfig: FieldMappingConfig = {
}, },
}, },
saleMark: { saleMark: {
sourceKey: "备注", sourceKey: ["备注", "saleMark"],
header: $t("_prop.sale.saleorder.saleMark"),
width: 20,
defaultValue: "", defaultValue: "",
}, },
}; };
const detailConfig: DetailLoadConfig = {
url: getDetailUrl,
paramName: "saleOrderId",
dataPath: "data",
};
// 计算总价 // 计算总价
const calculateTotalPrice = (row: any) => { const calculateTotalPrice = (row: any) => {
if (row.saleCount && row.price) { if (row.saleCount && row.price) {
@@ -315,6 +317,7 @@ const getFormStatusTagType = (code: number | null): string => {
<template #columns> <template #columns>
<el-table-column :label="$t('_prop.sale.saleorder.formCode')" prop="formCode" /> <el-table-column :label="$t('_prop.sale.saleorder.formCode')" prop="formCode" />
<el-table-column :label="$t('_prop.sale.saleorder.customerName')" prop="customerName" /> <el-table-column :label="$t('_prop.sale.saleorder.customerName')" prop="customerName" />
<el-table-column :label="$t('_prop.sale.saleorder.storeName')" prop="storeName" />
<el-table-column :label="$t('_prop.sale.saleorder.totalValue')" prop="totalValue" /> <el-table-column :label="$t('_prop.sale.saleorder.totalValue')" prop="totalValue" />
<el-table-column :label="$t('_prop.sale.saleorder.formName')" prop="formName" /> <el-table-column :label="$t('_prop.sale.saleorder.formName')" prop="formName" />
<el-table-column :label="$t('_prop.sale.saleorder.formMark')" prop="formMark" show-overflow-tooltip /> <el-table-column :label="$t('_prop.sale.saleorder.formMark')" prop="formMark" show-overflow-tooltip />
@@ -346,8 +349,9 @@ const getFormStatusTagType = (code: number | null): string => {
:base-title="$t('_title.sale.saleorder.baseTitle')" :base-title="$t('_title.sale.saleorder.baseTitle')"
:table-title="$t('_title.sale.saleorder.tableTitle')" :table-title="$t('_title.sale.saleorder.tableTitle')"
item-array-name="saleOrderItems" item-array-name="saleOrderItems"
:upload-desc="$t('_title.sale.saleorder.tableTitle')"
:mapping-config="mappingConfig" :mapping-config="mappingConfig"
:template-file-name="$t('_title.sale.saleorder.templateFileName')"
:detail-config="detailConfig"
> >
<template #form-items> <template #form-items>
<el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item> <el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item>
@@ -375,14 +379,29 @@ const getFormStatusTagType = (code: number | null): string => {
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-form-item :label="$t('_prop.sale.saleorder.formMark')" prop="formMark"> <el-row :gutter="20">
<el-input <el-col :span="8">
v-model="form.formMark" <el-form-item :label="$t('_prop.sale.saleorder.storeNo')" prop="storeNo">
:placeholder="$t('_message.sale.saleorder.input_formMark')" <BaseSelect
type="textarea" ref="warehouseSelectRef"
:rows="2" v-model="form.storeNo"
/> :url="warehouseSelectUrl"
</el-form-item> style="width: 100%"
@change="handleWarehouseChange"
/>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item :label="$t('_prop.sale.saleorder.formMark')" prop="formMark">
<el-input
v-model="form.formMark"
:placeholder="$t('_message.sale.saleorder.input_formMark')"
type="textarea"
:rows="1"
/>
</el-form-item>
</el-col>
</el-row>
</template> </template>
<template #form-table-columns> <template #form-table-columns>
<el-table-column :label="$t('_prop.sale.saleorder.partNumber')" width="150"> <el-table-column :label="$t('_prop.sale.saleorder.partNumber')" width="150">
@@ -393,6 +412,14 @@ const getFormStatusTagType = (code: number | null): string => {
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('_prop.sale.saleorder.productSpecs')" width="200">
<template #default="{ row, $index }">
<el-form-item :prop="`${itemArrayName}.${$index}.productSpecs`">
<el-input v-model="row.productSpecs" size="small" />
</el-form-item>
</template>
</el-table-column>
<el-table-column :label="$t('_prop.sale.saleorder.saleCount')" width="120"> <el-table-column :label="$t('_prop.sale.saleorder.saleCount')" width="120">
<template #default="{ row, $index }"> <template #default="{ row, $index }">
<el-form-item :prop="`${itemArrayName}.${$index}.saleCount`"> <el-form-item :prop="`${itemArrayName}.${$index}.saleCount`">

View File

@@ -240,8 +240,8 @@ const getFormTypeText = (reserve1: number) => {
:base-title="$t('_title.warehouse.inventorycount.baseTitle')" :base-title="$t('_title.warehouse.inventorycount.baseTitle')"
:table-title="$t('_title.warehouse.inventorycount.tableTitle')" :table-title="$t('_title.warehouse.inventorycount.tableTitle')"
item-array-name="countItems" item-array-name="countItems"
upload-desc="盘点明细"
:mapping-config="mappingConfig" :mapping-config="mappingConfig"
:template-file-name="$t('_title.warehouse.inventorycount.templateFileName')"
> >
<template #form-items> <template #form-items>
<el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item> <el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item>

View File

@@ -351,8 +351,8 @@ const authShowFunc = (row: any, button: globalThis.ButtonProp) => {
:base-title="$t('_title.warehouse.stocktransferorder.baseTitle')" :base-title="$t('_title.warehouse.stocktransferorder.baseTitle')"
:table-title="$t('_title.warehouse.stocktransferorder.tableTitle')" :table-title="$t('_title.warehouse.stocktransferorder.tableTitle')"
item-array-name="transferOrderItems" item-array-name="transferOrderItems"
upload-desc="物料信息"
:mapping-config="mappingConfig" :mapping-config="mappingConfig"
:template-file-name="$t('_title.warehouse.stocktransferorder.templateFileName')"
> >
<template #form-items> <template #form-items>
<el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item> <el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item>

View File

@@ -16,7 +16,7 @@ import { ElMessage } from "element-plus";
import { ref } from "vue"; import { ref } from "vue";
import { generateDucumentNo } from "@/common/utils/document-no-generator/document-no-generator"; import { generateDucumentNo } from "@/common/utils/document-no-generator/document-no-generator";
import { DocumentType } from "@/common/enums/DocumentType"; import { DocumentType } from "@/common/enums/DocumentType";
import { type FieldMappingConfig } from "@/components/base/base-form-with-table/type"; import { type FieldMappingConfig, type DetailLoadConfig } from "@/components/base/base-form-with-table/type";
import BaseItemDialog from "@/components/base/base-item-dialog/BaseItemDialog.vue"; import BaseItemDialog from "@/components/base/base-item-dialog/BaseItemDialog.vue";
const getPageUrl = "/warehouse/warehousereceipt/getWarehouseReceiptPage"; const getPageUrl = "/warehouse/warehousereceipt/getWarehouseReceiptPage";
@@ -62,15 +62,21 @@ const rules = reactive<FormRules>({
const mappingConfig: FieldMappingConfig = { const mappingConfig: FieldMappingConfig = {
partNumber: { partNumber: {
sourceKey: ["商品编号", "物料编号", "partNumber"], sourceKey: ["商品编号", "物料编号", "partNumber", "物料编码"],
header: $t("_prop.warehouse.warehousereceipt.partNumber"),
width: 20,
defaultValue: "", defaultValue: "",
}, },
productSpec: { productSpec: {
sourceKey: ["型号", "物料型号", "productSpecs", "productSpec"], sourceKey: ["物料规格", "型号", "物料型号", "规格", "规格型号", "productSpecs", "productSpec"],
header: $t("_prop.warehouse.warehousereceipt.productSpec"),
width: 20,
defaultValue: "", defaultValue: "",
}, },
productCount: { productCount: {
sourceKey: ["数量", "入库数量", "productCount"], sourceKey: ["数量", "入库数量", "productCount", "入库数"],
header: $t("_prop.warehouse.warehousereceipt.productCount"),
width: 15,
defaultValue: 0, defaultValue: 0,
transform: (val: any) => { transform: (val: any) => {
const num = Number(val); const num = Number(val);
@@ -79,6 +85,12 @@ const mappingConfig: FieldMappingConfig = {
}, },
}; };
const detailConfig: DetailLoadConfig = {
url: getItemUrl,
paramName: "id",
dataPath: "data",
};
const add = () => { const add = () => {
form.value = { form.value = {
formCode: generateDucumentNo(DocumentType.WarehouseReceipt), formCode: generateDucumentNo(DocumentType.WarehouseReceipt),
@@ -96,16 +108,6 @@ const add = () => {
const edit = (row: any) => { const edit = (row: any) => {
title.value = "_title.warehouse.warehousereceipt.edit"; title.value = "_title.warehouse.warehousereceipt.edit";
form.value = { ...row }; form.value = { ...row };
// 加载明细数据
get(getItemUrl, { id: row.id })
.then(res => {
if (res.code === 0) {
form.value.receiptItems = res.data || [];
}
})
.catch(() => {
form.value.receiptItems = [];
});
visible.value = true; visible.value = true;
formType.value = true; formType.value = true;
}; };
@@ -240,8 +242,9 @@ const submit = (form: any, formRef: FormInstance | undefined) => {
:base-title="$t('_title.warehouse.warehousereceipt.baseTitle')" :base-title="$t('_title.warehouse.warehousereceipt.baseTitle')"
:table-title="$t('_title.warehouse.warehousereceipt.tableTitle')" :table-title="$t('_title.warehouse.warehousereceipt.tableTitle')"
item-array-name="receiptItems" item-array-name="receiptItems"
upload-desc="物料信息"
:mapping-config="mappingConfig" :mapping-config="mappingConfig"
:template-file-name="$t('_title.warehouse.warehousereceipt.templateFileName')"
:detail-config="detailConfig"
> >
<template #form-items> <template #form-items>
<el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item> <el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item>