Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_NEW_pro
| | |
| | | v-model="form.productName" |
| | | placeholder="请输入产品名称" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('productName')" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | v-model="form.batchNumber" |
| | | placeholder="请输入产品批号" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('batchNumber')" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | type="date" |
| | | placeholder="请选择临期日期" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('expiryDate')" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | :min="0" |
| | | placeholder="请输入库存数量" |
| | | style="width: 100%" |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('stockQuantity')" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | v-model="form.customerName" |
| | | placeholder="请输入客户名称" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('customerName')" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | v-model="form.contactPhone" |
| | | placeholder="请输入联系电话" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('contactPhone')" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | v-model="form.problemDesc" |
| | | placeholder="请输入问题描述" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('problemDesc')" |
| | | type="textarea" |
| | | :rows="3" |
| | | /> |
| | |
| | | v-model="form.handlerId" |
| | | placeholder="请选择处理人" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('handlerId')" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | |
| | | type="date" |
| | | placeholder="请选择处理日期" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('handleDate')" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | v-model="form.handleResult" |
| | | placeholder="请输入处理结果" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | :disabled="isFieldDisabled('handleResult')" |
| | | type="textarea" |
| | | :rows="3" |
| | | /> |
| | |
| | | return '新增临期售后'; |
| | | case 'edit': |
| | | return '编辑临期售后'; |
| | | case 'handle': |
| | | return '处理临期售后'; |
| | | case 'view': |
| | | return '查看临期售后'; |
| | | default: |
| | |
| | | }) |
| | | 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) => { |
| | |
| | | } 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(); |
| | | } |
| | |
| | | } |
| | | |
| | | 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) { |
| | | const submitData = { |
| | | id: form.value.id, |
| | | productName: form.value.productName, |
| | | batchNumber: form.value.batchNumber, |
| | | expireDate: form.value.expiryDate, |
| | | stockQuantity: form.value.stockQuantity, |
| | | customerName: form.value.customerName, |
| | | contactPhone: form.value.contactPhone, |
| | | disRes: form.value.problemDesc, |
| | | status: form.value.status, |
| | | disposeUserId: form.value.handlerId, |
| | | disposeNickName: userList.value.find(item => item.userId === form.value.handlerId)?.nickName, |
| | | disposeResult: form.value.handleResult, |
| | | disDate: form.value.handleDate |
| | | }; |
| | | |
| | | const apiCall = operationType.value === 'add' ? expiryAfterSalesAdd : expiryAfterSalesUpdate; |
| | | apiCall(submitData).then(() => { |
| | | proxy.$modal.msgSuccess(operationType.value === 'add' ? "新增成功" : "更新成功"); |
| | | closeDia(); |
| | | }).catch(error => { |
| | | console.error('提交数据失败:', error); |
| | | proxy.$modal.msgError('提交数据失败,请稍后重试'); |
| | | }); |
| | | handleSubmit(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | const handleSubmit = () => { |
| | | const submitData = { |
| | | id: form.value.id, |
| | | productName: form.value.productName, |
| | | batchNumber: form.value.batchNumber, |
| | | expireDate: form.value.expiryDate, |
| | | stockQuantity: form.value.stockQuantity, |
| | | customerName: form.value.customerName, |
| | | contactPhone: form.value.contactPhone, |
| | | disRes: form.value.problemDesc, |
| | | 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, |
| | | disDate: form.value.handleDate |
| | | }; |
| | | |
| | | const apiCall = operationType.value === 'add' ? expiryAfterSalesAdd : expiryAfterSalesUpdate; |
| | | apiCall(submitData).then(() => { |
| | | const successText = operationType.value === "add" ? "新增成功" : operationType.value === "handle" ? "处理成功" : "更新成功"; |
| | | proxy.$modal.msgSuccess(successText); |
| | | closeDia(); |
| | | }).catch(error => { |
| | | console.error('提交数据失败:', error); |
| | | proxy.$modal.msgError('提交数据失败,请稍后重试'); |
| | | }); |
| | | } |
| | | |
| | | // 关闭弹框 |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | |
| | | <el-button type="danger" @click="handleDelete">删除</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | |
| | | |
| | | <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> |
| | |
| | | current: page.value.current, |
| | | size: page.value.size |
| | | }; |
| | | |
| | | |
| | | expiryAfterSalesListPage(queryParams).then(res => { |
| | | // 映射后端返回数据到前端表格 |
| | | tableData.value = res.data.records.map(item => ({ |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | title="新增售后单" |
| | | width="90%" |
| | | @close="closeDia" |
| | | > |
| | | <el-dialog v-model="dialogFormVisible" |
| | | title="新增售后单" |
| | | width="90%" |
| | | @close="closeDia"> |
| | | <div> |
| | | <span class="descriptions">基础资料</span> |
| | | <el-form |
| | | :model="form" |
| | | label-width="140px" |
| | | label-position="top" |
| | | :rules="rules" |
| | | ref="formRef" |
| | | > |
| | | <el-form :model="form" |
| | | label-width="140px" |
| | | label-position="top" |
| | | :rules="rules" |
| | | ref="formRef"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="4"> |
| | | <el-form-item label="客户名称:" prop="customerName"> |
| | | <el-select |
| | | v-model="form.customerName" |
| | | filterable |
| | | @change="customerNameChange" |
| | | > |
| | | <el-option |
| | | v-for="item in customerNameOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | <el-form-item label="客户名称:" |
| | | prop="customerName"> |
| | | <el-select v-model="form.customerName" |
| | | filterable |
| | | @change="customerNameChange"> |
| | | <el-option v-for="item in customerNameOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :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" |
| | | :key="dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | | <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" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item label="关联销售单号:" prop="salesContractNo"> |
| | | <el-select |
| | | v-model="form.salesContractNo" |
| | | @change="associatedSalesOrderNumberChange" |
| | | filterable |
| | | > |
| | | <el-option |
| | | v-for="item in associatedSalesOrderNumberOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | <el-form-item label="关联销售单号:" |
| | | prop="salesContractNo"> |
| | | <el-select v-model="form.salesContractNo" |
| | | @change="associatedSalesOrderNumberChange" |
| | | filterable> |
| | | <el-option v-for="item in associatedSalesOrderNumberOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :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" |
| | | :key="dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | | <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" /> |
| | | </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> |
| | | </el-form> |
| | | <hr> |
| | | <div style="padding-top: 20px"> |
| | | <div style="display: flex; justify-content: space-between"> |
| | | <span class="descriptions">关联产品</span> |
| | | <el-button |
| | | type="primary" |
| | | style="margin-right: 12px; margin-bottom: 10px" |
| | | @click="isShowProductSelectDialog = true" |
| | | > |
| | | <div style="padding-top: 20px"> |
| | | <div style="display: flex; justify-content: space-between"> |
| | | <span class="descriptions">关联产品</span> |
| | | <el-button type="primary" |
| | | style="margin-right: 12px; margin-bottom: 10px" |
| | | @click="isShowProductSelectDialog = true"> |
| | | 选择产品 |
| | | </el-button> |
| | | </div> |
| | | <PIMTable |
| | | :isShowPagination="false" |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | > |
| | | <template #approveStatus="{ row }"> |
| | | <el-tag :type="getApproveStatusType(row)" size="small"> |
| | | {{ getApproveStatusText(row) }} |
| | | </el-tag> |
| | | </template> |
| | | <template #shippingStatus="{ row }"> |
| | | <el-tag :type="getShippingStatusType(row)" size="small"> |
| | | {{ getShippingStatusText(row) }} |
| | | </el-tag> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | <PIMTable :isShowPagination="false" |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData"> |
| | | <template #approveStatus="{ row }"> |
| | | <el-tag :type="getApproveStatusType(row)" |
| | | size="small"> |
| | | {{ getApproveStatusText(row) }} |
| | | </el-tag> |
| | | </template> |
| | | <template #shippingStatus="{ row }"> |
| | | <el-tag :type="getShippingStatusType(row)" |
| | | size="small"> |
| | | {{ getShippingStatusText(row) }} |
| | | </el-tag> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDia">取消</el-button> |
| | | </div> |
| | | </template> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" |
| | | @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDia">取消</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 选择产品弹窗 --> |
| | | <ProductSelectDialog |
| | | v-model="isShowProductSelectDialog" |
| | | :products="currentSalesOrderProducts" |
| | | :selected-ids="currentSelectedProductIds" |
| | | @confirm="handleSelectProducts" |
| | | /> |
| | | <ProductSelectDialog v-model="isShowProductSelectDialog" |
| | | :products="currentSalesOrderProducts" |
| | | :selected-ids="currentSelectedProductIds" |
| | | @confirm="handleSelectProducts" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, toRefs, getCurrentInstance, computed } from "vue"; |
| | | 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 { getCurrentDate } from "@/utils/index.js"; |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | | const formRef = ref(null) |
| | | const customerNameOptions = ref([]) |
| | | const userStore = useUserStore(); |
| | | import { ref, reactive, toRefs, getCurrentInstance, computed } from "vue"; |
| | | 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 { getCurrentDate } from "@/utils/index.js"; |
| | | const { proxy } = getCurrentInstance(); |
| | | const emit = defineEmits(["close"]); |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref(""); |
| | | const formRef = ref(null); |
| | | const customerNameOptions = ref([]); |
| | | const userStore = useUserStore(); |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | topic: "", |
| | | serviceType: "", |
| | | urgency: "", |
| | | salesLedgerId: null, |
| | | productModelIds: "", |
| | | customerId: null, |
| | | salesContractNo: "", |
| | | proDesc: "", |
| | | customerName: "" |
| | | }, |
| | | rules: { |
| | | 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 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", |
| | | "degree_of_urgency" |
| | | ); |
| | | |
| | | 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 normalizeProductRow = (row) => { |
| | | return { |
| | | ...row, |
| | | id: getProductRowId(row), |
| | | 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 ?? '', |
| | | 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" }, |
| | | { label: "规格型号", prop: "specificationModel" }, |
| | | { label: "单位", prop: "unit" }, |
| | | { |
| | | label: "产品状态", |
| | | prop: "approveStatus", |
| | | width: 100, |
| | | align: "center", |
| | | dataType: "slot", |
| | | slot: "approveStatus", |
| | | }, |
| | | { |
| | | label: "发货状态", |
| | | align: "center", |
| | | width: 140, |
| | | dataType: "slot", |
| | | slot: "shippingStatus", |
| | | }, |
| | | { label: "快递公司", prop: "expressCompany", width: 140 }, |
| | | { label: "快递单号", prop: "expressNumber", width: 160 }, |
| | | { label: "发货车牌", prop: "shippingCarNumber", minWidth: 100, align: "center" }, |
| | | { label: "发货日期", prop: "shippingDate", minWidth: 100, align: "center" }, |
| | | { label: "数量", prop: "quantity", width: 100 }, |
| | | { label: "税率(%)", prop: "taxRate", width: 100 }, |
| | | { |
| | | label: "含税单价(元)", |
| | | prop: "taxInclusiveUnitPrice", |
| | | width: 160, |
| | | formatData: formatCurrency, |
| | | }, |
| | | { |
| | | label: "含税总价(元)", |
| | | prop: "taxInclusiveTotalPrice", |
| | | width: 160, |
| | | formatData: formatCurrency, |
| | | }, |
| | | { |
| | | label: "不含税总价(元)", |
| | | prop: "taxExclusiveTotalPrice", |
| | | width: 160, |
| | | formatData: formatCurrency, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "操作", |
| | | align: "center", |
| | | fixed: 'right', |
| | | operation: [ |
| | | { |
| | | name: "删除", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | tableData.value = tableData.value.filter(i => getProductRowId(i) !== getProductRowId(row)) |
| | | }, |
| | | |
| | | }, |
| | | ], |
| | | }, |
| | | ]) |
| | | 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 mapped = rows |
| | | .map(normalizeProductRow) |
| | | .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 !== '') |
| | | }) |
| | | |
| | | 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 |
| | | } |
| | | |
| | | const associatedSalesOrderNumberOptions = ref([]) |
| | | |
| | | const currentSalesOrderProducts = computed(() => { |
| | | const opt = associatedSalesOrderNumberOptions.value.find( |
| | | (item) => item.value === form.value.salesContractNo |
| | | ) |
| | | return (opt?.productData || []).map(normalizeProductRow) |
| | | }) |
| | | |
| | | const customerNameChange = (val) => { |
| | | form.value.salesContractNo = ""; |
| | | form.value.salesLedgerId = null; |
| | | tableData.value = []; |
| | | associatedSalesOrderNumberOptions.value = []; |
| | | const opt = customerNameOptions.value.find(item => item.value === val); |
| | | if (opt) { |
| | | form.value.customerId = opt.id; |
| | | } else { |
| | | form.value.customerId = null; |
| | | } |
| | | getSalesLedger({ |
| | | 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 |
| | | })) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | 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 '不足' |
| | | } |
| | | |
| | | const getApproveStatusType = (row) => { |
| | | const statusText = getApproveStatusText(row) |
| | | return statusText === '不足' ? 'danger' : 'success' |
| | | } |
| | | |
| | | const getShippingStatusText = (row) => { |
| | | if (!row) return '待发货' |
| | | if (row.shippingDate || row.shippingCarNumber) { |
| | | return '已发货' |
| | | } |
| | | const status = row.shippingStatus |
| | | if (status === null || status === undefined || status === '') { |
| | | return '待发货' |
| | | } |
| | | const map = { |
| | | '待发货': '待发货', |
| | | '待审核': '待审核', |
| | | '审核中': '审核中', |
| | | '审核拒绝': '审核拒绝', |
| | | '审核通过': '审核通过', |
| | | '已发货': '已发货' |
| | | } |
| | | return map[String(status).trim()] || '待发货' |
| | | } |
| | | |
| | | const getShippingStatusType = (row) => { |
| | | if (!row) return 'info' |
| | | if (row.shippingDate || row.shippingCarNumber) { |
| | | return 'success' |
| | | } |
| | | 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' |
| | | } |
| | | |
| | | // 打开弹框 |
| | | const openDialog =async (type, row) => { |
| | | // 请求多个接口,获取数据 |
| | | let res = await getAllCustomerList({ |
| | | current: 1, |
| | | size: 1000, |
| | | total: 0, |
| | | const data = reactive({ |
| | | form: { |
| | | topic: "", |
| | | serviceType: "", |
| | | urgency: "", |
| | | salesLedgerId: null, |
| | | productModelIds: "", |
| | | customerId: null, |
| | | salesContractNo: "", |
| | | proDesc: "", |
| | | customerName: "", |
| | | }, |
| | | rules: { |
| | | customerName: [ |
| | | { required: true, message: "请选择客户名称", trigger: "change" }, |
| | | ], |
| | | serviceType: [ |
| | | { required: true, message: "请选择售后类型", trigger: "change" }, |
| | | ], |
| | | urgency: [{ required: true, message: "请选择紧急程度", trigger: "change" }], |
| | | feedbackDate: [{ required: true, message: "请选择", trigger: "change" }], |
| | | }, |
| | | }); |
| | | if(res.records){ |
| | | customerNameOptions.value = res.records.map(item => ({ |
| | | label: item.customerName, |
| | | value: item.customerName, |
| | | id: item.id |
| | | })); |
| | | } |
| | | |
| | | // 自定义校验函数:判断是否需要校验售后编号 |
| | | |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | form.value = {} |
| | | proxy.resetForm("formRef"); |
| | | form.value.checkUserId = userStore.id; |
| | | form.value.feedbackDate = getCurrentDate(); |
| | | // 新增时清空已选关联产品 |
| | | if (type === "add") { |
| | | tableData.value = [] |
| | | } |
| | | userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data; |
| | | }); |
| | | if (type === "edit") { |
| | | form.value = {...row} |
| | | if (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 => ({ |
| | | const { form, rules } = toRefs(data); |
| | | 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 { post_sale_waiting_list, degree_of_urgency } = proxy.useDict( |
| | | "post_sale_waiting_list", |
| | | "degree_of_urgency" |
| | | ); |
| | | |
| | | 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 normalizeProductRow = row => { |
| | | return { |
| | | ...row, |
| | | id: getProductRowId(row), |
| | | 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 ?? "", |
| | | 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" }, |
| | | { label: "规格型号", prop: "specificationModel" }, |
| | | { label: "单位", prop: "unit" }, |
| | | { |
| | | label: "产品状态", |
| | | prop: "approveStatus", |
| | | width: 100, |
| | | align: "center", |
| | | dataType: "slot", |
| | | slot: "approveStatus", |
| | | }, |
| | | { |
| | | label: "发货状态", |
| | | align: "center", |
| | | width: 140, |
| | | dataType: "slot", |
| | | slot: "shippingStatus", |
| | | }, |
| | | { label: "快递公司", prop: "expressCompany", width: 140 }, |
| | | { label: "快递单号", prop: "expressNumber", width: 160 }, |
| | | { |
| | | label: "发货车牌", |
| | | prop: "shippingCarNumber", |
| | | minWidth: 100, |
| | | align: "center", |
| | | }, |
| | | { label: "发货日期", prop: "shippingDate", minWidth: 100, align: "center" }, |
| | | { label: "数量", prop: "quantity", width: 100 }, |
| | | { label: "税率(%)", prop: "taxRate", width: 100 }, |
| | | { |
| | | label: "含税单价(元)", |
| | | prop: "taxInclusiveUnitPrice", |
| | | width: 160, |
| | | formatData: formatCurrency, |
| | | }, |
| | | { |
| | | label: "含税总价(元)", |
| | | prop: "taxInclusiveTotalPrice", |
| | | width: 160, |
| | | formatData: formatCurrency, |
| | | }, |
| | | { |
| | | label: "不含税总价(元)", |
| | | prop: "taxExclusiveTotalPrice", |
| | | width: 160, |
| | | formatData: formatCurrency, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "操作", |
| | | align: "center", |
| | | fixed: "right", |
| | | operation: [ |
| | | { |
| | | name: "删除", |
| | | type: "text", |
| | | clickFun: row => { |
| | | tableData.value = tableData.value.filter( |
| | | i => getProductRowId(i) !== getProductRowId(row) |
| | | ); |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | 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 mapped = rows |
| | | .map(normalizeProductRow) |
| | | .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 !== ""); |
| | | }); |
| | | |
| | | 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; |
| | | }; |
| | | |
| | | const associatedSalesOrderNumberOptions = ref([]); |
| | | |
| | | const currentSalesOrderProducts = computed(() => { |
| | | const opt = associatedSalesOrderNumberOptions.value.find( |
| | | item => item.value === form.value.salesContractNo |
| | | ); |
| | | return (opt?.productData || []).map(normalizeProductRow); |
| | | }); |
| | | |
| | | const customerNameChange = val => { |
| | | form.value.salesContractNo = ""; |
| | | form.value.salesLedgerId = null; |
| | | tableData.value = []; |
| | | associatedSalesOrderNumberOptions.value = []; |
| | | const opt = customerNameOptions.value.find(item => item.value === val); |
| | | if (opt) { |
| | | form.value.customerId = opt.id; |
| | | } else { |
| | | form.value.customerId = null; |
| | | } |
| | | getSalesLedger({ |
| | | 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 "充足"; |
| | | } |
| | | console.log(form.value) |
| | | } |
| | | } |
| | | const submitForm = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | // 匹配产品型号IDs |
| | | form.value.productModelIds = tableData.value.map(item => item.id).join(",") |
| | | if (operationType.value === "add") { |
| | | afterSalesServiceAdd(form.value).then(response => { |
| | | proxy.$modal.msgSuccess("新增成功") |
| | | closeDia() |
| | | }) |
| | | } else { |
| | | afterSalesServiceUpdate(form.value).then(response => { |
| | | proxy.$modal.msgSuccess("修改成功") |
| | | closeDia() |
| | | }) |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | // 关闭弹框 |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | dialogFormVisible.value = false; |
| | | emit('close') |
| | | }; |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | | if (row.approveStatus === 0 && (row.shippingDate || row.shippingCarNumber)) { |
| | | return "已出库"; |
| | | } |
| | | return "不足"; |
| | | }; |
| | | |
| | | const getApproveStatusType = row => { |
| | | const statusText = getApproveStatusText(row); |
| | | return statusText === "不足" ? "danger" : "success"; |
| | | }; |
| | | |
| | | const getShippingStatusText = row => { |
| | | if (!row) return "待发货"; |
| | | if (row.shippingDate || row.shippingCarNumber) { |
| | | return "已发货"; |
| | | } |
| | | const status = row.shippingStatus; |
| | | if (status === null || status === undefined || status === "") { |
| | | return "待发货"; |
| | | } |
| | | const map = { |
| | | 待发货: "待发货", |
| | | 待审核: "待审核", |
| | | 审核中: "审核中", |
| | | 审核拒绝: "审核拒绝", |
| | | 审核通过: "审核通过", |
| | | 已发货: "已发货", |
| | | }; |
| | | return map[String(status).trim()] || "待发货"; |
| | | }; |
| | | |
| | | const getShippingStatusType = row => { |
| | | if (!row) return "info"; |
| | | if (row.shippingDate || row.shippingCarNumber) { |
| | | return "success"; |
| | | } |
| | | 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"; |
| | | }; |
| | | |
| | | // 打开弹框 |
| | | const openDialog = async (type, row) => { |
| | | // 请求多个接口,获取数据 |
| | | let res = await getAllCustomerList({ |
| | | current: 1, |
| | | size: 1000, |
| | | total: 0, |
| | | }); |
| | | console.log(res, "res"); |
| | | |
| | | if (res.data.records) { |
| | | customerNameOptions.value = res.data.records.map(item => ({ |
| | | label: item.customerName, |
| | | value: item.customerName, |
| | | id: item.id, |
| | | })); |
| | | } else { |
| | | } |
| | | |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | form.value = {}; |
| | | proxy.resetForm("formRef"); |
| | | form.value.checkUserId = userStore.id; |
| | | form.value.feedbackDate = getCurrentDate(); |
| | | // 新增时清空已选关联产品 |
| | | if (type === "add") { |
| | | tableData.value = []; |
| | | } |
| | | userListNoPageByTenantId().then(res => { |
| | | userList.value = res.data; |
| | | }); |
| | | if (type === "edit") { |
| | | form.value = { ...row }; |
| | | if (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 => ({ |
| | | label: item.salesContractNo, |
| | | value: item.salesContractNo, |
| | | productData: item.productData, |
| | | id: item.id, |
| | | }) |
| | | ); |
| | | } |
| | | } |
| | | console.log(form.value); |
| | | } |
| | | }; |
| | | const submitForm = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | // 匹配产品型号IDs |
| | | form.value.productModelIds = tableData.value |
| | | .map(item => item.id) |
| | | .join(","); |
| | | if (operationType.value === "add") { |
| | | afterSalesServiceAdd(form.value).then(response => { |
| | | proxy.$modal.msgSuccess("新增成功"); |
| | | closeDia(); |
| | | }); |
| | | } else { |
| | | afterSalesServiceUpdate(form.value).then(response => { |
| | | proxy.$modal.msgSuccess("修改成功"); |
| | | closeDia(); |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | // 关闭弹框 |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | dialogFormVisible.value = false; |
| | | emit("close"); |
| | | }; |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .descriptions { |
| | | margin-bottom: 20px; |
| | | display: inline-block; |
| | | font-size: 1rem; |
| | | font-weight: 600; |
| | | padding-left: 12px; |
| | | position: relative; |
| | | } |
| | | .descriptions { |
| | | margin-bottom: 20px; |
| | | display: inline-block; |
| | | font-size: 1rem; |
| | | font-weight: 600; |
| | | padding-left: 12px; |
| | | position: relative; |
| | | } |
| | | |
| | | .descriptions::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 0; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: 4px; |
| | | height: 1rem; |
| | | background-color: #002FA7; /* Element 默认红色 */ |
| | | border-radius: 2px; |
| | | } |
| | | .descriptions::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 0; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: 4px; |
| | | height: 1rem; |
| | | background-color: #002fa7; /* Element 默认红色 */ |
| | | border-radius: 2px; |
| | | } |
| | | </style> |
| | |
| | | align="center" |
| | | > |
| | | <template #default="scope"> |
| | | {{ scope.row.runtimeDuration || '-' }} |
| | | {{ getRuntimeDurationDisplay(scope.row) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | |
| | | </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, |
| | |
| | | |
| | | 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) => { |
| | |
| | | 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 = { |
| | |
| | | |
| | | |
| | | |
| | | // 组件挂载时初始化数据 |
| | | 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> |
| | | |
| | |
| | | </el-table-column> |
| | | <el-table-column label="审批状态" prop="approvalStatus" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getApprovalStatusLabel(scope.row.approvalStatus) }} |
| | | <el-tag :type="getApprovalStatusTagType(scope.row.approvalStatus)" size="small"> |
| | | {{ getApprovalStatusLabel(scope.row.approvalStatus) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | 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') { |
| | |
| | | prop="approvalStatus" |
| | | show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getApprovalStatusLabel(scope.row.approvalStatus) }} |
| | | <el-tag :type="getApprovalStatusTagType(scope.row.approvalStatus)" size="small"> |
| | | {{ getApprovalStatusLabel(scope.row.approvalStatus) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | 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; |
| | |
| | | <div class="search_form"> |
| | | <div class="search_left"> |
| | | <span class="search_title">报表类型:</span> |
| | | <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" /> |
| | | <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" /> |
| | | </el-select> |
| | | |
| | | <span class="search_title ml10">时间范围:</span> |
| | | <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'" |
| | | v-model="searchForm.monthRange" |
| | | type="monthrange" |
| | | range-separator="至" |
| | | start-placeholder="开始月份" |
| | | end-placeholder="结束月份" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 240px;" |
| | | /> |
| | | <el-date-picker |
| | | v-else |
| | | v-model="searchForm.dateRange" |
| | | type="daterange" |
| | | range-separator="至" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 240px;" |
| | | /> |
| | | |
| | | <el-button type="primary" @click="onSearch" style="margin-left: 10px"> |
| | | <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'" |
| | | v-model="searchForm.monthRange" |
| | | type="monthrange" |
| | | range-separator="至" |
| | | start-placeholder="开始月份" |
| | | end-placeholder="结束月份" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 240px;" /> |
| | | <el-date-picker v-else |
| | | v-model="searchForm.dateRange" |
| | | type="daterange" |
| | | range-separator="至" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | 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>--> |
| | | <!-- <el-button type="success" @click="handleExport" icon="Download">--> |
| | | <!-- 导出报表--> |
| | | <!-- </el-button>--> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- <!– 统计卡片 –>--> |
| | | <!-- <div class="stats_cards" v-if="reportData.summary">--> |
| | | <!-- <el-row :gutter="20">--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon in">--> |
| | | <!-- <el-icon><TrendCharts /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.totalIn || 0 }}</div>--> |
| | | <!-- <div class="stats_label">总入库量</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon out">--> |
| | | <!-- <el-icon><TrendCharts /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.totalOut || 0 }}</div>--> |
| | | <!-- <div class="stats_label">总出库量</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon stock">--> |
| | | <!-- <el-icon><Box /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.currentStock || 0 }}</div>--> |
| | | <!-- <div class="stats_label">当前库存</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon turnover">--> |
| | | <!-- <el-icon><Refresh /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.turnoverRate || 0 }}%</div>--> |
| | | <!-- <div class="stats_label">周转率</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- </el-row>--> |
| | | <!-- </div>--> |
| | | |
| | | <!-- <!– 图表区域 –>--> |
| | | <!-- <div class="chart_section" v-if="reportData.chartData">--> |
| | | <!-- <el-row :gutter="20">--> |
| | | <!-- <el-col :span="12">--> |
| | | <!-- <el-card>--> |
| | | <!-- <template #header>--> |
| | | <!-- <span>库存趋势图</span>--> |
| | | <!-- </template>--> |
| | | <!-- <div ref="trendChart" style="height: 300px;"></div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="12">--> |
| | | <!-- <el-card>--> |
| | | <!-- <template #header>--> |
| | | <!-- <span>进出库对比</span>--> |
| | | <!-- </template>--> |
| | | <!-- <div ref="comparisonChart" style="height: 300px;"></div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- </el-row>--> |
| | | <!-- </div>--> |
| | | |
| | | <!-- <!– 统计卡片 –>--> |
| | | <!-- <div class="stats_cards" v-if="reportData.summary">--> |
| | | <!-- <el-row :gutter="20">--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon in">--> |
| | | <!-- <el-icon><TrendCharts /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.totalIn || 0 }}</div>--> |
| | | <!-- <div class="stats_label">总入库量</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon out">--> |
| | | <!-- <el-icon><TrendCharts /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.totalOut || 0 }}</div>--> |
| | | <!-- <div class="stats_label">总出库量</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon stock">--> |
| | | <!-- <el-icon><Box /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.currentStock || 0 }}</div>--> |
| | | <!-- <div class="stats_label">当前库存</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="6">--> |
| | | <!-- <el-card class="stats_card">--> |
| | | <!-- <div class="stats_content">--> |
| | | <!-- <div class="stats_icon turnover">--> |
| | | <!-- <el-icon><Refresh /></el-icon>--> |
| | | <!-- </div>--> |
| | | <!-- <div class="stats_info">--> |
| | | <!-- <div class="stats_value">{{ reportData.summary.turnoverRate || 0 }}%</div>--> |
| | | <!-- <div class="stats_label">周转率</div>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- </el-row>--> |
| | | <!-- </div>--> |
| | | <!-- <!– 图表区域 –>--> |
| | | <!-- <div class="chart_section" v-if="reportData.chartData">--> |
| | | <!-- <el-row :gutter="20">--> |
| | | <!-- <el-col :span="12">--> |
| | | <!-- <el-card>--> |
| | | <!-- <template #header>--> |
| | | <!-- <span>库存趋势图</span>--> |
| | | <!-- </template>--> |
| | | <!-- <div ref="trendChart" style="height: 300px;"></div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="12">--> |
| | | <!-- <el-card>--> |
| | | <!-- <template #header>--> |
| | | <!-- <span>进出库对比</span>--> |
| | | <!-- </template>--> |
| | | <!-- <div ref="comparisonChart" style="height: 300px;"></div>--> |
| | | <!-- </el-card>--> |
| | | <!-- </el-col>--> |
| | | <!-- </el-row>--> |
| | | <!-- </div>--> |
| | | <!-- 详细数据表格 --> |
| | | <div class="table_section"> |
| | | <el-card> |
| | | <template #header> |
| | | <span>{{ getTableTitle() }}</span> |
| | | </template> |
| | | <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" |
| | | label="序号" |
| | | type="index" |
| | | width="60" |
| | | /> |
| | | <el-table-column |
| | | label="入库时间" |
| | | prop="createTime" |
| | | width="200" |
| | | show-overflow-tooltip |
| | | v-if="searchForm.reportType !== 'inout'" |
| | | /> |
| | | <el-table-column |
| | | label="入库批次" |
| | | prop="inboundBatches" |
| | | width="240" |
| | | show-overflow-tooltip |
| | | v-if="searchForm.reportType !== 'inout'" |
| | | /> |
| | | <el-table-column |
| | | label="产品大类" |
| | | prop="productName" |
| | | show-overflow-tooltip |
| | | /> |
| | | <el-table-column |
| | | label="规格型号" |
| | | prop="model" |
| | | show-overflow-tooltip |
| | | /> |
| | | <el-table-column |
| | | label="单位" |
| | | prop="unit" |
| | | show-overflow-tooltip |
| | | /> |
| | | <el-table-column |
| | | label="入库数量" |
| | | prop="totalStockIn" |
| | | align="center" |
| | | v-if="searchForm.reportType === 'inout'" |
| | | /> |
| | | <el-table-column |
| | | label="入库数量" |
| | | prop="stockInNum" |
| | | align="center" |
| | | v-else |
| | | /> |
| | | <el-table-column |
| | | label="出库数量" |
| | | prop="totalStockOut" |
| | | width="100" |
| | | align="center" |
| | | v-if="searchForm.reportType === 'inout'" |
| | | /> |
| | | <el-table-column |
| | | label="现在库存" |
| | | prop="currentStock" |
| | | align="center" |
| | | /> |
| | | <el-table-column label="来源" |
| | | prop="recordType" |
| | | v-if="searchForm.reportType !== 'inout'" |
| | | show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getRecordType(scope.row.recordType) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | label="入库人" |
| | | prop="createBy" |
| | | width="80" |
| | | v-if="searchForm.reportType !== 'inout'" |
| | | show-overflow-tooltip |
| | | /> |
| | | <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" |
| | | label="序号" |
| | | type="index" |
| | | width="60" /> |
| | | <el-table-column label="入库时间" |
| | | prop="createTime" |
| | | width="200" |
| | | show-overflow-tooltip |
| | | v-if="searchForm.reportType !== 'inout'" /> |
| | | <el-table-column label="入库批次" |
| | | prop="inboundBatches" |
| | | width="180" |
| | | show-overflow-tooltip |
| | | 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="规格型号" |
| | | prop="model" |
| | | show-overflow-tooltip /> |
| | | <el-table-column label="单位" |
| | | prop="unit" |
| | | show-overflow-tooltip /> |
| | | <el-table-column label="入库数量" |
| | | prop="totalStockIn" |
| | | align="center" |
| | | v-if="searchForm.reportType === 'inout'" /> |
| | | <el-table-column label="入库数量" |
| | | prop="stockInNum" |
| | | align="center" |
| | | v-else /> |
| | | <el-table-column label="出库数量" |
| | | prop="totalStockOut" |
| | | width="100" |
| | | align="center" |
| | | v-if="searchForm.reportType === 'inout'" /> |
| | | <el-table-column label="现在库存" |
| | | prop="currentStock" |
| | | align="center" /> |
| | | <el-table-column label="来源" |
| | | prop="recordType" |
| | | v-if="searchForm.reportType !== 'inout'" |
| | | show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getRecordType(scope.row.recordType) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="入库人" |
| | | prop="createBy" |
| | | width="80" |
| | | v-if="searchForm.reportType !== 'inout'" |
| | | show-overflow-tooltip /> |
| | | </el-table> |
| | | <pagination |
| | | :total="total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page="page.current" |
| | | :limit="page.size" |
| | | @pagination="paginationChange" |
| | | /> |
| | | <pagination :total="total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page="page.current" |
| | | :limit="page.size" |
| | | @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 { |
| | | getStockInventoryInAndOutReportList, |
| | | getStockInventoryReportList |
| | | } from "@/api/inventoryManagement/stockInventory.js"; |
| | | import { |
| | | findAllQualifiedStockInRecordTypeOptions,findAllUnQualifiedStockInRecordTypeOptions, |
| | | } from "@/api/basicData/enum.js"; |
| | | 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, |
| | | } from "@/api/inventoryManagement/stockInventory.js"; |
| | | import { |
| | | findAllQualifiedStockInRecordTypeOptions, |
| | | findAllUnQualifiedStockInRecordTypeOptions, |
| | | } from "@/api/basicData/enum.js"; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | // 响应式数据 |
| | | const tableLoading = ref(false); |
| | | const trendChart = ref(null); |
| | | const comparisonChart = ref(null); |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | // 响应式数据 |
| | | const tableLoading = ref(false) |
| | | const trendChart = ref(null) |
| | | const comparisonChart = ref(null) |
| | | const searchForm = reactive({ |
| | | reportType: "daily", |
| | | singleDate: "", |
| | | dateRange: [], |
| | | monthRange: [], |
| | | }); |
| | | |
| | | const searchForm = reactive({ |
| | | reportType: 'daily', |
| | | singleDate: '', |
| | | dateRange: [], |
| | | monthRange: [] |
| | | }) |
| | | |
| | | const reportData = ref({ |
| | | summary: null, |
| | | chartData: null, |
| | | tableData: [] |
| | | }) |
| | | |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | }) |
| | | |
| | | const total = ref(0) |
| | | |
| | | const stockRecordTypeOptions = ref([]) |
| | | |
| | | const getRecordType = (recordType) => { |
| | | return stockRecordTypeOptions.value.find(item => item.value === recordType)?.label || '' |
| | | } |
| | | |
| | | // 获取来源类型选项 |
| | | const fetchStockRecordTypeOptions = () => { |
| | | findAllQualifiedStockInRecordTypeOptions() |
| | | .then(res => { |
| | | stockRecordTypeOptions.value = res.data; |
| | | findAllUnQualifiedStockInRecordTypeOptions() |
| | | .then(res => { |
| | | stockRecordTypeOptions.value = [...stockRecordTypeOptions.value,...res.data]; |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | // 获取表格标题 |
| | | const getTableTitle = () => { |
| | | const typeMap = { |
| | | daily: '日报详细数据', |
| | | monthly: '月报详细数据', |
| | | inout: '进出存报表详细数据' |
| | | } |
| | | return typeMap[searchForm.reportType] || '报表详细数据' |
| | | } |
| | | |
| | | // 报表类型改变 |
| | | const handleReportTypeChange = () => { |
| | | page.current = 1 |
| | | reportData.value = { |
| | | const reportData = ref({ |
| | | summary: null, |
| | | chartData: null, |
| | | tableData: [] |
| | | } |
| | | } |
| | | tableData: [], |
| | | }); |
| | | |
| | | // 查询数据 |
| | | const handleQuery = async () => { |
| | | if (!validateSearchForm()) { |
| | | return |
| | | } |
| | | |
| | | tableLoading.value = true |
| | | try { |
| | | const baseParams = getQueryParams() |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | }); |
| | | |
| | | const total = ref(0); |
| | | |
| | | const stockRecordTypeOptions = ref([]); |
| | | |
| | | const getRecordType = recordType => { |
| | | return ( |
| | | stockRecordTypeOptions.value.find(item => item.value === recordType) |
| | | ?.label || "" |
| | | ); |
| | | }; |
| | | |
| | | // 获取来源类型选项 |
| | | const fetchStockRecordTypeOptions = () => { |
| | | findAllQualifiedStockInRecordTypeOptions().then(res => { |
| | | stockRecordTypeOptions.value = res.data; |
| | | findAllUnQualifiedStockInRecordTypeOptions().then(res => { |
| | | stockRecordTypeOptions.value = [ |
| | | ...stockRecordTypeOptions.value, |
| | | ...res.data, |
| | | ]; |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | // 获取表格标题 |
| | | const getTableTitle = () => { |
| | | const typeMap = { |
| | | daily: "日报详细数据", |
| | | monthly: "月报详细数据", |
| | | inout: "进出存报表详细数据", |
| | | }; |
| | | return typeMap[searchForm.reportType] || "报表详细数据"; |
| | | }; |
| | | |
| | | // 报表类型改变 |
| | | const handleReportTypeChange = () => { |
| | | page.current = 1; |
| | | reportData.value = { |
| | | summary: null, |
| | | chartData: null, |
| | | tableData: [], |
| | | }; |
| | | }; |
| | | |
| | | // 查询数据 |
| | | const handleQuery = async () => { |
| | | if (!validateSearchForm()) { |
| | | return; |
| | | } |
| | | |
| | | tableLoading.value = true; |
| | | try { |
| | | const baseParams = getQueryParams(); |
| | | const params = { |
| | | ...baseParams, |
| | | current: page.current, |
| | | size: page.size, |
| | | }; |
| | | let response; |
| | | |
| | | if (searchForm.reportType === "inout") { |
| | | response = await getStockInventoryInAndOutReportList(params); |
| | | } else { |
| | | response = await getStockInventoryReportList(params); |
| | | } |
| | | if (response.code === 200) { |
| | | 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); |
| | | } finally { |
| | | tableLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // 查询按钮:重置到第一页并查询 |
| | | const onSearch = () => { |
| | | page.current = 1; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // 分页变化 |
| | | const paginationChange = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | handleQuery(); |
| | | }; |
| | | // // 生成假数据 |
| | | // const generateMockData = () => { |
| | | // // 生成统计卡片假数据 |
| | | // const summary = { |
| | | // totalIn: 1000, |
| | | // totalOut: 600, |
| | | // currentStock: 400, |
| | | // turnoverRate: 30 |
| | | // } |
| | | |
| | | // // 生成图表假数据 |
| | | // const trendDates = ['2025-09-15', '2025-09-16', '2025-09-17', '2025-09-18', '2025-09-19'] |
| | | // const trendValues = [300, 350, 400, 380, 420] |
| | | // const comparisonDates = ['2025-09-15', '2025-09-16', '2025-09-17'] |
| | | // const inValues = [100, 150, 200] |
| | | // const outValues = [80, 120, 100] |
| | | |
| | | // const chartData = { |
| | | // trendDates, |
| | | // trendValues, |
| | | // comparisonDates, |
| | | // inValues, |
| | | // outValues |
| | | // } |
| | | |
| | | // reportData.value = { |
| | | // summary, |
| | | // chartData, |
| | | // tableData: [] |
| | | // } |
| | | // } |
| | | // 验证搜索表单 |
| | | const validateSearchForm = () => { |
| | | if (searchForm.reportType === "daily") { |
| | | if (!searchForm.singleDate) { |
| | | ElMessage.warning("请选择日期"); |
| | | return false; |
| | | } |
| | | } else if (searchForm.reportType === "inout") { |
| | | if (!searchForm.dateRange || searchForm.dateRange.length !== 2) { |
| | | ElMessage.warning("请选择日期范围"); |
| | | return false; |
| | | } |
| | | } else if (searchForm.reportType === "monthly") { |
| | | if (!searchForm.monthRange || searchForm.monthRange.length !== 2) { |
| | | ElMessage.warning("请选择月份范围"); |
| | | return false; |
| | | } |
| | | } |
| | | return true; |
| | | }; |
| | | |
| | | // 获取查询参数 |
| | | const getQueryParams = () => { |
| | | const params = { |
| | | ...baseParams, |
| | | current: page.current, |
| | | size: page.size, |
| | | } |
| | | let response |
| | | reportType: searchForm.reportType, |
| | | reportDate: "", |
| | | startMonth: "", |
| | | endMonth: "", |
| | | startDate: "", |
| | | endDate: "", |
| | | }; |
| | | |
| | | if (searchForm.reportType === 'inout') { |
| | | response = await getStockInventoryInAndOutReportList(params) |
| | | if (searchForm.reportType === "daily") { |
| | | params.reportDate = searchForm.singleDate; |
| | | } else if (searchForm.reportType === "monthly") { |
| | | params.startMonth = searchForm.monthRange[0]; |
| | | params.endMonth = searchForm.monthRange[1]; |
| | | } else { |
| | | response = await getStockInventoryReportList(params) |
| | | params.startDate = searchForm.dateRange[0]; |
| | | params.endDate = searchForm.dateRange[1]; |
| | | } |
| | | if (response.code === 200) { |
| | | 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() |
| | | // }) |
| | | |
| | | |
| | | return params; |
| | | }; |
| | | |
| | | // 重置搜索 |
| | | const handleReset = () => { |
| | | searchForm.reportType = "daily"; |
| | | searchForm.singleDate = ""; |
| | | searchForm.dateRange = []; |
| | | searchForm.monthRange = []; |
| | | reportData.value = { |
| | | summary: null, |
| | | chartData: null, |
| | | tableData: [], |
| | | }; |
| | | }; |
| | | |
| | | // 导出报表 |
| | | const handleExport = async () => { |
| | | if (!validateSearchForm()) { |
| | | return; |
| | | } |
| | | } catch (error) { |
| | | ElMessage.error('查询失败:' + error.message) |
| | | } finally { |
| | | tableLoading.value = false |
| | | } |
| | | } |
| | | |
| | | // 查询按钮:重置到第一页并查询 |
| | | const onSearch = () => { |
| | | page.current = 1 |
| | | handleQuery() |
| | | } |
| | | try { |
| | | const params = getQueryParams(); |
| | | // const response = await exportStockReport(params) |
| | | proxy.download("/stockin/exportCopy", params, "库存报表.xlsx"); |
| | | // 创建下载链接 |
| | | // const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) |
| | | // const url = window.URL.createObjectURL(blob) |
| | | // const link = document.createElement('a') |
| | | // link.href = url |
| | | // link.download = `${getTableTitle()}_${new Date().getTime()}.xlsx` |
| | | // document.body.appendChild(link) |
| | | // link.click() |
| | | // document.body.removeChild(link) |
| | | // window.URL.revokeObjectURL(url) |
| | | |
| | | // 分页变化 |
| | | const paginationChange = (obj) => { |
| | | page.current = obj.page |
| | | page.size = obj.limit |
| | | handleQuery() |
| | | } |
| | | // // 生成假数据 |
| | | // const generateMockData = () => { |
| | | // // 生成统计卡片假数据 |
| | | // const summary = { |
| | | // totalIn: 1000, |
| | | // totalOut: 600, |
| | | // currentStock: 400, |
| | | // turnoverRate: 30 |
| | | // } |
| | | |
| | | // // 生成图表假数据 |
| | | // const trendDates = ['2025-09-15', '2025-09-16', '2025-09-17', '2025-09-18', '2025-09-19'] |
| | | // const trendValues = [300, 350, 400, 380, 420] |
| | | // const comparisonDates = ['2025-09-15', '2025-09-16', '2025-09-17'] |
| | | // const inValues = [100, 150, 200] |
| | | // const outValues = [80, 120, 100] |
| | | |
| | | // const chartData = { |
| | | // trendDates, |
| | | // trendValues, |
| | | // comparisonDates, |
| | | // inValues, |
| | | // outValues |
| | | // } |
| | | |
| | | // reportData.value = { |
| | | // summary, |
| | | // chartData, |
| | | // tableData: [] |
| | | // } |
| | | // } |
| | | // 验证搜索表单 |
| | | const validateSearchForm = () => { |
| | | if (searchForm.reportType === 'daily') { |
| | | if (!searchForm.singleDate) { |
| | | ElMessage.warning('请选择日期') |
| | | return false |
| | | // ElMessage.success('导出成功') |
| | | } catch (error) { |
| | | ElMessage.error("导出失败:" + error.message); |
| | | } |
| | | } else if (searchForm.reportType === 'inout') { |
| | | if (!searchForm.dateRange || searchForm.dateRange.length !== 2) { |
| | | ElMessage.warning('请选择日期范围') |
| | | return false |
| | | } |
| | | } else if (searchForm.reportType === 'monthly') { |
| | | if (!searchForm.monthRange || searchForm.monthRange.length !== 2) { |
| | | ElMessage.warning('请选择月份范围') |
| | | return false |
| | | } |
| | | } |
| | | return true |
| | | } |
| | | }; |
| | | |
| | | // 获取查询参数 |
| | | const getQueryParams = () => { |
| | | const params = { |
| | | reportType: searchForm.reportType, |
| | | reportDate: "", |
| | | startMonth: "", |
| | | endMonth: "", |
| | | startDate: "", |
| | | endDate: "" |
| | | } |
| | | |
| | | 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] |
| | | } |
| | | |
| | | return params |
| | | } |
| | | // 初始化图表 |
| | | const initCharts = () => { |
| | | if (!reportData.value.chartData) return; |
| | | |
| | | // 重置搜索 |
| | | const handleReset = () => { |
| | | searchForm.reportType = 'daily' |
| | | searchForm.singleDate = '' |
| | | searchForm.dateRange = [] |
| | | searchForm.monthRange = [] |
| | | reportData.value = { |
| | | summary: null, |
| | | chartData: null, |
| | | tableData: [] |
| | | } |
| | | } |
| | | initTrendChart(); |
| | | initComparisonChart(); |
| | | }; |
| | | |
| | | // 导出报表 |
| | | const handleExport = async () => { |
| | | if (!validateSearchForm()) { |
| | | return |
| | | } |
| | | |
| | | try { |
| | | const params = getQueryParams() |
| | | // const response = await exportStockReport(params) |
| | | proxy.download("/stockin/exportCopy", params, '库存报表.xlsx') |
| | | // 创建下载链接 |
| | | // const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) |
| | | // const url = window.URL.createObjectURL(blob) |
| | | // const link = document.createElement('a') |
| | | // link.href = url |
| | | // link.download = `${getTableTitle()}_${new Date().getTime()}.xlsx` |
| | | // document.body.appendChild(link) |
| | | // link.click() |
| | | // document.body.removeChild(link) |
| | | // window.URL.revokeObjectURL(url) |
| | | |
| | | // ElMessage.success('导出成功') |
| | | } catch (error) { |
| | | ElMessage.error('导出失败:' + error.message) |
| | | } |
| | | } |
| | | // 初始化趋势图 |
| | | const initTrendChart = () => { |
| | | if (!trendChart.value) return; |
| | | |
| | | // 初始化图表 |
| | | const initCharts = () => { |
| | | if (!reportData.value.chartData) return |
| | | |
| | | initTrendChart() |
| | | initComparisonChart() |
| | | } |
| | | |
| | | // 初始化趋势图 |
| | | const initTrendChart = () => { |
| | | if (!trendChart.value) return |
| | | |
| | | const chart = echarts.init(trendChart.value) |
| | | const option = { |
| | | title: { |
| | | text: '库存变化趋势', |
| | | left: 'center' |
| | | }, |
| | | tooltip: { |
| | | trigger: 'axis' |
| | | }, |
| | | legend: { |
| | | data: ['库存量'], |
| | | top: 30 |
| | | }, |
| | | xAxis: { |
| | | 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' |
| | | }, |
| | | series: [ |
| | | { |
| | | name: '入库', |
| | | type: 'bar', |
| | | data: reportData.value.chartData.inValues || [], |
| | | itemStyle: { |
| | | color: '#67C23A' |
| | | } |
| | | const chart = echarts.init(trendChart.value); |
| | | const option = { |
| | | title: { |
| | | text: "库存变化趋势", |
| | | left: "center", |
| | | }, |
| | | { |
| | | name: '出库', |
| | | type: 'bar', |
| | | data: reportData.value.chartData.outValues || [], |
| | | itemStyle: { |
| | | color: '#F56C6C' |
| | | } |
| | | } |
| | | ] |
| | | } |
| | | chart.setOption(option) |
| | | } |
| | | tooltip: { |
| | | trigger: "axis", |
| | | }, |
| | | legend: { |
| | | data: ["库存量"], |
| | | top: 30, |
| | | }, |
| | | xAxis: { |
| | | 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); |
| | | }; |
| | | |
| | | // 组件挂载时设置默认时间 |
| | | onMounted(() => { |
| | | const today = new Date() |
| | | searchForm.singleDate = today.toISOString().split('T')[0] |
| | | |
| | | const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000) |
| | | searchForm.dateRange = [ |
| | | yesterday.toISOString().split('T')[0], |
| | | today.toISOString().split('T')[0] |
| | | ] |
| | | // 初始化对比图 |
| | | const initComparisonChart = () => { |
| | | if (!comparisonChart.value) return; |
| | | |
| | | fetchStockRecordTypeOptions() |
| | | // 初始化加载一次数据 |
| | | handleQuery() |
| | | }) |
| | | 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", |
| | | }, |
| | | }, |
| | | { |
| | | name: "出库", |
| | | type: "bar", |
| | | data: reportData.value.chartData.outValues || [], |
| | | itemStyle: { |
| | | color: "#F56C6C", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | chart.setOption(option); |
| | | }; |
| | | |
| | | // 组件挂载时设置默认时间 |
| | | onMounted(() => { |
| | | const today = new Date(); |
| | | searchForm.singleDate = today.toISOString().split("T")[0]; |
| | | |
| | | const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000); |
| | | searchForm.dateRange = [ |
| | | yesterday.toISOString().split("T")[0], |
| | | today.toISOString().split("T")[0], |
| | | ]; |
| | | |
| | | fetchStockRecordTypeOptions(); |
| | | // 初始化加载一次数据 |
| | | handleQuery(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .app-container { |
| | | padding: 20px; |
| | | } |
| | | .app-container { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .search_form { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | padding: 20px; |
| | | background: #fff; |
| | | border-radius: 4px; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | .search_form { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | padding: 20px; |
| | | background: #fff; |
| | | border-radius: 4px; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .search_left { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | .search_left { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .search_title { |
| | | font-weight: 500; |
| | | color: #333; |
| | | margin-right: 8px; |
| | | } |
| | | .search_title { |
| | | font-weight: 500; |
| | | color: #333; |
| | | margin-right: 8px; |
| | | } |
| | | |
| | | .ml10 { |
| | | margin-left: 10px; |
| | | } |
| | | .ml10 { |
| | | margin-left: 10px; |
| | | } |
| | | |
| | | .stats_cards { |
| | | margin-bottom: 20px; |
| | | } |
| | | .stats_cards { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .stats_card { |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | } |
| | | .stats_card { |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .stats_content { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 10px 0; |
| | | } |
| | | .stats_content { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 10px 0; |
| | | } |
| | | |
| | | .stats_icon { |
| | | width: 50px; |
| | | height: 50px; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 15px; |
| | | font-size: 24px; |
| | | color: #fff; |
| | | } |
| | | .stats_icon { |
| | | width: 50px; |
| | | height: 50px; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 15px; |
| | | font-size: 24px; |
| | | color: #fff; |
| | | } |
| | | |
| | | .stats_icon.in { |
| | | background: linear-gradient(135deg, #67C23A, #85CE61); |
| | | } |
| | | .stats_icon.in { |
| | | background: linear-gradient(135deg, #67c23a, #85ce61); |
| | | } |
| | | |
| | | .stats_icon.out { |
| | | background: linear-gradient(135deg, #F56C6C, #F78989); |
| | | } |
| | | .stats_icon.out { |
| | | background: linear-gradient(135deg, #f56c6c, #f78989); |
| | | } |
| | | |
| | | .stats_icon.stock { |
| | | background: linear-gradient(135deg, #409EFF, #66B1FF); |
| | | } |
| | | .stats_icon.stock { |
| | | background: linear-gradient(135deg, #409eff, #66b1ff); |
| | | } |
| | | |
| | | .stats_icon.turnover { |
| | | background: linear-gradient(135deg, #E6A23C, #EEBE77); |
| | | } |
| | | .stats_icon.turnover { |
| | | background: linear-gradient(135deg, #e6a23c, #eebe77); |
| | | } |
| | | |
| | | .stats_info { |
| | | flex: 1; |
| | | } |
| | | .stats_info { |
| | | flex: 1; |
| | | } |
| | | |
| | | .stats_value { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #333; |
| | | line-height: 1; |
| | | margin-bottom: 5px; |
| | | } |
| | | .stats_value { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #333; |
| | | line-height: 1; |
| | | margin-bottom: 5px; |
| | | } |
| | | |
| | | .stats_label { |
| | | font-size: 14px; |
| | | color: #666; |
| | | } |
| | | .stats_label { |
| | | font-size: 14px; |
| | | color: #666; |
| | | } |
| | | |
| | | .chart_section { |
| | | margin-bottom: 20px; |
| | | } |
| | | .chart_section { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .table_section { |
| | | margin-bottom: 20px; |
| | | } |
| | | .table_section { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | :deep(.el-card__header) { |
| | | background: #f8f9fa; |
| | | border-bottom: 1px solid #e9ecef; |
| | | font-weight: 500; |
| | | } |
| | | :deep(.el-card__header) { |
| | | background: #f8f9fa; |
| | | border-bottom: 1px solid #e9ecef; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | :deep(.el-table .el-table__header-wrapper th) { |
| | | background-color: #F0F1F5 !important; |
| | | color: #333333; |
| | | font-weight: 600; |
| | | } |
| | | :deep(.el-table .el-table__header-wrapper th) { |
| | | background-color: #f0f1f5 !important; |
| | | color: #333333; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | :deep(.el-table .el-table__body-wrapper td) { |
| | | padding: 8px 0; |
| | | } |
| | | :deep(.el-table .el-table__body-wrapper td) { |
| | | padding: 8px 0; |
| | | } |
| | | </style> |