| src/views/customerService/expiryAfterSales/components/formDia.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/customerService/expiryAfterSales/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/customerService/feedbackRegistration/components/formDia.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/equipmentManagement/operationManagement/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/inventoryManagement/dispatchLog/Record.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/inventoryManagement/receiptManagement/Record.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/inventoryManagement/stockReport/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/views/customerService/expiryAfterSales/components/formDia.vue
@@ -20,7 +20,7 @@ v-model="form.productName" placeholder="请输入产品名称" clearable :disabled="operationType === 'view'" :disabled="isFieldDisabled('productName')" /> </el-form-item> </el-col> @@ -30,7 +30,7 @@ v-model="form.batchNumber" placeholder="请输入产品批号" clearable :disabled="operationType === 'view'" :disabled="isFieldDisabled('batchNumber')" /> </el-form-item> </el-col> @@ -46,7 +46,7 @@ type="date" placeholder="请选择临期日期" clearable :disabled="operationType === 'view'" :disabled="isFieldDisabled('expiryDate')" /> </el-form-item> </el-col> @@ -57,7 +57,7 @@ :min="0" placeholder="请输入库存数量" style="width: 100%" :disabled="operationType === 'view'" :disabled="isFieldDisabled('stockQuantity')" /> </el-form-item> </el-col> @@ -69,7 +69,7 @@ v-model="form.customerName" placeholder="请输入客户名称" clearable :disabled="operationType === 'view'" :disabled="isFieldDisabled('customerName')" /> </el-form-item> </el-col> @@ -79,7 +79,7 @@ v-model="form.contactPhone" placeholder="请输入联系电话" clearable :disabled="operationType === 'view'" :disabled="isFieldDisabled('contactPhone')" /> </el-form-item> </el-col> @@ -91,7 +91,7 @@ v-model="form.problemDesc" placeholder="请输入问题描述" clearable :disabled="operationType === 'view'" :disabled="isFieldDisabled('problemDesc')" type="textarea" :rows="3" /> @@ -105,7 +105,7 @@ v-model="form.handlerId" placeholder="请选择处理人" clearable :disabled="operationType === 'view'" :disabled="isFieldDisabled('handlerId')" style="width: 100%" > <el-option @@ -127,7 +127,7 @@ type="date" placeholder="请选择处理日期" clearable :disabled="operationType === 'view'" :disabled="isFieldDisabled('handleDate')" /> </el-form-item> </el-col> @@ -139,7 +139,7 @@ v-model="form.handleResult" placeholder="请输入处理结果" clearable :disabled="operationType === 'view'" :disabled="isFieldDisabled('handleResult')" type="textarea" :rows="3" /> @@ -175,6 +175,8 @@ return '新增临期售后'; case 'edit': return '编辑临期售后'; case 'handle': return '处理临期售后'; case 'view': return '查看临期售后'; default: @@ -212,6 +214,13 @@ }) const { form, rules } = toRefs(data); const userList = ref([]) const handleEditableFields = ["handlerId", "handleDate", "handleResult"]; const isFieldDisabled = (field) => { if (operationType.value === "view") return true; if (operationType.value === "handle") return !handleEditableFields.includes(field); return false; }; // 打开弹框 const openDialog = (type, row) => { @@ -242,7 +251,7 @@ } else { // 编辑或查看时填充数据 form.value = { ...row }; if (type === 'edit' && !form.value.handlerId) { if (type === 'handle' && !form.value.handlerId) { form.value.handlerId = userStore.id; form.value.handleDate = getCurrentDate(); } @@ -250,8 +259,22 @@ } const submitForm = () => { if (operationType.value === "handle") { if (!form.value.handlerId || !form.value.handleDate || !form.value.handleResult) { proxy.$modal.msgWarning("请填写处理人、处理日期和处理结果"); return; } handleSubmit(); return; } proxy.$refs["formRef"].validate(valid => { if (valid) { handleSubmit(); } }); } const handleSubmit = () => { const submitData = { id: form.value.id, productName: form.value.productName, @@ -261,7 +284,7 @@ customerName: form.value.customerName, contactPhone: form.value.contactPhone, disRes: form.value.problemDesc, status: form.value.status, status: operationType.value === "handle" ? 2 : form.value.status, disposeUserId: form.value.handlerId, disposeNickName: userList.value.find(item => item.userId === form.value.handlerId)?.nickName, disposeResult: form.value.handleResult, @@ -270,13 +293,12 @@ const apiCall = operationType.value === 'add' ? expiryAfterSalesAdd : expiryAfterSalesUpdate; apiCall(submitData).then(() => { proxy.$modal.msgSuccess(operationType.value === 'add' ? "新增成功" : "更新成功"); const successText = operationType.value === "add" ? "新增成功" : operationType.value === "handle" ? "处理成功" : "更新成功"; proxy.$modal.msgSuccess(successText); closeDia(); }).catch(error => { console.error('提交数据失败:', error); proxy.$modal.msgError('提交数据失败,请稍后重试'); }); } }); } src/views/customerService/expiryAfterSales/index.vue
@@ -60,7 +60,7 @@ <template #operation="{ row }"> <el-button type="primary" link @click="openForm('view', row)">查看</el-button> <el-button type="primary" link @click="openForm('edit', row)" v-if="row.status === 1">编辑</el-button> <el-button type="primary" link @click="openForm('handle', row)" v-if="row.status === 1">处理</el-button> </template> </PIMTable> </div> src/views/customerService/feedbackRegistration/components/formDia.vue
@@ -1,89 +1,72 @@ <template> <div> <el-dialog v-model="dialogFormVisible" <el-dialog v-model="dialogFormVisible" title="新增售后单" width="90%" @close="closeDia" > @close="closeDia"> <div> <span class="descriptions">基础资料</span> <el-form :model="form" <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef" > ref="formRef"> <el-row :gutter="30"> <el-col :span="4"> <el-form-item label="客户名称:" prop="customerName"> <el-select v-model="form.customerName" <el-form-item label="客户名称:" prop="customerName"> <el-select v-model="form.customerName" filterable @change="customerNameChange" > <el-option v-for="item in customerNameOptions" @change="customerNameChange"> <el-option v-for="item in customerNameOptions" :key="item.value" :label="item.label" :value="item.value" /> :value="item.value" /> </el-select> </el-form-item> </el-col> <el-col :span="4"> <el-form-item label="售后类型:" prop="serviceType"> <el-select v-model="form.serviceType" filterable > <el-option v-for="dict in serviceTypeOptions" <el-form-item label="售后类型:" prop="serviceType"> <el-select v-model="form.serviceType" filterable> <el-option v-for="dict in serviceTypeOptions" :key="dict.value" :label="dict.label" :value="dict.value" /> :value="dict.value" /> </el-select> </el-form-item> </el-col> <el-col :span="4"> <el-form-item label="关联销售单号:" prop="salesContractNo"> <el-select v-model="form.salesContractNo" <el-form-item label="关联销售单号:" prop="salesContractNo"> <el-select v-model="form.salesContractNo" @change="associatedSalesOrderNumberChange" filterable > <el-option v-for="item in associatedSalesOrderNumberOptions" filterable> <el-option v-for="item in associatedSalesOrderNumberOptions" :key="item.value" :label="item.label" :value="item.value" /> :value="item.value" /> </el-select> </el-form-item> </el-col> <el-col :span="4"> <el-form-item label="紧急程度:" prop="urgency"> <el-select v-model="form.urgency" filterable > <el-option v-for="dict in urgencyOptions" <el-form-item label="紧急程度:" prop="urgency"> <el-select v-model="form.urgency" filterable> <el-option v-for="dict in urgencyOptions" :key="dict.value" :label="dict.label" :value="dict.value" /> :value="dict.value" /> </el-select> </el-form-item> </el-col> <el-col :span="4"> <el-form-item label="问题描述:" prop="proDesc"> <el-input v-model="form.proDesc" placeholder="请输入问题描述" /> <el-form-item label="问题描述:" prop="proDesc"> <el-input v-model="form.proDesc" placeholder="请输入问题描述" /> </el-form-item> </el-col> </el-row> @@ -92,27 +75,25 @@ <div style="padding-top: 20px"> <div style="display: flex; justify-content: space-between"> <span class="descriptions">关联产品</span> <el-button type="primary" <el-button type="primary" style="margin-right: 12px; margin-bottom: 10px" @click="isShowProductSelectDialog = true" > @click="isShowProductSelectDialog = true"> 选择产品 </el-button> </div> <PIMTable :isShowPagination="false" <PIMTable :isShowPagination="false" rowKey="id" :column="tableColumn" :tableData="tableData" > :tableData="tableData"> <template #approveStatus="{ row }"> <el-tag :type="getApproveStatusType(row)" size="small"> <el-tag :type="getApproveStatusType(row)" size="small"> {{ getApproveStatusText(row) }} </el-tag> </template> <template #shippingStatus="{ row }"> <el-tag :type="getShippingStatusType(row)" size="small"> <el-tag :type="getShippingStatusType(row)" size="small"> {{ getShippingStatusText(row) }} </el-tag> </template> @@ -121,18 +102,17 @@ </div> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitForm">确认</el-button> <el-button type="primary" @click="submitForm">确认</el-button> <el-button @click="closeDia">取消</el-button> </div> </template> </el-dialog> <!-- 选择产品弹窗 --> <ProductSelectDialog v-model="isShowProductSelectDialog" <ProductSelectDialog v-model="isShowProductSelectDialog" :products="currentSalesOrderProducts" :selected-ids="currentSelectedProductIds" @confirm="handleSelectProducts" /> @confirm="handleSelectProducts" /> </div> </template> @@ -141,14 +121,19 @@ import ProductSelectDialog from "./ProductSelectDialog.vue"; import useUserStore from "@/store/modules/user.js"; import {userListNoPageByTenantId} from "@/api/system/user.js"; import {afterSalesServiceAdd, afterSalesServiceUpdate, getAllCustomerList, getSalesLedger } from "@/api/customerService/index.js"; import { afterSalesServiceAdd, afterSalesServiceUpdate, getAllCustomerList, getSalesLedger, } from "@/api/customerService/index.js"; import { getCurrentDate } from "@/utils/index.js"; const { proxy } = getCurrentInstance() const emit = defineEmits(['close']) const { proxy } = getCurrentInstance(); const emit = defineEmits(["close"]); const dialogFormVisible = ref(false); const operationType = ref('') const formRef = ref(null) const customerNameOptions = ref([]) const operationType = ref(""); const formRef = ref(null); const customerNameOptions = ref([]); const userStore = useUserStore(); const data = reactive({ @@ -161,26 +146,30 @@ customerId: null, salesContractNo: "", proDesc: "", customerName: "" customerName: "", }, rules: { customerName: [{required: true, message: "请选择客户名称", trigger: "change"}], serviceType: [{required: true, message: "请选择售后类型", trigger: "change"}], customerName: [ { required: true, message: "请选择客户名称", trigger: "change" }, ], serviceType: [ { required: true, message: "请选择售后类型", trigger: "change" }, ], urgency: [{required: true, message: "请选择紧急程度", trigger: "change"}], feedbackDate: [{required: true, message: "请选择", trigger: "change"}], } }) }, }); // 自定义校验函数:判断是否需要校验售后编号 const { form, rules } = toRefs(data); const userList = ref([]) const userList = ref([]); const formatCurrency = (val) => { if (val === null || val === undefined || val === '') return '-' const num = Number(val) return Number.isFinite(num) ? num.toFixed(2) : '-' } const formatCurrency = val => { if (val === null || val === undefined || val === "") return "-"; const num = Number(val); return Number.isFinite(num) ? num.toFixed(2) : "-"; }; const { post_sale_waiting_list, degree_of_urgency } = proxy.useDict( "post_sale_waiting_list", @@ -190,31 +179,38 @@ const serviceTypeOptions = computed(() => post_sale_waiting_list?.value || []); const urgencyOptions = computed(() => degree_of_urgency?.value || []); const getProductRowId = (row) => { return row?.id ?? row?.productModelId ?? row?.modelId ?? `${row?.productCategory || row?.productName || ""}-${row?.specificationModel || row?.model || ""}-${row?.unit || ""}` } const getProductRowId = row => { return ( row?.id ?? row?.productModelId ?? row?.modelId ?? `${row?.productCategory || row?.productName || ""}-${ row?.specificationModel || row?.model || "" }-${row?.unit || ""}` ); }; const normalizeProductRow = (row) => { const normalizeProductRow = row => { return { ...row, id: getProductRowId(row), productCategory: row?.productCategory ?? row?.productName ?? '', specificationModel: row?.specificationModel ?? row?.model ?? '', unit: row?.unit ?? '', productCategory: row?.productCategory ?? row?.productName ?? "", specificationModel: row?.specificationModel ?? row?.model ?? "", unit: row?.unit ?? "", approveStatus: row?.approveStatus ?? null, shippingStatus: row?.shippingStatus ?? '', expressCompany: row?.expressCompany ?? '', expressNumber: row?.expressNumber ?? '', shippingCarNumber: row?.shippingCarNumber ?? '', shippingDate: row?.shippingDate ?? '', shippingStatus: row?.shippingStatus ?? "", expressCompany: row?.expressCompany ?? "", expressNumber: row?.expressNumber ?? "", shippingCarNumber: row?.shippingCarNumber ?? "", shippingDate: row?.shippingDate ?? "", quantity: row?.quantity ?? 0, taxRate: row?.taxRate ?? 0, taxInclusiveUnitPrice: row?.taxInclusiveUnitPrice ?? 0, taxInclusiveTotalPrice: row?.taxInclusiveTotalPrice ?? 0, taxExclusiveTotalPrice: row?.taxExclusiveTotalPrice ?? 0, noQuantity: row?.noQuantity ?? 0, } } }; }; const tableColumn = ref([ { label: "产品大类", prop: "productCategory" }, @@ -237,7 +233,12 @@ }, { label: "快递公司", prop: "expressCompany", width: 140 }, { label: "快递单号", prop: "expressNumber", width: 160 }, { label: "发货车牌", prop: "shippingCarNumber", minWidth: 100, align: "center" }, { label: "发货车牌", prop: "shippingCarNumber", minWidth: 100, align: "center", }, { label: "发货日期", prop: "shippingDate", minWidth: 100, align: "center" }, { label: "数量", prop: "quantity", width: 100 }, { label: "税率(%)", prop: "taxRate", width: 100 }, @@ -263,52 +264,57 @@ dataType: "action", label: "操作", align: "center", fixed: 'right', fixed: "right", operation: [ { name: "删除", type: "text", clickFun: (row) => { tableData.value = tableData.value.filter(i => getProductRowId(i) !== getProductRowId(row)) clickFun: row => { tableData.value = tableData.value.filter( i => getProductRowId(i) !== getProductRowId(row) ); }, }, ], }, ]) const tableData = ref([]) ]); const tableData = ref([]); // 选择产品弹窗 const isShowProductSelectDialog = ref(false) const handleSelectProducts = (rows) => { if (!Array.isArray(rows)) return const existingIds = new Set(tableData.value.map(i => String(getProductRowId(i)))) const isShowProductSelectDialog = ref(false); const handleSelectProducts = rows => { if (!Array.isArray(rows)) return; const existingIds = new Set( tableData.value.map(i => String(getProductRowId(i))) ); const mapped = rows .map(normalizeProductRow) .filter(r => !existingIds.has(String(getProductRowId(r)))) tableData.value = tableData.value.concat(mapped) } .filter(r => !existingIds.has(String(getProductRowId(r)))); tableData.value = tableData.value.concat(mapped); }; const currentSelectedProductIds = computed(() => { return tableData.value.map(item => getProductRowId(item)).filter(item => item !== undefined && item !== null && item !== '') }) return tableData.value .map(item => getProductRowId(item)) .filter(item => item !== undefined && item !== null && item !== ""); }); const associatedSalesOrderNumberChange = () => { const opt = associatedSalesOrderNumberOptions.value.find( (item) => item.value === form.value.salesContractNo ) tableData.value = (opt?.productData || []).map(normalizeProductRow) form.value.salesLedgerId = opt?.id || null } item => item.value === form.value.salesContractNo ); tableData.value = (opt?.productData || []).map(normalizeProductRow); form.value.salesLedgerId = opt?.id || null; }; const associatedSalesOrderNumberOptions = ref([]) const associatedSalesOrderNumberOptions = ref([]); const currentSalesOrderProducts = computed(() => { const opt = associatedSalesOrderNumberOptions.value.find( (item) => item.value === form.value.salesContractNo ) return (opt?.productData || []).map(normalizeProductRow) }) item => item.value === form.value.salesContractNo ); return (opt?.productData || []).map(normalizeProductRow); }); const customerNameChange = (val) => { const customerNameChange = val => { form.value.salesContractNo = ""; form.value.salesLedgerId = null; tableData.value = []; @@ -320,74 +326,77 @@ form.value.customerId = null; } getSalesLedger({ customerName: form.value.customerName customerName: form.value.customerName, }).then(res => { if(res.code === 200){ associatedSalesOrderNumberOptions.value = res.data.records.map(item => ({ label: item.salesContractNo, value: item.salesContractNo, productData:item.productData, id: item.id })) id: item.id, })); } }) } }); }; const getApproveStatusText = (row) => { if (!row) return '不足' if (row.approveStatus === 1 && (!row.shippingDate || !row.shippingCarNumber)) { return '充足' const getApproveStatusText = row => { if (!row) return "不足"; if ( row.approveStatus === 1 && (!row.shippingDate || !row.shippingCarNumber) ) { return "充足"; } if (row.approveStatus === 0 && (row.shippingDate || row.shippingCarNumber)) { return '已出库' return "已出库"; } return '不足' } return "不足"; }; const getApproveStatusType = (row) => { const statusText = getApproveStatusText(row) return statusText === '不足' ? 'danger' : 'success' } const getApproveStatusType = row => { const statusText = getApproveStatusText(row); return statusText === "不足" ? "danger" : "success"; }; const getShippingStatusText = (row) => { if (!row) return '待发货' const getShippingStatusText = row => { if (!row) return "待发货"; if (row.shippingDate || row.shippingCarNumber) { return '已发货' return "已发货"; } const status = row.shippingStatus if (status === null || status === undefined || status === '') { return '待发货' const status = row.shippingStatus; if (status === null || status === undefined || status === "") { return "待发货"; } const map = { '待发货': '待发货', '待审核': '待审核', '审核中': '审核中', '审核拒绝': '审核拒绝', '审核通过': '审核通过', '已发货': '已发货' } return map[String(status).trim()] || '待发货' } 待发货: "待发货", 待审核: "待审核", 审核中: "审核中", 审核拒绝: "审核拒绝", 审核通过: "审核通过", 已发货: "已发货", }; return map[String(status).trim()] || "待发货"; }; const getShippingStatusType = (row) => { if (!row) return 'info' const getShippingStatusType = row => { if (!row) return "info"; if (row.shippingDate || row.shippingCarNumber) { return 'success' return "success"; } const status = row.shippingStatus if (status === null || status === undefined || status === '') { return 'info' const status = row.shippingStatus; if (status === null || status === undefined || status === "") { return "info"; } const map = { '待发货': 'info', '待审核': 'warning', '审核中': 'warning', '审核拒绝': 'danger', '审核通过': 'success', '已发货': 'success' } return map[String(status).trim()] || 'info' } 待发货: "info", 待审核: "warning", 审核中: "warning", 审核拒绝: "danger", 审核通过: "success", 已发货: "success", }; return map[String(status).trim()] || "info"; }; // 打开弹框 const openDialog =async (type, row) => { @@ -397,69 +406,77 @@ size: 1000, total: 0, }); if(res.records){ customerNameOptions.value = res.records.map(item => ({ console.log(res, "res"); if (res.data.records) { customerNameOptions.value = res.data.records.map(item => ({ label: item.customerName, value: item.customerName, id: item.id id: item.id, })); } else { } operationType.value = type; dialogFormVisible.value = true; form.value = {} form.value = {}; proxy.resetForm("formRef"); form.value.checkUserId = userStore.id; form.value.feedbackDate = getCurrentDate(); // 新增时清空已选关联产品 if (type === "add") { tableData.value = [] tableData.value = []; } userListNoPageByTenantId().then((res) => { userListNoPageByTenantId().then(res => { userList.value = res.data; }); if (type === "edit") { form.value = {...row} form.value = { ...row }; if (form.value.customerName) { const res = await getSalesLedger({ customerName: form.value.customerName }) const res = await getSalesLedger({ customerName: form.value.customerName, }); if (res?.code === 200) { console.log(res) associatedSalesOrderNumberOptions.value = (res.data?.records || []).map(item => ({ console.log(res); associatedSalesOrderNumberOptions.value = (res.data?.records || []).map( item => ({ label: item.salesContractNo, value: item.salesContractNo, productData: item.productData, id: item.id })) id: item.id, }) ); } } console.log(form.value) console.log(form.value); } } }; const submitForm = () => { proxy.$refs["formRef"].validate(valid => { if (valid) { // 匹配产品型号IDs form.value.productModelIds = tableData.value.map(item => item.id).join(",") form.value.productModelIds = tableData.value .map(item => item.id) .join(","); if (operationType.value === "add") { afterSalesServiceAdd(form.value).then(response => { proxy.$modal.msgSuccess("新增成功") closeDia() }) proxy.$modal.msgSuccess("新增成功"); closeDia(); }); } else { afterSalesServiceUpdate(form.value).then(response => { proxy.$modal.msgSuccess("修改成功") closeDia() }) proxy.$modal.msgSuccess("修改成功"); closeDia(); }); } } }) } }); }; // 关闭弹框 const closeDia = () => { proxy.resetForm("formRef"); dialogFormVisible.value = false; emit('close') emit("close"); }; defineExpose({ openDialog, @@ -484,7 +501,7 @@ transform: translateY(-50%); width: 4px; height: 1rem; background-color: #002FA7; /* Element 默认红色 */ background-color: #002fa7; /* Element 默认红色 */ border-radius: 2px; } </style> src/views/equipmentManagement/operationManagement/index.vue
@@ -104,7 +104,7 @@ align="center" > <template #default="scope"> {{ scope.row.runtimeDuration || '-' }} {{ getRuntimeDurationDisplay(scope.row) }} </template> </el-table-column> <el-table-column @@ -154,7 +154,8 @@ </template> <script setup> import { ref, onMounted, computed } from 'vue' import { ref, onMounted, onUnmounted, computed } from 'vue' import dayjs from 'dayjs' import { ElMessage } from 'element-plus' import { VideoPlay, @@ -193,6 +194,98 @@ return filtered }) // 运行中无结束时间时,运行时长需随当前时间变化,用 tick 触发模板重算 const runtimeDisplayTick = ref(0) /** 取后端可能使用的开始/结束时间字段 */ const pickStartTime = (row) => row?.startRuntimeTime ?? row?.startTime ?? row?.start_time const pickEndTime = (row) => row?.endRuntimeTime ?? row?.endTime ?? row?.end_time /** * 解析接口/前端写入的各类时间:时间戳、ISO 字符串、yyyy-MM-dd HH:mm:ss、Jackson 数组 [y,M,d,h,m,s]、含中文的 toLocaleString 等 */ const parseDeviceTime = (input) => { if (input === null || input === undefined || input === '') return null if (typeof input === 'number' && !Number.isNaN(input)) { const d = dayjs(input) return d.isValid() ? d.toDate() : null } if (Array.isArray(input)) { const [y, mo, day, h = 0, mi = 0, se = 0] = input if (y == null || y === '') return null const d = dayjs() .year(Number(y)) .month(Number(mo || 1) - 1) .date(Number(day || 1)) .hour(Number(h) || 0) .minute(Number(mi) || 0) .second(Number(se) || 0) return d.isValid() ? d.toDate() : null } const s = String(input).trim() if (!s || s === '-') return null let d = dayjs(s) if (d.isValid()) return d.toDate() d = dayjs(s.replace(/-/g, '/')) if (d.isValid()) return d.toDate() d = dayjs(s.replace(/\//g, '-')) if (d.isValid()) return d.toDate() return null } const formatDurationMs = (durationMs) => { if (durationMs == null || Number.isNaN(durationMs) || durationMs < 0) return '-' const hours = Math.floor(durationMs / (1000 * 60 * 60)) const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)) if (hours === 0 && minutes === 0) return '不足1分钟' return `${hours}小时${minutes}分钟` } const hasMeaningfulEnd = (endRaw) => endRaw !== null && endRaw !== undefined && String(endRaw).trim() !== '' && String(endRaw).trim() !== '-' const formatStoredDuration = (row) => { const rd = row?.runtimeDuration if (rd === null || rd === undefined) return '' const t = String(rd).trim() return t === '' || t === '-' ? '' : String(rd) } /** 运行中:始终用「当前时间 - 开始时间」;已停止:优先接口 runtimeDuration,否则用结束-开始;无结束可看已存时长或动态推算 */ const getRuntimeDurationDisplay = (row) => { void runtimeDisplayTick.value const start = parseDeviceTime(pickStartTime(row)) if (!start) { return formatStoredDuration(row) || '-' } const statusStr = String(row?.status ?? '').trim() const isRunning = statusStr === '运行中' || statusStr === '1' const endRaw = pickEndTime(row) const hasEnd = hasMeaningfulEnd(endRaw) // 无结束时间:运行中一定动态算;已停止则优先展示后端已存时长,没有再按当前时间推算 if (!hasEnd) { if (isRunning) return formatDurationMs(Date.now() - start.getTime()) const stored = formatStoredDuration(row) if (stored) return stored return formatDurationMs(Date.now() - start.getTime()) } if (isRunning) { return formatDurationMs(Date.now() - start.getTime()) } const end = parseDeviceTime(endRaw) const stored = formatStoredDuration(row) if (stored) return stored if (end) return formatDurationMs(end.getTime() - start.getTime()) return '-' } // 检查设备是否超时未启动 const isOverdue = (device) => { @@ -246,12 +339,11 @@ device.endRuntimeTime = currentTime // 计算运行时长 if (device.startRuntimeTime) { const startTime = new Date(device.startRuntimeTime) const endTime = new Date(currentTime) const duration = endTime - startTime const hours = Math.floor(duration / (1000 * 60 * 60)) const minutes = Math.floor((duration % (1000 * 60 * 60)) / (1000 * 60)) device.runtimeDuration = `${hours}小时${minutes}分钟` const startTime = parseDeviceTime(device.startRuntimeTime) const endTime = parseDeviceTime(currentTime) if (startTime && endTime) { device.runtimeDuration = formatDurationMs(endTime.getTime() - startTime.getTime()) } } } const params = { @@ -297,9 +389,31 @@ // 组件挂载时初始化数据 const POLL_MS = 60 * 1000 const RUNTIME_TICK_MS = 30 * 1000 let listPollTimer = null let runtimeTickTimer = null // 组件挂载时拉取数据,并每分钟刷新一次列表;运行中时长每 30 秒刷新显示 onMounted(() => { getList() listPollTimer = setInterval(() => { getList() }, POLL_MS) runtimeTickTimer = setInterval(() => { runtimeDisplayTick.value++ }, RUNTIME_TICK_MS) }) onUnmounted(() => { if (listPollTimer != null) { clearInterval(listPollTimer) listPollTimer = null } if (runtimeTickTimer != null) { clearInterval(runtimeTickTimer) runtimeTickTimer = null } }) </script> src/views/inventoryManagement/dispatchLog/Record.vue
@@ -96,7 +96,9 @@ </el-table-column> <el-table-column label="审批状态" prop="approvalStatus" show-overflow-tooltip> <template #default="scope"> <el-tag :type="getApprovalStatusTagType(scope.row.approvalStatus)" size="small"> {{ getApprovalStatusLabel(scope.row.approvalStatus) }} </el-tag> </template> </el-table-column> </el-table> @@ -216,6 +218,13 @@ return approvalStatusLabelMap[status] || "待审批"; }; // 通过/驳回固定色;其余(含待审批、空值、未映射但文案为待审批)统一用 warning 预警色 const getApprovalStatusTagType = (status) => { if (status === 1 || status === "1" || status === "approved" || status === "APPROVED") return "success"; if (status === 2 || status === "2" || status === "rejected" || status === "REJECTED") return "danger"; return "warning"; }; // 获取来源类型选项 const fetchStockRecordTypeOptions = () => { if (props.type === '0') { src/views/inventoryManagement/receiptManagement/Record.vue
@@ -91,7 +91,9 @@ prop="approvalStatus" show-overflow-tooltip> <template #default="scope"> <el-tag :type="getApprovalStatusTagType(scope.row.approvalStatus)" size="small"> {{ getApprovalStatusLabel(scope.row.approvalStatus) }} </el-tag> </template> </el-table-column> </el-table> @@ -187,6 +189,13 @@ return approvalStatusLabelMap[status] || "待审批"; }; // 通过/驳回固定色;其余(含待审批、空值、未映射但文案为待审批)统一用 warning 预警色 const getApprovalStatusTagType = (status) => { if (status === 1 || status === "1" || status === "approved" || status === "APPROVED") return "success"; if (status === 2 || status === "2" || status === "rejected" || status === "REJECTED") return "danger"; return "warning"; }; const pageProductChange = obj => { page.current = obj.page; page.size = obj.limit; src/views/inventoryManagement/stockReport/index.vue
@@ -4,29 +4,26 @@ <div class="search_form"> <div class="search_left"> <span class="search_title">报表类型:</span> <el-select v-model="searchForm.reportType" <el-select v-model="searchForm.reportType" style="width: 150px;" placeholder="请选择" @change="handleReportTypeChange" > <el-option label="日报" value="daily" /> <el-option label="月报" value="monthly" /> <el-option label="进出存报表" value="inout" /> @change="handleReportTypeChange"> <el-option label="日报" value="daily" /> <el-option label="月报" value="monthly" /> <el-option label="进出存报表" value="inout" /> </el-select> <span class="search_title ml10">时间范围:</span> <el-date-picker v-if="searchForm.reportType === 'daily'" <el-date-picker v-if="searchForm.reportType === 'daily'" v-model="searchForm.singleDate" type="date" placeholder="请选择日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" style="width: 200px;" /> <el-date-picker v-else-if="searchForm.reportType === 'monthly'" style="width: 200px;" /> <el-date-picker v-else-if="searchForm.reportType === 'monthly'" v-model="searchForm.monthRange" type="monthrange" range-separator="至" @@ -34,10 +31,8 @@ end-placeholder="结束月份" format="YYYY-MM-DD" value-format="YYYY-MM-DD" style="width: 240px;" /> <el-date-picker v-else style="width: 240px;" /> <el-date-picker v-else v-model="searchForm.dateRange" type="daterange" range-separator="至" @@ -45,22 +40,20 @@ end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" style="width: 240px;" /> <el-button type="primary" @click="onSearch" style="margin-left: 10px"> style="width: 240px;" /> <el-button type="primary" @click="onSearch" style="margin-left: 10px"> 查询 </el-button> <el-button @click="handleReset">重置</el-button> </div> <div class="search_right"> <!-- <el-button type="success" @click="handleExport" icon="Download">--> <!-- 导出报表--> <!-- </el-button>--> </div> </div> <!-- <!– 统计卡片 –>--> <!-- <div class="stats_cards" v-if="reportData.summary">--> <!-- <el-row :gutter="20">--> @@ -118,7 +111,6 @@ <!-- </el-col>--> <!-- </el-row>--> <!-- </div>--> <!-- <!– 图表区域 –>--> <!-- <div class="chart_section" v-if="reportData.chartData">--> <!-- <el-row :gutter="20">--> @@ -140,80 +132,62 @@ <!-- </el-col>--> <!-- </el-row>--> <!-- </div>--> <!-- 详细数据表格 --> <div class="table_section"> <el-card> <template #header> <span>{{ getTableTitle() }}</span> </template> <el-table v-loading="tableLoading" <el-table v-loading="tableLoading" :data="reportData.tableData" border height="400" style="width: 100%" :header-cell-style="{ background: '#F0F1F5', color: '#333333' }" > <el-table-column align="center" :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"> <el-table-column align="center" label="序号" type="index" width="60" /> <el-table-column label="入库时间" width="60" /> <el-table-column label="入库时间" prop="createTime" width="200" show-overflow-tooltip v-if="searchForm.reportType !== 'inout'" /> <el-table-column label="入库批次" v-if="searchForm.reportType !== 'inout'" /> <el-table-column label="入库批次" prop="inboundBatches" width="240" width="180" show-overflow-tooltip v-if="searchForm.reportType !== 'inout'" /> <el-table-column label="产品大类" v-if="searchForm.reportType !== 'inout'" /> <el-table-column label="批号" prop="batchNo" width="180" show-overflow-tooltip v-if="searchForm.reportType !== 'inout'" /> <el-table-column label="产品大类" prop="productName" show-overflow-tooltip /> <el-table-column label="规格型号" show-overflow-tooltip /> <el-table-column label="规格型号" prop="model" show-overflow-tooltip /> <el-table-column label="单位" show-overflow-tooltip /> <el-table-column label="单位" prop="unit" show-overflow-tooltip /> <el-table-column label="入库数量" show-overflow-tooltip /> <el-table-column label="入库数量" prop="totalStockIn" align="center" v-if="searchForm.reportType === 'inout'" /> <el-table-column label="入库数量" v-if="searchForm.reportType === 'inout'" /> <el-table-column label="入库数量" prop="stockInNum" align="center" v-else /> <el-table-column label="出库数量" v-else /> <el-table-column label="出库数量" prop="totalStockOut" width="100" align="center" v-if="searchForm.reportType === 'inout'" /> <el-table-column label="现在库存" v-if="searchForm.reportType === 'inout'" /> <el-table-column label="现在库存" prop="currentStock" align="center" /> align="center" /> <el-table-column label="来源" prop="recordType" v-if="searchForm.reportType !== 'inout'" @@ -222,154 +196,153 @@ {{ getRecordType(scope.row.recordType) }} </template> </el-table-column> <el-table-column label="入库人" <el-table-column label="入库人" prop="createBy" width="80" v-if="searchForm.reportType !== 'inout'" show-overflow-tooltip /> show-overflow-tooltip /> </el-table> <pagination :total="total" <pagination :total="total" layout="total, sizes, prev, pager, next, jumper" :page="page.current" :limit="page.size" @pagination="paginationChange" /> @pagination="paginationChange" /> </el-card> </div> </div> </template> <script setup> import { ref, reactive, onMounted, nextTick, getCurrentInstance } from 'vue' import { ElMessage } from 'element-plus' import * as echarts from 'echarts' import pagination from '@/components/PIMTable/Pagination.vue' import { ref, reactive, onMounted, nextTick, getCurrentInstance } from "vue"; import { ElMessage } from "element-plus"; import * as echarts from "echarts"; import pagination from "@/components/PIMTable/Pagination.vue"; import { getStockInventoryInAndOutReportList, getStockInventoryReportList getStockInventoryReportList, } from "@/api/inventoryManagement/stockInventory.js"; import { findAllQualifiedStockInRecordTypeOptions,findAllUnQualifiedStockInRecordTypeOptions, findAllQualifiedStockInRecordTypeOptions, findAllUnQualifiedStockInRecordTypeOptions, } from "@/api/basicData/enum.js"; const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance(); // 响应式数据 const tableLoading = ref(false) const trendChart = ref(null) const comparisonChart = ref(null) const tableLoading = ref(false); const trendChart = ref(null); const comparisonChart = ref(null); const searchForm = reactive({ reportType: 'daily', singleDate: '', reportType: "daily", singleDate: "", dateRange: [], monthRange: [] }) monthRange: [], }); const reportData = ref({ summary: null, chartData: null, tableData: [] }) tableData: [], }); const page = reactive({ current: 1, size: 10, }) }); const total = ref(0) const total = ref(0); const stockRecordTypeOptions = ref([]) const stockRecordTypeOptions = ref([]); const getRecordType = (recordType) => { return stockRecordTypeOptions.value.find(item => item.value === recordType)?.label || '' } const getRecordType = recordType => { return ( stockRecordTypeOptions.value.find(item => item.value === recordType) ?.label || "" ); }; // 获取来源类型选项 const fetchStockRecordTypeOptions = () => { findAllQualifiedStockInRecordTypeOptions() .then(res => { findAllQualifiedStockInRecordTypeOptions().then(res => { stockRecordTypeOptions.value = res.data; findAllUnQualifiedStockInRecordTypeOptions() .then(res => { stockRecordTypeOptions.value = [...stockRecordTypeOptions.value,...res.data]; }) }) } findAllUnQualifiedStockInRecordTypeOptions().then(res => { stockRecordTypeOptions.value = [ ...stockRecordTypeOptions.value, ...res.data, ]; }); }); }; // 获取表格标题 const getTableTitle = () => { const typeMap = { daily: '日报详细数据', monthly: '月报详细数据', inout: '进出存报表详细数据' } return typeMap[searchForm.reportType] || '报表详细数据' } daily: "日报详细数据", monthly: "月报详细数据", inout: "进出存报表详细数据", }; return typeMap[searchForm.reportType] || "报表详细数据"; }; // 报表类型改变 const handleReportTypeChange = () => { page.current = 1 page.current = 1; reportData.value = { summary: null, chartData: null, tableData: [] } } tableData: [], }; }; // 查询数据 const handleQuery = async () => { if (!validateSearchForm()) { return return; } tableLoading.value = true tableLoading.value = true; try { const baseParams = getQueryParams() const baseParams = getQueryParams(); const params = { ...baseParams, current: page.current, size: page.size, } let response }; let response; if (searchForm.reportType === 'inout') { response = await getStockInventoryInAndOutReportList(params) if (searchForm.reportType === "inout") { response = await getStockInventoryInAndOutReportList(params); } else { response = await getStockInventoryReportList(params) response = await getStockInventoryReportList(params); } if (response.code === 200) { reportData.value.tableData = response.data.records || [] total.value = response.data.total || 0 reportData.value.tableData = response.data.records || []; total.value = response.data.total || 0; // reportData.value.summary = response.data.summary // reportData.value.chartData = response.data.chartData // nextTick(() => { // initCharts() // }) } } catch (error) { ElMessage.error('查询失败:' + error.message) ElMessage.error("查询失败:" + error.message); } finally { tableLoading.value = false tableLoading.value = false; } } }; // 查询按钮:重置到第一页并查询 const onSearch = () => { page.current = 1 handleQuery() } page.current = 1; handleQuery(); }; // 分页变化 const paginationChange = (obj) => { page.current = obj.page page.size = obj.limit handleQuery() } const paginationChange = obj => { page.current = obj.page; page.size = obj.limit; handleQuery(); }; // // 生成假数据 // const generateMockData = () => { // // 生成统计卡片假数据 @@ -403,24 +376,24 @@ // } // 验证搜索表单 const validateSearchForm = () => { if (searchForm.reportType === 'daily') { if (searchForm.reportType === "daily") { if (!searchForm.singleDate) { ElMessage.warning('请选择日期') return false ElMessage.warning("请选择日期"); return false; } } else if (searchForm.reportType === 'inout') { } else if (searchForm.reportType === "inout") { if (!searchForm.dateRange || searchForm.dateRange.length !== 2) { ElMessage.warning('请选择日期范围') return false ElMessage.warning("请选择日期范围"); return false; } } else if (searchForm.reportType === 'monthly') { } else if (searchForm.reportType === "monthly") { if (!searchForm.monthRange || searchForm.monthRange.length !== 2) { ElMessage.warning('请选择月份范围') return false ElMessage.warning("请选择月份范围"); return false; } } return true } return true; }; // 获取查询参数 const getQueryParams = () => { @@ -430,45 +403,45 @@ startMonth: "", endMonth: "", startDate: "", endDate: "" } endDate: "", }; if (searchForm.reportType === 'daily') { params.reportDate = searchForm.singleDate } else if (searchForm.reportType === 'monthly') { params.startMonth = searchForm.monthRange[0] params.endMonth = searchForm.monthRange[1] if (searchForm.reportType === "daily") { params.reportDate = searchForm.singleDate; } else if (searchForm.reportType === "monthly") { params.startMonth = searchForm.monthRange[0]; params.endMonth = searchForm.monthRange[1]; } else { params.startDate = searchForm.dateRange[0] params.endDate = searchForm.dateRange[1] params.startDate = searchForm.dateRange[0]; params.endDate = searchForm.dateRange[1]; } return params } return params; }; // 重置搜索 const handleReset = () => { searchForm.reportType = 'daily' searchForm.singleDate = '' searchForm.dateRange = [] searchForm.monthRange = [] searchForm.reportType = "daily"; searchForm.singleDate = ""; searchForm.dateRange = []; searchForm.monthRange = []; reportData.value = { summary: null, chartData: null, tableData: [] } } tableData: [], }; }; // 导出报表 const handleExport = async () => { if (!validateSearchForm()) { return return; } try { const params = getQueryParams() const params = getQueryParams(); // const response = await exportStockReport(params) proxy.download("/stockin/exportCopy", params, '库存报表.xlsx') proxy.download("/stockin/exportCopy", params, "库存报表.xlsx"); // 创建下载链接 // const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) // const url = window.URL.createObjectURL(blob) @@ -482,116 +455,118 @@ // ElMessage.success('导出成功') } catch (error) { ElMessage.error('导出失败:' + error.message) ElMessage.error("导出失败:" + error.message); } } }; // 初始化图表 const initCharts = () => { if (!reportData.value.chartData) return if (!reportData.value.chartData) return; initTrendChart() initComparisonChart() } initTrendChart(); initComparisonChart(); }; // 初始化趋势图 const initTrendChart = () => { if (!trendChart.value) return if (!trendChart.value) return; const chart = echarts.init(trendChart.value) const chart = echarts.init(trendChart.value); const option = { title: { text: '库存变化趋势', left: 'center' text: "库存变化趋势", left: "center", }, tooltip: { trigger: 'axis' trigger: "axis", }, legend: { data: ['库存量'], top: 30 data: ["库存量"], top: 30, }, xAxis: { type: 'category', data: reportData.value.chartData.trendDates || [] type: "category", data: reportData.value.chartData.trendDates || [], }, yAxis: { type: 'value' }, series: [{ name: '库存量', type: 'line', data: reportData.value.chartData.trendValues || [], smooth: true, itemStyle: { color: '#409EFF' } }] } chart.setOption(option) } // 初始化对比图 const initComparisonChart = () => { if (!comparisonChart.value) return const chart = echarts.init(comparisonChart.value) const option = { title: { text: '进出库对比', left: 'center' }, tooltip: { trigger: 'axis' }, legend: { data: ['入库', '出库'], top: 30 }, xAxis: { type: 'category', data: reportData.value.chartData.comparisonDates || [] }, yAxis: { type: 'value' type: "value", }, series: [ { name: '入库', type: 'bar', name: "库存量", type: "line", data: reportData.value.chartData.trendValues || [], smooth: true, itemStyle: { color: "#409EFF", }, }, ], }; chart.setOption(option); }; // 初始化对比图 const initComparisonChart = () => { if (!comparisonChart.value) return; const chart = echarts.init(comparisonChart.value); const option = { title: { text: "进出库对比", left: "center", }, tooltip: { trigger: "axis", }, legend: { data: ["入库", "出库"], top: 30, }, xAxis: { type: "category", data: reportData.value.chartData.comparisonDates || [], }, yAxis: { type: "value", }, series: [ { name: "入库", type: "bar", data: reportData.value.chartData.inValues || [], itemStyle: { color: '#67C23A' } color: "#67C23A", }, }, { name: '出库', type: 'bar', name: "出库", type: "bar", data: reportData.value.chartData.outValues || [], itemStyle: { color: '#F56C6C' } } ] } chart.setOption(option) } color: "#F56C6C", }, }, ], }; chart.setOption(option); }; // 组件挂载时设置默认时间 onMounted(() => { const today = new Date() searchForm.singleDate = today.toISOString().split('T')[0] const today = new Date(); searchForm.singleDate = today.toISOString().split("T")[0]; const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000) const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000); searchForm.dateRange = [ yesterday.toISOString().split('T')[0], today.toISOString().split('T')[0] ] yesterday.toISOString().split("T")[0], today.toISOString().split("T")[0], ]; fetchStockRecordTypeOptions() fetchStockRecordTypeOptions(); // 初始化加载一次数据 handleQuery() }) handleQuery(); }); </script> <style scoped> @@ -653,19 +628,19 @@ } .stats_icon.in { background: linear-gradient(135deg, #67C23A, #85CE61); background: linear-gradient(135deg, #67c23a, #85ce61); } .stats_icon.out { background: linear-gradient(135deg, #F56C6C, #F78989); background: linear-gradient(135deg, #f56c6c, #f78989); } .stats_icon.stock { background: linear-gradient(135deg, #409EFF, #66B1FF); background: linear-gradient(135deg, #409eff, #66b1ff); } .stats_icon.turnover { background: linear-gradient(135deg, #E6A23C, #EEBE77); background: linear-gradient(135deg, #e6a23c, #eebe77); } .stats_info { @@ -700,7 +675,7 @@ } :deep(.el-table .el-table__header-wrapper th) { background-color: #F0F1F5 !important; background-color: #f0f1f5 !important; color: #333333; font-weight: 600; }