feat: 完成 SN 溯源、销售管理和维修记录。

This commit is contained in:
c
2026-03-12 16:26:59 +08:00
parent d22857b890
commit e7ac6b5f62
8 changed files with 556 additions and 68 deletions

View File

@@ -40,6 +40,7 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@icon-park/vue-next": "^1.4.2", "@icon-park/vue-next": "^1.4.2",
"@remixicon/vue": "^4.9.0",
"@vueuse/core": "^13.7.0", "@vueuse/core": "^13.7.0",
"axios": "^1.11.0", "axios": "^1.11.0",
"decimal.js": "^10.6.0", "decimal.js": "^10.6.0",

20
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
'@icon-park/vue-next': '@icon-park/vue-next':
specifier: ^1.4.2 specifier: ^1.4.2
version: 1.4.2(vue@3.5.18(typescript@5.9.2)) version: 1.4.2(vue@3.5.18(typescript@5.9.2))
'@remixicon/vue':
specifier: ^4.9.0
version: 4.9.0(vue@3.5.18(typescript@5.9.2))
'@vueuse/core': '@vueuse/core':
specifier: ^13.7.0 specifier: ^13.7.0
version: 13.7.0(vue@3.5.18(typescript@5.9.2)) version: 13.7.0(vue@3.5.18(typescript@5.9.2))
@@ -44,6 +47,9 @@ 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
@@ -913,6 +919,11 @@ packages:
'@polka/url@1.0.0-next.29': '@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@remixicon/vue@4.9.0':
resolution: {integrity: sha512-4P0MY5ScL+MRLnHu1iT3xHAsliBwOGDIgSgHCZhSj2wkLa5NzqiQ4SGD/04VoHM+BRr8+xjmF7ZemkOUlMDJ3A==}
peerDependencies:
vue: '>= 3'
'@rolldown/pluginutils@1.0.0-beta.26': '@rolldown/pluginutils@1.0.0-beta.26':
resolution: {integrity: sha512-r/5po89voz/QRPDmoErL10+hVuTAuz1SHvokx+yWBlOIPB5C41jC7QhLqq9kaebx/+EHyoV3z22/qBfX81Ns8A==} resolution: {integrity: sha512-r/5po89voz/QRPDmoErL10+hVuTAuz1SHvokx+yWBlOIPB5C41jC7QhLqq9kaebx/+EHyoV3z22/qBfX81Ns8A==}
@@ -4249,6 +4260,9 @@ 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'}
@@ -6040,6 +6054,10 @@ snapshots:
'@polka/url@1.0.0-next.29': {} '@polka/url@1.0.0-next.29': {}
'@remixicon/vue@4.9.0(vue@3.5.18(typescript@5.9.2))':
dependencies:
vue: 3.5.18(typescript@5.9.2)
'@rolldown/pluginutils@1.0.0-beta.26': {} '@rolldown/pluginutils@1.0.0-beta.26': {}
'@rolldown/pluginutils@1.0.0-beta.29': {} '@rolldown/pluginutils@1.0.0-beta.29': {}
@@ -9657,6 +9675,8 @@ 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

@@ -320,6 +320,7 @@ export default {
customerName: "客户名称", customerName: "客户名称",
customerId: "客户", customerId: "客户",
partNumber: "物料编号", partNumber: "物料编号",
productSpecs: "物料型号",
saleCount: "销售数量", saleCount: "销售数量",
price: "单价", price: "单价",
totalPrice: "总价", totalPrice: "总价",
@@ -333,7 +334,7 @@ export default {
serialNum: "序列号", serialNum: "序列号",
productSn: "SN 号", productSn: "SN 号",
repairMark: "返修记录", repairMark: "返修记录",
outProductDate: "出货时间", outProductDate: "出货日期",
productType: "型号", productType: "型号",
mac: "MAC 地址", mac: "MAC 地址",
alNum: "算法标志", alNum: "算法标志",
@@ -354,6 +355,11 @@ export default {
manufacturingDate: "生产日期", manufacturingDate: "生产日期",
mark: "备注", mark: "备注",
keyAccountId: "客户", keyAccountId: "客户",
repairCount: "维修次数",
serialNum: "序列号",
softVersion: "软件版本",
alVersion: "算法版本",
createDate: "创建日期",
}, },
}, },
systemset: { systemset: {
@@ -456,6 +462,7 @@ export default {
reject_success: "反审成功", reject_success: "反审成功",
reject_fail: "反审失败", reject_fail: "反审失败",
reject_cannel: "反审取消", reject_cannel: "反审取消",
all_customers: "所有客户",
}, },
warehouse: { warehouse: {
warehouse_item: { warehouse_item: {
@@ -680,15 +687,6 @@ export default {
input_productSn: "请输入SN号", input_productSn: "请输入SN号",
input_mac: "请输入MAC地址", input_mac: "请输入MAC地址",
}, },
repairrecord: {
input_productSn: "请输入SN号",
input_mac: "请输入MAC地址",
input_repairStatus: "请选择维修状态",
input_repairDate: "请输入维修日期",
input_manufacturingDate: "请输入生产日期",
input_mark: "请输入备注",
input_repairMark: "请输入返修记录",
},
}, },
purchase: { purchase: {
purchase_plan: { purchase_plan: {
@@ -780,6 +778,24 @@ export default {
import_fail: "导入失败", import_fail: "导入失败",
delete_message: "删除销售订单", delete_message: "删除销售订单",
}, },
repairrecord: {
input_productSn: "请输入SN号",
input_mac: "请输入MAC地址",
input_repairStatus: "请选择维修状态",
input_repairDate: "请输入维修日期",
input_manufacturingDate: "请输入生产日期",
input_mark: "请输入备注",
input_repairMark: "请输入返修记录",
input_repair_mark: "请输入返修记录",
input_identifier: "SN号、MAC地址、序列号至少填写一个",
delete_message: "删除维修记录",
identifier_tip: "提示SN号、MAC地址、序列号三者至少填写一个",
input_productSn_optional: "SN号选填",
input_mac_optional: "MAC地址选填",
input_serialNum_optional: "序列号(选填)",
input_softVersion_optional: "软件版本(选填)",
input_alVersion_optional: "算法版本(选填)",
},
}, },
systemset: { systemset: {
vendor: { vendor: {
@@ -915,6 +931,13 @@ export default {
saleorder: { saleorder: {
add: "添加销售订单", add: "添加销售订单",
edit: "编辑销售订单", edit: "编辑销售订单",
showItem: "销售明细",
baseTitle: "销售订单基本信息",
tableTitle: "销售明细",
},
repairrecord: {
add: "添加维修记录",
edit: "编辑维修记录",
}, },
}, },
systemset: { systemset: {

View File

@@ -7,6 +7,7 @@ import { useStatus } from "@/common/languages/mapping/base-info-mapping";
const props = defineProps({ const props = defineProps({
tableEl: ref, tableEl: ref,
statusLabelMapping: Function, statusLabelMapping: Function,
tagTypeMapping: Function,
statusParamName: { statusParamName: {
type: String, type: String,
default: "status", default: "status",
@@ -37,14 +38,26 @@ const getLabel = (code: number | null) => {
if (props.statusLabelMapping === undefined) return getCommonStatusLabel(code); if (props.statusLabelMapping === undefined) return getCommonStatusLabel(code);
return props.statusLabelMapping(code); return props.statusLabelMapping(code);
}; };
const getTagType = (code: number | null): string => {
if (props.tagTypeMapping !== undefined) {
return props.tagTypeMapping(code);
}
// 默认逻辑switchOnValue 为 success其他为 info
return code === props.switchOnValue ? "success" : "info";
};
</script> </script>
<template> <template>
<el-table-column :label="$t('_prop.common.status')" :prop="statusParamName"> <el-table-column :label="$t('_prop.common.status')" :prop="statusParamName">
<template #default="scope"> <template #default="scope">
<!-- 没有权限按钮时显示带框标签 -->
<span v-if="buttonList.length === 0"> <span v-if="buttonList.length === 0">
{{ getLabel(scope.row[statusParamName]) }} <el-tag :type="getTagType(scope.row[statusParamName])" size="small">
{{ getLabel(scope.row[statusParamName]) }}
</el-tag>
</span> </span>
<template v-for="button in buttonList" :key="button.buttonName"> <!-- 有权限按钮时显示开关或标签 -->
<template v-else v-for="button in buttonList" :key="button.buttonName">
<StatusSwitch <StatusSwitch
v-model="scope.row[statusParamName]" v-model="scope.row[statusParamName]"
@change="val => change(val, scope.row)" @change="val => change(val, scope.row)"
@@ -54,6 +67,21 @@ const getLabel = (code: number | null) => {
" "
/> />
</template> </template>
<!-- 有权限但没有匹配到开关时,显示带框标签 -->
<el-tag
v-if="
buttonList.length > 0 &&
!buttonList.some(
button =>
(button.eventName === 'enable' && scope.row[statusParamName] === switchOffValue) ||
(button.eventName === 'disable' && scope.row[statusParamName] === switchOnValue)
)
"
:type="getTagType(scope.row[statusParamName])"
size="small"
>
{{ getLabel(scope.row[statusParamName]) }}
</el-tag>
</template> </template>
</el-table-column> </el-table-column>
</template> </template>

View File

@@ -83,7 +83,7 @@ export const dynamicRoutes: RouterConfigRaw[] = [
component: "/systemset/vendor/form", component: "/systemset/vendor/form",
meta: { meta: {
title: "总表", title: "总表",
icon: Compass, icon: "ep:Compass",
}, },
}, },
]; ];

View File

@@ -1,18 +1,25 @@
<script lang="ts" setup> <script lang="ts" setup>
import TreeSidePageableTable from "@/components/base/treeside-pageable-table/TreeSidePageableTable.vue"; import TreeSidePageableTable from "@/components/base/treeside-pageable-table/TreeSidePageableTable.vue";
import { $t } from "@/common/languages"; import { $t } from "@/common/languages";
import { ref } from "vue";
/** /**
* 必须要的变量 * 必须要的变量
*/ */
const getPageUrl = "/sale/device/getDevicePage"; const tableRef = ref<InstanceType<typeof TreeSidePageableTable> | null>(null);
const treeSideUrl = "/sale/device/getKeyAccount"; const getPageUrl = "/sale/device/getDeviceSnPage";
const treeSideUrl = "/sys/keyaccount/getKeyAccountTree";
const searchers = [ const searchers = [
{ {
name: "searchCode", name: "searchCode",
type: "text" as const, type: "text" as const,
placeholder: $t("_prop.sale.device.searchCode"), placeholder:
$t("_prop.sale.devicesn.productSn") +
"/" +
$t("_prop.sale.devicesn.mac") +
"/" +
$t("_prop.sale.devicesn.serialNum"),
}, },
]; ];
</script> </script>
@@ -22,21 +29,19 @@ const searchers = [
:searchers="searchers" :searchers="searchers"
ref="tableRef" ref="tableRef"
:tree-side-url="treeSideUrl" :tree-side-url="treeSideUrl"
tree-side-node-name="keyAccountId" tree-side-node-name="id"
tree-side-param-name="keyAccountId" tree-side-param-name="keyAccountId"
tree-side-title="所有客户" :tree-side-title="$t('_message.common.all_customers')"
> >
<template #columns> <template #columns>
<el-table-column :label="$t('_prop.sale.device.productType')" prop="productType" /> <el-table-column :label="$t('_prop.sale.devicesn.productType')" prop="productType" />
<el-table-column :label="$t('_prop.sale.device.productSn')" prop="productSn" /> <el-table-column :label="$t('_prop.sale.devicesn.productSn')" prop="productSn" />
<el-table-column :label="$t('_prop.sale.device.mac')" prop="mac" /> <el-table-column :label="$t('_prop.sale.devicesn.mac')" prop="mac" />
<el-table-column :label="$t('_prop.sale.device.serialNum')" prop="serialNum" /> <el-table-column :label="$t('_prop.sale.devicesn.serialNum')" prop="serialNum" />
<el-table-column :label="$t('_prop.sale.device.softVersion')" prop="softVersion" /> <el-table-column :label="$t('_prop.sale.devicesn.softVersion')" prop="softVersion" />
<el-table-column :label="$t('_prop.sale.device.alVersion')" prop="alVersion" /> <el-table-column :label="$t('_prop.sale.devicesn.AlVersion')" prop="alVersion" />
<el-table-column :label="$t('_prop.sale.device.alNum')" prop="alNum" /> <el-table-column :label="$t('_prop.sale.devicesn.outProductDate')" prop="outProductDate" />
<el-table-column :label="$t('_prop.sale.device.outStatus')" prop="outStatus" /> <el-table-column :label="$t('_prop.sale.devicesn.repairMark')" prop="repairRecords" show-overflow-tooltip />
<el-table-column :label="$t('_prop.sale.device.outProductDate')" prop="outProductDate" />
<el-table-column :label="$t('_prop.sale.device.repairMark')" prop="repairMark" />
</template> </template>
</TreeSidePageableTable> </TreeSidePageableTable>
</template> </template>

View File

@@ -6,26 +6,64 @@ import BaseForm from "@/components/base/base-form/BaseForm.vue";
import { $t } from "@/common/languages"; import { $t } from "@/common/languages";
import type { FormInstance, FormRules } from "element-plus"; import type { FormInstance, FormRules } from "element-plus";
import TreeSidePageableTable from "@/components/base/treeside-pageable-table/TreeSidePageableTable.vue"; import TreeSidePageableTable from "@/components/base/treeside-pageable-table/TreeSidePageableTable.vue";
import { get } from "@/common/http/request";
/** /**
* 必须要的变量 * 必须要的变量
*/ */
const getPageUrl = "/sale/repairrecord/getRepairRecordPage"; const getPageUrl = "/sale/repairrecord/getRepairReportPage";
const addUrl = "/sale/repairrecord/addRepairRecord"; const addUrl = "/sale/repairrecord/addRepairRecord";
const editUrl = "/sale/repairrecord/updateRepairRecord"; const editUrl = "/sale/repairrecord/updateRepairRecord";
const removeUrl = "/sale/repairrecord/deleteRepairRecord"; const removeUrl = "/sale/repairrecord/deleteRepairRecord";
const treeSideUrl = "/sale/repairrecord/getKeyAccount"; const treeSideUrl = "/sys/keyaccount/getKeyAccountTree";
const keyAccountSelectUrl = "/sys/keyaccount/getKeyAccountSelectList";
const searchers = [ const searchers = [
{ name: "searchCode", type: "text" as const, placeholder: $t("_prop.sale.repairrecord.searchCode") }, {
name: "searchCode",
type: "text" as const,
placeholder: $t("_prop.sale.repairrecord.productSn") + "/" + $t("_prop.sale.repairrecord.mac"),
clearable: true,
},
]; ];
const rules = reactive<FormRules>({});
const validateIdentifier = (_rule: any, _value: any, callback: any) => {
const formData = form.value;
if (formData.productSn || formData.mac || formData.serialNum) {
callback();
} else {
callback(new Error($t("_message.sale.repairrecord.input_identifier")));
}
};
const rules = reactive<FormRules>({
repairMark: [{ required: true, message: $t("_message.sale.repairrecord.input_repair_mark"), trigger: "blur" }],
productSn: [{ validator: validateIdentifier, trigger: "blur" }],
});
const keyAccountOptions = ref<{ label: string; value: string }[]>([]);
const loadKeyAccountOptions = async () => {
const res = await get(keyAccountSelectUrl);
if (res.data) {
keyAccountOptions.value = res.data.map((item: { label: string; value: string }) => ({
label: item.label,
value: item.value,
}));
}
};
onMounted(() => {
loadKeyAccountOptions();
});
/** /**
* 基本不变通用变量 * 基本不变通用变量
*/ */
const tableRef = ref<InstanceType<typeof TreeSidePageableTable> | null>(null); const tableRef = ref<InstanceType<typeof TreeSidePageableTable> | null>(null);
const { useAdd, useEdit, useRemove, useGeneralPageRef } = usePage(tableRef); const { useAdd, useEdit, useRemove, useGeneralPageRef } = usePage(tableRef);
const { title, visible, formType, form } = useGeneralPageRef(); const { title, visible, formType, form } = useGeneralPageRef();
/** /**
* 可以自定义的变量 * 可以自定义的变量
*/ */
@@ -36,25 +74,32 @@ const add = () => {
visible.value = true; visible.value = true;
formType.value = false; formType.value = false;
}; };
const edit = (row: any) => { const edit = (row: any) => {
title.value = "_title.sale.repairrecord.edit"; title.value = "_title.sale.repairrecord.edit";
form.value = { ...row }; form.value = { ...row };
visible.value = true; visible.value = true;
formType.value = true; formType.value = true;
}; };
const remove = (row: any) => { const remove = (row: any) => {
useRemove(removeUrl, row.id, "_message.sale.repairrecord.delete_message"); useRemove(removeUrl, row.id, "_message.sale.repairrecord.delete_message");
}; };
const submit = (form: any, formRef: FormInstance | undefined) => {
const submit = (formData: any, formRef: FormInstance | undefined) => {
if (formRef !== undefined) { if (formRef !== undefined) {
formRef.validate(valid => { formRef.validate(valid => {
if (valid) { if (valid) {
if (formType.value) useEdit(editUrl, form, visible); if (formType.value) {
else useAdd(addUrl, form, visible); useEdit(editUrl, formData, visible);
} else {
useAdd(addUrl, formData, visible);
}
} }
}); });
} }
}; };
const topButtonClick = (eventName: string) => { const topButtonClick = (eventName: string) => {
switch (eventName) { switch (eventName) {
case "add": case "add":
@@ -62,6 +107,7 @@ const topButtonClick = (eventName: string) => {
break; break;
} }
}; };
const operateButtonClick = (eventName: string, row: any) => { const operateButtonClick = (eventName: string, row: any) => {
switch (eventName) { switch (eventName) {
case "edit": case "edit":
@@ -79,27 +125,98 @@ const operateButtonClick = (eventName: string, row: any) => {
:searchers="searchers" :searchers="searchers"
ref="tableRef" ref="tableRef"
:tree-side-url="treeSideUrl" :tree-side-url="treeSideUrl"
tree-side-node-name="keyAccountId" tree-side-node-name="id"
tree-side-param-name="keyAccountId" tree-side-param-name="keyAccountIdSearch"
tree-side-title="所有客户" :tree-side-title="$t('_message.common.all_customers')"
> >
<template #tool-button> <template #tool-button>
<DefaultToolButton @top-button-click="topButtonClick" /> <DefaultToolButton @top-button-click="topButtonClick" />
</template> </template>
<template #columns> <template #columns>
<el-table-column prop="id" type="hidden" width="40" />
<el-table-column :label="$t('_prop.sale.repairrecord.productType')" prop="productType" /> <el-table-column :label="$t('_prop.sale.repairrecord.productType')" prop="productType" />
<el-table-column :label="$t('_prop.sale.repairrecord.productSn')" prop="productSn" /> <el-table-column :label="$t('_prop.sale.repairrecord.productSn')" prop="productSn" />
<el-table-column :label="$t('_prop.sale.repairrecord.mac')" prop="mac" /> <el-table-column :label="$t('_prop.sale.repairrecord.mac')" prop="mac" />
<el-table-column :label="$t('_prop.sale.repairrecord.outProductDate')" prop="outProductDate" /> <el-table-column :label="$t('_prop.sale.repairrecord.outProductDate')" prop="outProductDate" />
<el-table-column :label="$t('_prop.sale.repairrecord.repairDate')" prop="repairDate" /> <el-table-column :label="$t('_prop.common.createDate')" prop="createDate" />
<el-table-column :label="$t('_prop.sale.repairrecord.repairMark')" prop="repairMark" /> <el-table-column :label="$t('_prop.sale.repairrecord.repairCount')" prop="repairCount" />
<el-table-column :label="$t('_prop.sale.repairrecord.repairMark')" prop="repairRecords" show-overflow-tooltip />
<DefaultOperateButtonColumn @operate-button-click="operateButtonClick" /> <DefaultOperateButtonColumn @operate-button-click="operateButtonClick" />
</template> </template>
</TreeSidePageableTable> </TreeSidePageableTable>
<BaseForm v-model:visible="visible" @submit="submit" v-model:form="form" :title="$t(title)" :rules="rules"> <BaseForm v-model:visible="visible" @submit="submit" v-model:form="form" :title="$t(title)" :rules="rules">
<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>
<el-alert
:title="$t('_message.sale.repairrecord.identifier_tip')"
type="info"
:closable="false"
style="margin-bottom: 15px"
/>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('_prop.sale.repairrecord.productType')" prop="productType">
<el-input v-model="form.productType" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('_prop.sale.repairrecord.productSn')" prop="productSn">
<el-input
v-model="form.productSn"
:placeholder="$t('_message.sale.repairrecord.input_productSn_optional')"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('_prop.sale.repairrecord.mac')" prop="mac">
<el-input v-model="form.mac" :placeholder="$t('_message.sale.repairrecord.input_mac_optional')" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('_prop.sale.devicesn.serialNum')" prop="serialNum">
<el-input
v-model="form.serialNum"
:placeholder="$t('_message.sale.repairrecord.input_serialNum_optional')"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('_prop.sale.devicesn.softVersion')" prop="softVersion">
<el-input
v-model="form.softVersion"
:placeholder="$t('_message.sale.repairrecord.input_softVersion_optional')"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('_prop.sale.devicesn.AlVersion')" prop="alVersion">
<el-input
v-model="form.alVersion"
:placeholder="$t('_message.sale.repairrecord.input_alVersion_optional')"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('_prop.sale.repairrecord.keyAccountId')" prop="keyAccountId">
<el-select v-model="form.keyAccountId" clearable style="width: 100%">
<el-option
v-for="item in keyAccountOptions"
:key="item.value"
:label="item.label"
:value="Number(item.value)"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="$t('_prop.sale.repairrecord.repairMark')" prop="repairMark">
<el-input v-model="form.repairMark" type="textarea" :rows="3" />
</el-form-item>
</template> </template>
</BaseForm> </BaseForm>
</template> </template>

View File

@@ -1,12 +1,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import BasePageableTable from "@/components/base/base-pageable-table/BasePageableTable.vue";
import DefaultToolButton from "@/components/base/default-tool-button/DefaultToolButton.vue"; import DefaultToolButton from "@/components/base/default-tool-button/DefaultToolButton.vue";
import DefaultOperateButtonColumn from "@/components/base/default-column/DefaultOperateButtonColumn.vue"; import DefaultOperateButtonColumn from "@/components/base/default-column/DefaultOperateButtonColumn.vue";
import DefaultStatusSwitchColumn from "@/components/base/default-column/DefaultStatusSwitchColumn.vue";
import { usePage } from "@/composables/use-page"; import { usePage } from "@/composables/use-page";
import BaseForm from "@/components/base/base-form/BaseForm.vue";
import { $t } from "@/common/languages"; import { $t } from "@/common/languages";
import type { FormInstance, FormRules } from "element-plus";
import { formatDate } from "@/common/utils/format-utils"; import { formatDate } from "@/common/utils/format-utils";
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
import BaseFormWithTable from "@/components/base/base-form-with-table/BaseFormWithTable.vue";
import BaseItemDialog from "@/components/base/base-item-dialog/BaseItemDialog.vue";
import ExpandablePageableTable from "@/components/base/expandable-pageable-table/ExpandablePageableTable.vue";
import { get } from "@/common/http/request";
import { type FieldMappingConfig } from "@/components/base/base-form-with-table/type";
import { useStatus } from "@/common/languages/mapping/base-info-mapping";
/** /**
* 必须要的变量 * 必须要的变量
@@ -16,45 +21,152 @@ const getPageUrl = "/sale/saleorder/getSaleOrderPage";
const addUrl = "/sale/saleorder/addSaleOrder"; const addUrl = "/sale/saleorder/addSaleOrder";
const editUrl = "/sale/saleorder/updateSaleOrder"; const editUrl = "/sale/saleorder/updateSaleOrder";
const removeUrl = "/sale/saleorder/deleteSaleOrder"; const removeUrl = "/sale/saleorder/deleteSaleOrder";
const approveUrl = "/sale/saleorder/approveSaleOrder";
const rejectUrl = "/sale/saleorder/unapproveSaleOrder";
const itemUrl = "/sale/saleorder/getSaleOrderItemList";
const keyAccountSelectUrl = "/sys/keyaccount/getKeyAccountSelectList";
const searchers = [ const searchers = [
{ name: "customerName", type: "text" as const, placeholder: $t("_prop.sale.saleorder.customerName") }, {
name: "customerName",
type: "text" as const,
placeholder: $t("_prop.sale.saleorder.customerName"),
clearable: true,
},
]; ];
const rules = reactive<FormRules>({});
const rules = reactive<FormRules>({
formCode: [{ required: true, message: $t("_message.sale.saleorder.input_formCode"), 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" }],
customerName: [{ required: true, message: $t("_message.sale.saleorder.select_customerId"), trigger: "change" }],
saleOrderItems: [
{
required: true,
validator: (rule, value, callback) => {
if (value === undefined || value.length === 0) {
callback($t("_message.sale.saleorder.no_sale_order_items"));
}
callback();
},
trigger: "change",
},
],
});
const itemArrayName = "saleOrderItems";
const getDetailUrl = "/sale/saleorder/getSaleOrderItemList";
/** /**
* 基本不变通用变量 * 基本不变通用变量
*/ */
const tableRef = ref<InstanceType<typeof BasePageableTable> | null>(null); const tableRef = ref<InstanceType<typeof ExpandablePageableTable> | null>(null);
const { useAdd, useEdit, useRemove, useGeneralPageRef } = usePage(tableRef); const { useAdd, useEdit, useRemove, useGeneralPageRef, useApprove, useReject } = usePage(tableRef);
const { title, visible, formType, form } = useGeneralPageRef(); const { title, visible, formType, form, itemVisible, itemParentId } = useGeneralPageRef();
const { getFormStatusLabel } = useStatus();
const baseFormWithTableRef = ref<InstanceType<typeof BaseFormWithTable>>();
/** /**
* 可以自定义的变量 * 可以自定义的变量
*/ */
const keyAccountOptions = ref<{ label: string; value: string }[]>([]);
const loadKeyAccountOptions = async () => {
const res = await get(keyAccountSelectUrl);
if (res.data) {
keyAccountOptions.value = res.data.map((item: { label: string; value: string }) => ({
label: item.label,
value: item.value,
}));
}
};
onMounted(() => {
loadKeyAccountOptions();
});
// 生成单据编号
const generateFormCode = () => {
const now = new Date();
const dateStr =
now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, "0") +
String(now.getDate()).padStart(2, "0") +
String(now.getHours()).padStart(2, "0") +
String(now.getMinutes()).padStart(2, "0") +
String(now.getSeconds()).padStart(2, "0");
return "SO" + dateStr;
};
// 监听客户选择变化,同步更新客户名称
const handleCustomerChange = (value: number) => {
const selected = keyAccountOptions.value.find(item => Number(item.value) === value);
if (selected) {
form.value.customerName = selected.label;
}
};
const add = () => { const add = () => {
form.value = {}; form.value = {
formCode: generateFormCode(),
};
title.value = "_title.sale.saleorder.add"; title.value = "_title.sale.saleorder.add";
visible.value = true; visible.value = true;
formType.value = false; formType.value = false;
}; };
const edit = (row: any) => { const edit = (row: any) => {
title.value = "_title.sale.saleorder.edit"; title.value = "_title.sale.saleorder.edit";
form.value = { ...row }; form.value = {
...row,
saleOrderItems: [],
};
visible.value = true; visible.value = true;
formType.value = true; formType.value = true;
// 加载明细数据
get(getDetailUrl, { saleOrderId: row.id })
.then(res => {
form.value.saleOrderItems = res.data || [];
})
.catch(err => {
console.error("Failed to load sale order items:", err);
});
}; };
const remove = (row: any) => { const remove = (row: any) => {
useRemove(removeUrl, row.id, "_message.sale.saleorder.delete_message"); useRemove(removeUrl, row.id, "_message.sale.saleorder.delete_message");
}; };
const submit = (form: any, formRef: FormInstance | undefined) => {
if (formRef !== undefined) { const approve = (row: any) => {
formRef.validate(valid => { useApprove(approveUrl, row.id);
if (valid) {
if (formType.value) useEdit(editUrl, form, visible);
else useAdd(addUrl, form, visible);
}
});
}
}; };
const reject = (row: any) => {
useReject(rejectUrl, row.id);
};
const showItem = (row: any) => {
itemParentId.value = row.id;
itemVisible.value = true;
};
const submit = (formData: any, formRef: FormInstance | undefined) => {
const targetRef = formRef || (baseFormWithTableRef.value as any)?.baseFormRef;
if (!targetRef) return;
targetRef.validate((valid: any) => {
if (valid) {
if (formType.value) {
useEdit(editUrl, formData, visible);
} else {
useAdd(addUrl, formData, visible);
}
}
});
};
const topButtonClick = (eventName: string) => { const topButtonClick = (eventName: string) => {
switch (eventName) { switch (eventName) {
case "add": case "add":
@@ -62,6 +174,7 @@ const topButtonClick = (eventName: string) => {
break; break;
} }
}; };
const operateButtonClick = (eventName: string, row: any) => { const operateButtonClick = (eventName: string, row: any) => {
switch (eventName) { switch (eventName) {
case "edit": case "edit":
@@ -70,29 +183,210 @@ const operateButtonClick = (eventName: string, row: any) => {
case "remove": case "remove":
remove(row); remove(row);
break; break;
case "approve":
approve(row);
break;
case "reject":
reject(row);
break;
case "showItem":
showItem(row);
break;
}
};
const authShowFunc = (row: any, button: globalThis.ButtonProp) => {
if (row.formStatus === 1 && button.eventName === "approve") return false;
if (row.formStatus === 0 && button.eventName === "reject") return false;
if (row.formStatus === 1 && button.eventName === "edit") return false;
if (row.formStatus === 1 && button.eventName === "remove") return false;
return true;
};
const mappingConfig: FieldMappingConfig = {
partNumber: {
sourceKey: "物料编号",
defaultValue: "",
},
productSpecs: {
sourceKey: "物料型号",
defaultValue: "",
},
saleCount: {
sourceKey: "数量",
defaultValue: 0,
transform: (val: any) => {
const num = Number(val);
return isNaN(num) ? 0 : num;
},
},
price: {
sourceKey: "单价",
defaultValue: 0,
transform: (val: any) => {
const num = Number(val);
return isNaN(num) ? 0 : num;
},
},
saleMark: {
sourceKey: "备注",
defaultValue: "",
},
};
// 计算总价
const calculateTotalPrice = (row: any) => {
if (row.saleCount && row.price) {
row.totalPrice = Number((row.saleCount * row.price).toFixed(2));
} }
}; };
</script> </script>
<template> <template>
<BasePageableTable :url="getPageUrl" :searchers="searchers" ref="tableRef"> <ExpandablePageableTable
:url="getPageUrl"
:searchers="searchers"
ref="tableRef"
:item-url="itemUrl"
item-id-key="id"
item-id-name="saleOrderId"
item-field-name="saleOrderItems"
>
<template #tool-button> <template #tool-button>
<DefaultToolButton @top-button-click="topButtonClick" /> <DefaultToolButton @top-button-click="topButtonClick" />
</template> </template>
<template #columns> <template #columns>
<el-table-column prop="id" type="hidden" width="40" />
<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.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" /> <el-table-column :label="$t('_prop.sale.saleorder.formMark')" prop="formMark" show-overflow-tooltip />
<el-table-column :label="$t('_prop.common.createDate')" prop="createDate" :formatter="formatDate" /> <el-table-column :label="$t('_prop.common.createDate')" prop="createDate" :formatter="formatDate" />
<el-table-column :label="$t('_prop.sale.saleorder.formStatus')" prop="formStatus" /> <DefaultStatusSwitchColumn
<DefaultOperateButtonColumn @operate-button-click="operateButtonClick" /> status-param-name="formStatus"
:status-label-mapping="getFormStatusLabel"
:switch-on-value="1"
:switch-off-value="0"
/>
<DefaultOperateButtonColumn @operate-button-click="operateButtonClick" :auth-show-func="authShowFunc" />
</template> </template>
</BasePageableTable> <template #item-content="{ itemData }">
<BaseForm v-model:visible="visible" @submit="submit" v-model:form="form" :title="$t(title)" :rules="rules"> <el-table :data="itemData" size="small" border stripe>
<el-table-column :label="$t('_prop.sale.saleorder.partNumber')" prop="partNumber" width="150" />
<el-table-column :label="$t('_prop.sale.saleorder.productSpecs')" prop="productSpecs" width="200" />
<el-table-column :label="$t('_prop.sale.saleorder.saleCount')" prop="saleCount" />
<el-table-column :label="$t('_prop.sale.saleorder.price')" prop="price" />
<el-table-column :label="$t('_prop.sale.saleorder.totalPrice')" prop="totalPrice" />
</el-table>
</template>
</ExpandablePageableTable>
<BaseFormWithTable
ref="baseFormWithTableRef"
v-model:visible="visible"
@submit="submit"
v-model:form="form"
:title="$t(title)"
:rules="rules"
:base-title="$t('_title.sale.saleorder.baseTitle')"
:table-title="$t('_title.sale.saleorder.tableTitle')"
item-array-name="saleOrderItems"
upload-desc="销售明细"
:mapping-config="mappingConfig"
>
<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>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item :label="$t('_prop.sale.saleorder.formCode')" prop="formCode">
<el-input v-model="form.formCode" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('_prop.sale.saleorder.customerId')" prop="customerId">
<el-select v-model="form.customerId" clearable style="width: 100%" @change="handleCustomerChange">
<el-option
v-for="item in keyAccountOptions"
:key="item.value"
:label="item.label"
:value="Number(item.value)"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('_prop.sale.saleorder.formName')" prop="formName">
<el-input v-model="form.formName" :placeholder="$t('_message.sale.saleorder.input_formName')" />
</el-form-item>
</el-col>
</el-row>
<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="2"
/>
</el-form-item>
</template> </template>
</BaseForm> <template #form-table-columns>
<el-table-column :label="$t('_prop.sale.saleorder.partNumber')" width="150">
<template #default="{ row, $index }">
<el-form-item :prop="`${itemArrayName}.${$index}.partNumber`" :rules="rules.partNumber">
<el-input v-model="row.partNumber" size="small" />
</el-form-item>
</template>
</el-table-column>
<el-table-column :label="$t('_prop.sale.saleorder.saleCount')" width="120">
<template #default="{ row, $index }">
<el-form-item :prop="`${itemArrayName}.${$index}.saleCount`">
<el-input-number v-model="row.saleCount" size="small" :min="1" @change="calculateTotalPrice(row)" />
</el-form-item>
</template>
</el-table-column>
<el-table-column :label="$t('_prop.sale.saleorder.price')" width="120">
<template #default="{ row, $index }">
<el-form-item :prop="`${itemArrayName}.${$index}.price`">
<el-input-number
v-model="row.price"
size="small"
:min="0"
:precision="2"
@change="calculateTotalPrice(row)"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column :label="$t('_prop.sale.saleorder.totalPrice')" width="120">
<template #default="{ row }">
<span>{{ row.totalPrice?.toFixed(2) || "0.00" }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('_prop.sale.saleorder.saleMark')" width="150">
<template #default="{ row, $index }">
<el-form-item :prop="`${itemArrayName}.${$index}.saleMark`">
<el-input v-model="row.saleMark" placeholder="请输入备注" size="small" />
</el-form-item>
</template>
</el-table-column>
</template>
</BaseFormWithTable>
<BaseItemDialog
:title="$t('_title.sale.saleorder.showItem')"
v-model:visible="itemVisible"
:url="getDetailUrl"
parent-param-name="saleOrderId"
v-model:parent-param-value="itemParentId"
>
<template #columns>
<el-table-column :label="$t('_prop.sale.saleorder.partNumber')" prop="partNumber" width="150" />
<el-table-column :label="$t('_prop.sale.saleorder.productSpecs')" prop="productSpecs" width="200" />
<el-table-column :label="$t('_prop.sale.saleorder.saleCount')" prop="saleCount" />
<el-table-column :label="$t('_prop.sale.saleorder.price')" prop="price" />
<el-table-column :label="$t('_prop.sale.saleorder.totalPrice')" prop="totalPrice" />
<el-table-column :label="$t('_prop.sale.saleorder.saleMark')" prop="saleMark" />
</template>
</BaseItemDialog>
</template> </template>