新疆-新聚
1.销售台账中的销售合同按框架合同处理
2.设备台账需按车间/区域汇总设备,车间/位置可自行新增,且支持子父级递归
3.计量器具台账支持通过 Excel 文件批量新增数据
4.设备保养定时任务新增“年度保养”任务频率
| | |
| | | }); |
| | | }; |
| | | |
| | | export const getDeviceLedger = () => { |
| | | export const getDeviceLedger = (params) => { |
| | | return request({ |
| | | url: "/device/ledger/getDeviceLedger", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | } |
| | | |
| | | // 创建框架订单 |
| | | export function createFrameworkOrder(query) { |
| | | return request({ |
| | | url: "/sales/ledger/createFrameworkOrder", |
| | | method: "post", |
| | | data: query, |
| | | }); |
| | | } |
| | | |
| | | // 查询框架订单列表 |
| | | export function listFrameworkOrders(query) { |
| | | return request({ |
| | | url: "/sales/ledger/listFrameworkOrders", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 查询框架合同累计发货数量 |
| | | export function frameworkContractShippedQuantity(query) { |
| | | return request({ |
| | | url: "/sales/ledger/frameworkContractShippedQuantity", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-width="120px"> |
| | | <el-row> |
| | | <el-col :span="12"> |
| | | <el-form-item label="设备名称" prop="taskId"> |
| | | <el-select v-model="form.taskId" @change="setDeviceModel"> |
| | | <el-option |
| | | v-for="(item, index) in deviceOptions" |
| | | :key="index" |
| | | :label="item.deviceName" |
| | | :value="item.id" |
| | | ></el-option> |
| | | </el-select> |
| | | <el-form-item label="设备名称" prop="taskName"> |
| | | <div style="display: flex; gap: 8px; width: 100%"> |
| | | <el-input v-model="form.taskName" placeholder="请选择设备" readonly style="flex: 1" /> |
| | | <el-button @click="openDeviceDialog">选择设备</el-button> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 选择巡检设备弹窗 --> |
| | | <el-dialog v-model="deviceDialogVisible" title="选择巡检设备" width="760px" draggable append-to-body> |
| | | <div class="device-select-wrap"> |
| | | <div class="device-select-tree"> |
| | | <div class="all-device-node" |
| | | :class="{ active: deviceAreaId === null }" |
| | | @click="selectAllDevice"> |
| | | 全部设备 |
| | | </div> |
| | | <el-tree |
| | | ref="deviceAreaTreeRef" |
| | | v-loading="deviceAreaLoading" |
| | | :data="areaTree" |
| | | node-key="id" |
| | | :props="{ label: 'label', children: 'children' }" |
| | | default-expand-all |
| | | :expand-on-click-node="false" |
| | | highlight-current |
| | | :current-node-key="deviceAreaId" |
| | | @node-click="onDeviceAreaClick" |
| | | /> |
| | | </div> |
| | | <div class="device-select-list"> |
| | | <el-table |
| | | :data="deviceList" |
| | | v-loading="deviceListLoading" |
| | | border |
| | | height="420" |
| | | style="cursor: pointer" |
| | | @row-click="selectDevice" |
| | | > |
| | | <el-table-column prop="deviceName" label="设备名称" min-width="140" /> |
| | | <el-table-column prop="deviceModel" label="规格型号" min-width="120" /> |
| | | <el-table-column prop="areaName" label="所属区域" min-width="120" /> |
| | | <el-table-column label="操作" width="80" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" @click.stop="selectDevice(scope.row)">选择</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import {addOrEditTimingTask} from "@/api/inspectionManagement/index.js"; |
| | | import {userListNoPageByTenantId} from "@/api/system/user.js"; |
| | | import { getDeviceLedger } from "@/api/equipmentManagement/ledger"; |
| | | import { getDeviceAreaTree } from "@/api/equipmentManagement/area"; |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits() |
| | |
| | | const dialogVisitable = ref(false); |
| | | const operationType = ref('add'); |
| | | const deviceOptions = ref([]); |
| | | |
| | | // 设备选择弹窗相关 |
| | | const deviceDialogVisible = ref(false); |
| | | const areaTree = ref([]); |
| | | const deviceAreaId = ref(null); |
| | | const deviceAreaLoading = ref(false); |
| | | const deviceAreaTreeRef = ref(); |
| | | const deviceList = ref([]); |
| | | const deviceListLoading = ref(false); |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | taskId: undefined, |
| | |
| | | time: '' |
| | | }, |
| | | rules: { |
| | | taskId: [{ required: true, message: "请选择设备", trigger: "change" },], |
| | | taskName: [{ required: true, message: "请选择设备", trigger: "change" },], |
| | | inspector: [{ required: true, message: "请输入巡检人", trigger: "blur" },], |
| | | dateStr: [{ required: true, message: "请选择登记时间", trigger: "change" }], |
| | | frequencyType: [{ required: true, message: "请选择任务频率", trigger: "change" }], |
| | |
| | | |
| | | const loadDeviceName = async () => { |
| | | const { data } = await getDeviceLedger(); |
| | | deviceOptions.value = data; |
| | | deviceOptions.value = data || []; |
| | | }; |
| | | |
| | | const setDeviceModel = (id) => { |
| | |
| | | } |
| | | } |
| | | |
| | | // 加载区域树 |
| | | const loadDeviceAreaTree = () => { |
| | | deviceAreaLoading.value = true; |
| | | getDeviceAreaTree() |
| | | .then((res) => { |
| | | areaTree.value = res.data || []; |
| | | }) |
| | | .finally(() => { |
| | | deviceAreaLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | // 加载设备列表 |
| | | const loadDevices = (areaId) => { |
| | | deviceListLoading.value = true; |
| | | getDeviceLedger(areaId ? { areaId } : undefined) |
| | | .then((res) => { |
| | | deviceList.value = res.data || []; |
| | | }) |
| | | .finally(() => { |
| | | deviceListLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | // 打开设备选择弹窗 |
| | | const openDeviceDialog = () => { |
| | | deviceDialogVisible.value = true; |
| | | deviceAreaId.value = null; |
| | | loadDeviceAreaTree(); |
| | | loadDevices(); |
| | | }; |
| | | |
| | | // 选择全部设备 |
| | | const selectAllDevice = () => { |
| | | deviceAreaId.value = null; |
| | | loadDevices(); |
| | | }; |
| | | |
| | | // 点击区域节点 |
| | | const onDeviceAreaClick = (data) => { |
| | | deviceAreaId.value = data.id; |
| | | loadDevices(data.id); |
| | | }; |
| | | |
| | | // 选中设备回填 |
| | | const selectDevice = (row) => { |
| | | form.value.taskId = row.id; |
| | | form.value.taskName = row.deviceName; |
| | | deviceDialogVisible.value = false; |
| | | }; |
| | | |
| | | // 打开弹框 |
| | | const openDialog = async (type, row) => { |
| | | dialogVisitable.value = true |
| | | operationType.value = type |
| | | |
| | | |
| | | // 重置表单 |
| | | resetForm(); |
| | | |
| | | |
| | | // 加载用户列表 |
| | | userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data; |
| | | }); |
| | | |
| | | // 加载设备列表 |
| | | |
| | | // 加载设备列表(编辑回显用) |
| | | await loadDeviceName(); |
| | | |
| | | |
| | | if (type === 'edit' && row) { |
| | | form.value = {...row} |
| | | form.value.inspector = form.value.inspectorIds.split(',').map(Number) |
| | | |
| | | |
| | | // 如果有设备ID,自动设置设备信息 |
| | | if (form.value.taskId) { |
| | | setDeviceModel(form.value.taskId); |
| | | if (!form.value.taskName) { |
| | | setDeviceModel(form.value.taskId); |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | try { |
| | | form.value.inspectorIds = form.value.inspector.join(',') |
| | | delete form.value.inspector |
| | | |
| | | |
| | | if (form.value.frequencyType === 'WEEKLY') { |
| | | let frequencyDetail = '' |
| | | frequencyDetail = form.value.week + ',' + form.value.time |
| | | form.value.frequencyDetail = frequencyDetail |
| | | } |
| | | |
| | | |
| | | let res = await userStore.getInfo() |
| | | form.value.registrantId = res.user.userId |
| | | |
| | | |
| | | await addOrEditTimingTask(form.value) |
| | | cancel() |
| | | proxy.$modal.msgSuccess('提交成功') |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | |
| | | </style> |
| | | .device-select-wrap { |
| | | display: flex; |
| | | } |
| | | .device-select-tree { |
| | | width: 260px; |
| | | flex-shrink: 0; |
| | | padding-right: 12px; |
| | | border-right: 1px solid #ebeef5; |
| | | max-height: 460px; |
| | | overflow-y: auto; |
| | | } |
| | | .device-select-list { |
| | | flex: 1; |
| | | min-width: 0; |
| | | padding-left: 12px; |
| | | } |
| | | .all-device-node { |
| | | padding: 8px 12px; |
| | | cursor: pointer; |
| | | font-size: 14px; |
| | | border-radius: 4px; |
| | | margin-bottom: 8px; |
| | | } |
| | | .all-device-node:hover { |
| | | background: #f5f7fa; |
| | | } |
| | | .all-device-node.active { |
| | | background: #ecf5ff; |
| | | color: #409eff; |
| | | } |
| | | </style> |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="所属区域" prop="areaId"> |
| | | <el-tree-select |
| | | v-model="form.areaId" |
| | | :data="areaTree" |
| | | node-key="id" |
| | | :props="{ label: 'label', children: 'children' }" |
| | | check-strictly |
| | | clearable |
| | | placeholder="请选择所属区域" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="供应商" prop="supplierName"> |
| | | <el-input v-model="form.supplierName" placeholder="请输入供应商" /> |
| | | </el-form-item> |
| | |
| | | import useFormData from "@/hooks/useFormData"; |
| | | // import useUserStore from "@/store/modules/user"; |
| | | import { getLedgerById } from "@/api/equipmentManagement/ledger"; |
| | | import { getDeviceAreaTree } from "@/api/equipmentManagement/area"; |
| | | import dayjs from "dayjs"; |
| | | import { |
| | | calculateTaxIncludeTotalPrice, |
| | |
| | | name: "设备台账表单", |
| | | }); |
| | | const formRef = ref(null); |
| | | const areaTree = ref([]); |
| | | const operationType = ref(''); |
| | | // 设备类型固定选项 |
| | | const deviceTypeOptions = ref([ |
| | |
| | | deviceModel: undefined, // 规格型号 |
| | | deviceBrand: undefined, // 设备品牌 |
| | | type: undefined, // 设备类型 |
| | | areaId: undefined, // 所属区域 |
| | | supplierName: undefined, // 供应商 |
| | | storageLocation: undefined, // 存放位置 |
| | | isDepr: 2, // 是否启用折旧 1-是 2-否 |
| | |
| | | } |
| | | }); |
| | | |
| | | const loadAreaTree = () => { |
| | | getDeviceAreaTree().then((res) => { |
| | | areaTree.value = res.data || []; |
| | | }); |
| | | }; |
| | | loadAreaTree(); |
| | | |
| | | const loadForm = async (id) => { |
| | | loadAreaTree(); |
| | | if (id) { |
| | | operationType.value = 'edit' |
| | | } |
| | |
| | | form.deviceBrand = data.deviceBrand; |
| | | form.type = data.type; |
| | | form.supplierName = data.supplierName; |
| | | form.areaId = data.areaId; |
| | | form.storageLocation = data.storageLocation; |
| | | form.isDepr = data.isDepr; |
| | | form.annualDepreciationAmount = data.annualDepreciationAmount; |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-form :model="filters" :inline="true"> |
| | | <el-form-item label="设备名称"> |
| | | <el-input |
| | | v-model="filters.deviceName" |
| | | style="width: 200px" |
| | | placeholder="请输入设备名称" |
| | | clearable |
| | | @change="getTableData" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="规格型号"> |
| | | <el-input |
| | | v-model="filters.deviceModel" |
| | | style="width: 200px" |
| | | placeholder="请输入规格型号" |
| | | clearable |
| | | @change="getTableData" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="供应商"> |
| | | <el-input |
| | | v-model="filters.supplierName" |
| | | style="width: 200px" |
| | | placeholder="请输入供应商" |
| | | clearable |
| | | @change="getTableData" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="录入日期:"> |
| | | <el-date-picker v-model="filters.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange" |
| | | placeholder="请选择" clearable @change="changeDaterange" /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="getTableData">搜索</el-button> |
| | | <el-button @click="resetFilters">重置</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="table_list"> |
| | | <div class="actions"> |
| | | <div></div> |
| | | <div> |
| | | <el-button type="primary" @click="add" icon="Plus"> 新增 </el-button> |
| | | <el-button type="info" @click="handleImport" icon="Upload">导入</el-button> |
| | | <el-button @click="handleOut" icon="download">导出</el-button> |
| | | <el-button |
| | | type="danger" |
| | | icon="Delete" |
| | | :disabled="multipleList.length <= 0" |
| | | @click="deleteRow(multipleList.map((item) => item.id))" |
| | | > |
| | | 批量删除 |
| | | <div class="area-view"> |
| | | <!-- 左侧区域树 --> |
| | | <div class="area-left"> |
| | | <div class="area-left-header"> |
| | | <el-button type="primary" @click="openAreaDialog('add')" icon="Plus"> |
| | | 新增区域 |
| | | </el-button> |
| | | </div> |
| | | <div |
| | | class="all-device-node" |
| | | :class="{ active: currentAreaId === null }" |
| | | @click="handleAllDeviceClick" |
| | | > |
| | | 全部设备 |
| | | </div> |
| | | <el-tree |
| | | ref="areaTreeRef" |
| | | v-loading="areaTreeLoading" |
| | | :data="areaTree" |
| | | node-key="id" |
| | | :props="{ label: 'label', children: 'children' }" |
| | | :expand-on-click-node="false" |
| | | default-expand-all |
| | | draggable |
| | | highlight-current |
| | | :current-node-key="currentAreaId" |
| | | @node-click="handleAreaNodeClick" |
| | | @node-drop="handleAreaNodeDrop" |
| | | > |
| | | <template #default="{ node, data }"> |
| | | <div class="area-tree-node"> |
| | | <span class="area-node-label">{{ data.label }}</span> |
| | | <span class="area-node-count">{{ data.deviceCount ?? 0 }}</span> |
| | | <span class="area-node-actions"> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | size="small" |
| | | @click.stop="openAreaDialog('addChild', data)" |
| | | > |
| | | 子级 |
| | | </el-button> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | size="small" |
| | | @click.stop="openAreaDialog('edit', data)" |
| | | > |
| | | 编辑 |
| | | </el-button> |
| | | <el-button |
| | | link |
| | | type="danger" |
| | | size="small" |
| | | @click.stop="handleAreaDelete(node, data)" |
| | | > |
| | | 删除 |
| | | </el-button> |
| | | </span> |
| | | </div> |
| | | </template> |
| | | </el-tree> |
| | | </div> |
| | | <PIMTable |
| | | rowKey="id" |
| | | isSelection |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | | total: pagination.total, |
| | | }" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination="changePage" |
| | | > |
| | | </PIMTable> |
| | | |
| | | <!-- 右侧设备列表 --> |
| | | <div class="area-right"> |
| | | <el-form :model="filters" :inline="true"> |
| | | <el-form-item label="设备名称"> |
| | | <el-input |
| | | v-model="filters.deviceName" |
| | | style="width: 200px" |
| | | placeholder="请输入设备名称" |
| | | clearable |
| | | @change="getTableData" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="规格型号"> |
| | | <el-input |
| | | v-model="filters.deviceModel" |
| | | style="width: 200px" |
| | | placeholder="请输入规格型号" |
| | | clearable |
| | | @change="getTableData" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="供应商"> |
| | | <el-input |
| | | v-model="filters.supplierName" |
| | | style="width: 200px" |
| | | placeholder="请输入供应商" |
| | | clearable |
| | | @change="getTableData" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="录入日期:"> |
| | | <el-date-picker |
| | | v-model="filters.entryDate" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | type="daterange" |
| | | placeholder="请选择" |
| | | clearable |
| | | @change="changeDaterange" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="getTableData">搜索</el-button> |
| | | <el-button @click="handleReset">重置</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="table_list"> |
| | | <div class="actions"> |
| | | <div></div> |
| | | <div> |
| | | <el-button type="primary" @click="add" icon="Plus"> 新增 </el-button> |
| | | <el-button type="info" @click="handleImport" icon="Upload">导入</el-button> |
| | | <el-button @click="handleOut" icon="download">导出</el-button> |
| | | <el-button |
| | | type="danger" |
| | | icon="Delete" |
| | | :disabled="multipleList.length <= 0" |
| | | @click="deleteRow(multipleList.map((item) => item.id))" |
| | | > |
| | | 批量删除 |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <PIMTable |
| | | rowKey="id" |
| | | isSelection |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | | total: pagination.total, |
| | | }" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination="changePage" |
| | | > |
| | | </PIMTable> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <Modal ref="modalRef" @success="getTableData"></Modal> |
| | | <el-dialog v-model="qrDialogVisible" title="二维码" width="300px" draggable> |
| | | <div style="text-align:center;"> |
| | | <img :src="qrCodeUrl" alt="二维码" style="width:200px;height:200px;" /> |
| | | <div style="margin:10px 0;"> |
| | | <div style="text-align: center"> |
| | | <img :src="qrCodeUrl" alt="二维码" style="width: 200px; height: 200px" /> |
| | | <div style="margin: 10px 0"> |
| | | <el-button type="primary" @click="downloadQRCode">下载二维码图片</el-button> |
| | | </div> |
| | | </div> |
| | | </el-dialog> |
| | | |
| | | |
| | | <!-- 导入对话框 --> |
| | | <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body> |
| | | <el-upload |
| | |
| | | <template #tip> |
| | | <div class="el-upload__tip text-center"> |
| | | <span>仅允许导入xls、xlsx格式文件。</span> |
| | | <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline; margin-left: 5px;" @click="importTemplate">下载模板</el-link> |
| | | <el-link |
| | | type="primary" |
| | | :underline="false" |
| | | style="font-size: 12px; vertical-align: baseline; margin-left: 5px" |
| | | @click="importTemplate" |
| | | > |
| | | 下载模板 |
| | | </el-link> |
| | | </div> |
| | | </template> |
| | | </el-upload> |
| | |
| | | <el-descriptions-item label="设备品牌">{{ detailData.deviceBrand }}</el-descriptions-item> |
| | | <el-descriptions-item label="设备类型">{{ detailData.type }}</el-descriptions-item> |
| | | <el-descriptions-item label="供应商">{{ detailData.supplierName }}</el-descriptions-item> |
| | | <el-descriptions-item label="所属区域">{{ detailData.areaName }}</el-descriptions-item> |
| | | <el-descriptions-item label="存放位置">{{ detailData.storageLocation }}</el-descriptions-item> |
| | | <el-descriptions-item label="单位">{{ detailData.unit }}</el-descriptions-item> |
| | | <el-descriptions-item label="数量">{{ detailData.number }}</el-descriptions-item> |
| | |
| | | <el-descriptions-item label="税率(%)">{{ detailData.taxRate }}</el-descriptions-item> |
| | | <el-descriptions-item label="不含税总价">{{ detailData.unTaxIncludingPriceTotal }}</el-descriptions-item> |
| | | <el-descriptions-item label="录入日期">{{ detailData.createTime }}</el-descriptions-item> |
| | | <el-descriptions-item label="预计运行时间">{{ detailData.planRuntimeTime ? dayjs(detailData.planRuntimeTime).format('YYYY-MM-DD') : '' }}</el-descriptions-item> |
| | | <el-descriptions-item label="预计运行时间">{{ |
| | | detailData.planRuntimeTime ? dayjs(detailData.planRuntimeTime).format('YYYY-MM-DD') : '' |
| | | }}</el-descriptions-item> |
| | | <el-descriptions-item label="设备图片" :span="2"> |
| | | <div v-if="detailData.storageBlobVOs && detailData.storageBlobVOs.length > 0" style="display: flex; gap: 10px; flex-wrap: wrap;"> |
| | | <el-image |
| | | v-for="(file, index) in detailData.storageBlobVOs" |
| | | :key="index" |
| | | :src="file.previewURL || file.url" |
| | | :preview-src-list="detailData.storageBlobVOs.map(u => u.previewURL || u.url)" |
| | | <div |
| | | v-if="detailData.storageBlobVOs && detailData.storageBlobVOs.length > 0" |
| | | style="display: flex; gap: 10px; flex-wrap: wrap" |
| | | > |
| | | <el-image |
| | | v-for="(file, index) in detailData.storageBlobVOs" |
| | | :key="index" |
| | | :src="file.previewURL || file.url" |
| | | :preview-src-list="detailData.storageBlobVOs.map((u) => u.previewURL || u.url)" |
| | | :initial-index="index" |
| | | style="width: 100px; height: 100px" |
| | | fit="cover" |
| | | style="width: 100px; height: 100px" |
| | | fit="cover" |
| | | /> |
| | | </div> |
| | | <span v-else>无图片</span> |
| | |
| | | <el-button @click="detailDialogVisible = false">关闭</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 区域新增/编辑弹窗 --> |
| | | <el-dialog |
| | | v-model="areaDialogVisible" |
| | | :title="areaDialogTitle" |
| | | width="420px" |
| | | draggable |
| | | @closed="areaFormRef && areaFormRef.resetFields()" |
| | | > |
| | | <el-form :model="areaForm" :rules="areaRules" ref="areaFormRef" label-width="90px"> |
| | | <el-form-item label="上级区域" prop="parentId"> |
| | | <el-tree-select |
| | | v-model="areaForm.parentId" |
| | | :data="areaTree" |
| | | node-key="id" |
| | | :props="{ label: 'label', children: 'children' }" |
| | | check-strictly |
| | | clearable |
| | | placeholder="不选则为根区域" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="区域名称" prop="name"> |
| | | <el-input v-model="areaForm.name" placeholder="请输入区域/车间/位置名称" clearable /> |
| | | </el-form-item> |
| | | <el-form-item label="排序" prop="sort"> |
| | | <el-input-number v-model="areaForm.sort" :min="0" style="width: 100%" placeholder="可选" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button type="primary" @click="submitAreaForm">确定</el-button> |
| | | <el-button @click="areaDialogVisible = false">取消</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { usePaginationApi } from "@/hooks/usePaginationApi"; |
| | | // import { Search } from "@element-plus/icons-vue"; |
| | | import { getLedgerPage, delLedger, getLedgerById } from "@/api/equipmentManagement/ledger"; |
| | | import { onMounted, getCurrentInstance, ref, reactive } from "vue"; |
| | | import { |
| | | getLedgerPage, |
| | | delLedger, |
| | | getLedgerById, |
| | | } from "@/api/equipmentManagement/ledger"; |
| | | import { |
| | | getDeviceAreaTree, |
| | | addDeviceArea, |
| | | editDeviceArea, |
| | | delDeviceArea, |
| | | } from "@/api/equipmentManagement/area"; |
| | | import { onMounted, getCurrentInstance, ref, reactive, computed } from "vue"; |
| | | import Modal from "./Modal.vue"; |
| | | import { ElMessageBox, ElMessage } from "element-plus"; |
| | | import { UploadFilled } from "@element-plus/icons-vue"; |
| | |
| | | const detailDialogVisible = ref(false); |
| | | const detailData = ref({}); |
| | | |
| | | // 区域相关 |
| | | const areaTree = ref([]); |
| | | const areaTreeLoading = ref(false); |
| | | const areaTreeRef = ref(); |
| | | const currentAreaId = ref(null); |
| | | const areaDialogVisible = ref(false); |
| | | const areaDialogType = ref("add"); |
| | | const areaFormRef = ref(); |
| | | const areaForm = reactive({ |
| | | id: undefined, |
| | | name: "", |
| | | parentId: null, |
| | | sort: 0, |
| | | }); |
| | | const areaRules = { |
| | | name: [{ required: true, message: "请输入区域名称", trigger: "blur" }], |
| | | }; |
| | | const areaDialogTitle = computed(() => { |
| | | if (areaDialogType.value === "edit") return "编辑区域"; |
| | | if (areaDialogType.value === "addChild") return "新增子区域"; |
| | | return "新增区域"; |
| | | }); |
| | | |
| | | // 导入相关 |
| | | const uploadRef = ref(null) |
| | | const uploadRef = ref(null); |
| | | const upload = reactive({ |
| | | // 是否显示弹出层 |
| | | open: false, |
| | | // 弹出层标题 |
| | | title: "", |
| | | // 是否禁用上传 |
| | | isUploading: false, |
| | | // 设置上传的请求头部 |
| | | headers: { Authorization: "Bearer " + getToken() }, |
| | | // 上传的地址 |
| | | url: import.meta.env.VITE_APP_BASE_API + "/device/ledger/import" |
| | | }) |
| | | url: import.meta.env.VITE_APP_BASE_API + "/device/ledger/import", |
| | | }); |
| | | |
| | | const { |
| | | filters, |
| | |
| | | supplierName: undefined, |
| | | entryDateStart: undefined, |
| | | entryDateEnd: undefined, |
| | | areaId: undefined, |
| | | }, |
| | | [ |
| | | { |
| | |
| | | { |
| | | label: "设备类型", |
| | | prop: "type", |
| | | }, |
| | | { |
| | | label: "所属区域", |
| | | prop: "areaName", |
| | | }, |
| | | { |
| | | label: "供应商", |
| | |
| | | label: "录入日期", |
| | | prop: "createTime", |
| | | formatData: (v) => { |
| | | if (!v) return ''; |
| | | // 如果包含时分秒,只取日期部分 |
| | | if (v.includes(' ')) { |
| | | return v.split(' ')[0]; |
| | | if (!v) return ""; |
| | | if (v.includes(" ")) { |
| | | return v.split(" ")[0]; |
| | | } |
| | | return v; |
| | | }, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "操作", |
| | | align: "center", |
| | | fixed: 'right', |
| | | width: 180, |
| | | operation: [ |
| | | { |
| | | dataType: "action", |
| | | label: "操作", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 180, |
| | | operation: [ |
| | | { |
| | | name: "详情", |
| | | clickFun: (row) => { |
| | | handleDetail(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "编辑", |
| | | clickFun: (row) => { |
| | | edit(row.id) |
| | | }, |
| | | }, |
| | | { |
| | | name: "二维码", |
| | | clickFun: (row) => { |
| | | showQRCode(row) |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | { |
| | | name: "编辑", |
| | | clickFun: (row) => { |
| | | edit(row.id); |
| | | }, |
| | | }, |
| | | { |
| | | name: "二维码", |
| | | clickFun: (row) => { |
| | | showQRCode(row); |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | ] |
| | | ); |
| | | |
| | | // 加载区域树 |
| | | const getAreaTree = () => { |
| | | areaTreeLoading.value = true; |
| | | getDeviceAreaTree() |
| | | .then((res) => { |
| | | areaTree.value = res.data || []; |
| | | }) |
| | | .finally(() => { |
| | | areaTreeLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | // 重置搜索与区域筛选 |
| | | const handleReset = () => { |
| | | currentAreaId.value = null; |
| | | resetFilters(); |
| | | }; |
| | | |
| | | // 点击"全部设备" |
| | | const handleAllDeviceClick = () => { |
| | | currentAreaId.value = null; |
| | | filters.areaId = undefined; |
| | | getTableData(); |
| | | }; |
| | | |
| | | // 点击区域节点 |
| | | const handleAreaNodeClick = (data) => { |
| | | currentAreaId.value = data.id; |
| | | filters.areaId = data.id; |
| | | getTableData(); |
| | | }; |
| | | |
| | | // 拖拽调整父子关系 |
| | | const handleAreaNodeDrop = (draggingNode, dropNode, dropType) => { |
| | | const node = draggingNode.data; |
| | | let parentId = null; |
| | | if (dropNode && dropType === "inner") { |
| | | parentId = dropNode.data.id; |
| | | } else if (dropNode) { |
| | | parentId = dropNode.data.parentId ?? null; |
| | | } else { |
| | | parentId = null; |
| | | } |
| | | if (parentId === node.id) { |
| | | proxy.$modal.msgWarning("不能将区域挂载到自身"); |
| | | getAreaTree(); |
| | | return; |
| | | } |
| | | editDeviceArea({ |
| | | id: node.id, |
| | | name: node.label, |
| | | parentId, |
| | | sort: node.sort, |
| | | }) |
| | | .then(() => { |
| | | proxy.$modal.msgSuccess("调整成功"); |
| | | getAreaTree(); |
| | | }) |
| | | .catch(() => { |
| | | getAreaTree(); |
| | | }); |
| | | }; |
| | | |
| | | // 打开区域弹窗 |
| | | const openAreaDialog = (type, data) => { |
| | | areaDialogType.value = type; |
| | | areaForm.id = undefined; |
| | | areaForm.name = ""; |
| | | areaForm.parentId = null; |
| | | areaForm.sort = 0; |
| | | if (type === "addChild" && data) { |
| | | areaForm.parentId = data.id; |
| | | } else if (type === "edit" && data) { |
| | | areaForm.id = data.id; |
| | | areaForm.name = data.label; |
| | | areaForm.parentId = data.parentId ?? null; |
| | | areaForm.sort = data.sort ?? 0; |
| | | } |
| | | areaDialogVisible.value = true; |
| | | }; |
| | | |
| | | // 提交区域表单 |
| | | const submitAreaForm = () => { |
| | | if (!areaFormRef.value) return; |
| | | areaFormRef.value.validate((valid) => { |
| | | if (!valid) return; |
| | | const payload = { |
| | | name: areaForm.name, |
| | | parentId: areaForm.parentId ?? null, |
| | | sort: areaForm.sort ?? 0, |
| | | }; |
| | | const api = |
| | | areaDialogType.value === "edit" |
| | | ? editDeviceArea({ ...payload, id: areaForm.id }) |
| | | : addDeviceArea(payload); |
| | | api |
| | | .then(() => { |
| | | proxy.$modal.msgSuccess("操作成功"); |
| | | areaDialogVisible.value = false; |
| | | getAreaTree(); |
| | | }) |
| | | .catch(() => {}); |
| | | }); |
| | | }; |
| | | |
| | | // 删除区域 |
| | | const handleAreaDelete = (node, data) => { |
| | | ElMessageBox.confirm("确认删除该区域吗?", "提示", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | }) |
| | | .then(() => delDeviceArea(data.id)) |
| | | .then(() => { |
| | | proxy.$modal.msgSuccess("删除成功"); |
| | | if (currentAreaId.value === data.id) { |
| | | currentAreaId.value = null; |
| | | filters.areaId = undefined; |
| | | } |
| | | getAreaTree(); |
| | | getTableData(); |
| | | }) |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | // 多选后做什么 |
| | | const handleSelectionChange = (selectionList) => { |
| | |
| | | }; |
| | | const changePage = ({ page, limit }) => { |
| | | pagination.currentPage = page; |
| | | pagination.pageSize = limit; |
| | | pagination.pageSize = limit; |
| | | onCurrentChange(page); |
| | | }; |
| | | const deleteRow = (id) => { |
| | |
| | | }; |
| | | |
| | | const showQRCode = async (row) => { |
| | | // 直接使用URL,不要用JSON.stringify包装 |
| | | const qrContent = proxy.javaApi + '/device-info?deviceId=' + row.id; |
| | | const qrContent = proxy.javaApi + "/device-info?deviceId=" + row.id; |
| | | const qrDataUrl = await QRCode.toDataURL(qrContent, { width: 200, margin: 2 }); |
| | | |
| | | // 创建canvas合成带名称的二维码图片 |
| | | const canvas = document.createElement('canvas'); |
| | | const ctx = canvas.getContext('2d'); |
| | | |
| | | const canvas = document.createElement("canvas"); |
| | | const ctx = canvas.getContext("2d"); |
| | | const qrSize = 200; |
| | | const textHeight = 30; |
| | | const padding = 10; |
| | | canvas.width = qrSize + padding * 2; |
| | | canvas.height = qrSize + textHeight + padding * 2; |
| | | |
| | | // 填充白色背景 |
| | | ctx.fillStyle = '#ffffff'; |
| | | |
| | | ctx.fillStyle = "#ffffff"; |
| | | ctx.fillRect(0, 0, canvas.width, canvas.height); |
| | | |
| | | // 绘制二维码 |
| | | |
| | | const qrImg = new Image(); |
| | | qrImg.src = qrDataUrl; |
| | | await new Promise((resolve) => { qrImg.onload = resolve; }); |
| | | await new Promise((resolve) => { |
| | | qrImg.onload = resolve; |
| | | }); |
| | | ctx.drawImage(qrImg, padding, padding, qrSize, qrSize); |
| | | |
| | | // 绘制设备名称 |
| | | ctx.fillStyle = '#333333'; |
| | | ctx.font = 'bold 14px Arial'; |
| | | ctx.textAlign = 'center'; |
| | | ctx.fillText(row.deviceName || '', canvas.width / 2, qrSize + padding + 20); |
| | | |
| | | qrCodeUrl.value = canvas.toDataURL('image/png'); |
| | | |
| | | ctx.fillStyle = "#333333"; |
| | | ctx.font = "bold 14px Arial"; |
| | | ctx.textAlign = "center"; |
| | | ctx.fillText(row.deviceName || "", canvas.width / 2, qrSize + padding + 20); |
| | | |
| | | qrCodeUrl.value = canvas.toDataURL("image/png"); |
| | | qrRowData.value = row; |
| | | qrDialogVisible.value = true; |
| | | }; |
| | |
| | | a.click(); |
| | | }; |
| | | |
| | | // 导入按钮操作 |
| | | const handleImport = () => { |
| | | upload.title = "设备台账导入" |
| | | upload.open = true |
| | | } |
| | | upload.title = "设备台账导入"; |
| | | upload.open = true; |
| | | }; |
| | | |
| | | // 下载模板操作 |
| | | const importTemplate = () => { |
| | | proxy.download("/device/ledger/downloadTemplate", {}, `设备台账导入模板_${new Date().getTime()}.xlsx`) |
| | | } |
| | | proxy.download( |
| | | "/device/ledger/downloadTemplate", |
| | | {}, |
| | | `设备台账导入模板_${new Date().getTime()}.xlsx` |
| | | ); |
| | | }; |
| | | |
| | | // 文件上传中处理 |
| | | const handleFileUploadProgress = (event, file, fileList) => { |
| | | upload.isUploading = true |
| | | } |
| | | upload.isUploading = true; |
| | | }; |
| | | |
| | | // 文件上传成功处理 |
| | | const handleFileSuccess = (response, file, fileList) => { |
| | | upload.open = false |
| | | upload.isUploading = false |
| | | proxy.$refs["uploadRef"].handleRemove(file) |
| | | proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true }) |
| | | getTableData() |
| | | } |
| | | upload.open = false; |
| | | upload.isUploading = false; |
| | | proxy.$refs["uploadRef"].handleRemove(file); |
| | | proxy.$alert( |
| | | "<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + |
| | | response.msg + |
| | | "</div>", |
| | | "导入结果", |
| | | { dangerouslyUseHTMLString: true } |
| | | ); |
| | | getTableData(); |
| | | }; |
| | | |
| | | // 提交上传文件 |
| | | const submitFileForm = () => { |
| | | proxy.$refs["uploadRef"].submit() |
| | | } |
| | | proxy.$refs["uploadRef"].submit(); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getAreaTree(); |
| | | getTableData(); |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .area-view { |
| | | display: flex; |
| | | } |
| | | .area-left { |
| | | width: 320px; |
| | | flex-shrink: 0; |
| | | padding: 16px; |
| | | background: #ffffff; |
| | | margin-right: 16px; |
| | | } |
| | | .area-left-header { |
| | | margin-bottom: 10px; |
| | | } |
| | | .all-device-node { |
| | | padding: 8px 12px; |
| | | cursor: pointer; |
| | | font-size: 14px; |
| | | border-radius: 4px; |
| | | margin-bottom: 8px; |
| | | } |
| | | .all-device-node:hover { |
| | | background: #f5f7fa; |
| | | } |
| | | .all-device-node.active { |
| | | background: #ecf5ff; |
| | | color: #409eff; |
| | | } |
| | | .area-tree-node { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | font-size: 14px; |
| | | padding-right: 4px; |
| | | } |
| | | .area-node-count { |
| | | color: #909399; |
| | | font-size: 12px; |
| | | margin-left: 6px; |
| | | } |
| | | .area-node-actions { |
| | | display: none; |
| | | } |
| | | .area-tree-node:hover .area-node-actions { |
| | | display: inline-flex; |
| | | } |
| | | .area-right { |
| | | flex: 1; |
| | | min-width: 0; |
| | | padding: 16px; |
| | | background: #ffffff; |
| | | } |
| | | .table_list { |
| | | margin-top: unset; |
| | | } |
| | |
| | | <div> |
| | | <el-button type="primary" |
| | | @click="openForm('add')">新增计量器具</el-button> |
| | | <el-button type="primary" |
| | | plain |
| | | @click="handleImport">批量导入</el-button> |
| | | <el-button @click="handleDownloadTemplate">下载模板</el-button> |
| | | <el-button type="danger" |
| | | plain |
| | | @click="handleDelete">删除</el-button> |
| | |
| | | @close="handleQuery"></calibration-dia> |
| | | <files-dia ref="filesDia"></files-dia> |
| | | <rowClickDataForm ref="rowClickData"></rowClickDataForm> |
| | | <ImportDialog ref="importDialogRef" |
| | | v-model="importDialogVisible" |
| | | title="批量导入计量器具" |
| | | :action="importAction" |
| | | :headers="importHeaders" |
| | | :auto-upload="false" |
| | | :on-success="handleImportSuccess" |
| | | :on-error="handleImportError" |
| | | @confirm="handleImportConfirm" |
| | | @download-template="handleDownloadTemplate" |
| | | @close="handleImportClose" /> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import FormDia from "@/views/equipmentManagement/measurementEquipment/components/formDia.vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import { getToken } from "@/utils/auth"; |
| | | import ImportDialog from "@/components/Dialog/ImportDialog.vue"; |
| | | import CalibrationDia from "@/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue"; |
| | | import { |
| | | measuringInstrumentDelete, |
| | |
| | | proxy.$modal.msg("已取消"); |
| | | }); |
| | | }; |
| | | // 批量导入 |
| | | const importDialogRef = ref(); |
| | | const importDialogVisible = ref(false); |
| | | const importAction = |
| | | import.meta.env.VITE_APP_BASE_API + "/measuringInstrumentLedger/import"; |
| | | const importHeaders = ref({ |
| | | Authorization: `Bearer ${getToken()}`, |
| | | }); |
| | | |
| | | const handleImport = () => { |
| | | importDialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleImportSuccess = response => { |
| | | if (response.code === 200) { |
| | | proxy.$modal.msgSuccess(response.msg || "导入成功"); |
| | | importDialogVisible.value = false; |
| | | if (importDialogRef.value) { |
| | | importDialogRef.value.clearFiles(); |
| | | } |
| | | getList(); |
| | | } else { |
| | | proxy.$modal.msgError(response.msg || "导入失败"); |
| | | } |
| | | }; |
| | | |
| | | const handleImportError = error => { |
| | | proxy.$modal.msgError("导入失败,请检查文件格式是否正确"); |
| | | }; |
| | | |
| | | const handleImportConfirm = () => { |
| | | if (importDialogRef.value) { |
| | | importDialogRef.value.submit(); |
| | | } |
| | | }; |
| | | |
| | | const handleDownloadTemplate = () => { |
| | | proxy.download( |
| | | "/measuringInstrumentLedger/downloadTemplate", |
| | | {}, |
| | | "计量器具台账导入模板.xlsx" |
| | | ); |
| | | }; |
| | | |
| | | const handleImportClose = () => { |
| | | importDialogVisible.value = false; |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | |
| | | <el-option label="每周" value="WEEKLY"/> |
| | | <el-option label="每月" value="MONTHLY"/> |
| | | <el-option label="季度" value="QUARTERLY"/> |
| | | <el-option label="年度保养" value="YEARLY"/> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12" v-if="form.frequencyType === 'YEARLY'"> |
| | | <el-form-item label="执行日期" prop="frequencyDetail"> |
| | | <div style="display: flex; gap: 4px; width: 100%"> |
| | | <el-select v-model="form.yearMonth" placeholder="月" clearable style="flex: 1"> |
| | | <el-option v-for="m in 12" :key="m" :label="m + '月'" :value="m" /> |
| | | </el-select> |
| | | <el-select v-model="form.yearDay" placeholder="日" clearable style="flex: 1"> |
| | | <el-option v-for="d in 31" :key="d" :label="d + '日'" :value="d" /> |
| | | </el-select> |
| | | <el-time-picker v-model="form.yearTime" placeholder="时间" format="HH:mm" value-format="HH:mm" style="flex: 1.2" /> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | |
| | | </el-row> |
| | | <el-row> |
| | | <el-col :span="12"> |
| | |
| | | frequencyDetail: '', |
| | | week: '', |
| | | time: '', |
| | | yearMonth: undefined, |
| | | yearDay: undefined, |
| | | yearTime: '', |
| | | deviceModel: undefined, // 规格型号 |
| | | registrationDate: '', |
| | | maintenancePerson: '' // 保养人 |
| | |
| | | if (form.value.taskId) { |
| | | setDeviceModel(form.value.taskId); |
| | | } |
| | | // YEARLY 频率回显:解析 月份,日期,时间 |
| | | if (form.value.frequencyType === 'YEARLY' && form.value.frequencyDetail) { |
| | | const parts = String(form.value.frequencyDetail).split(','); |
| | | form.value.yearMonth = Number(parts[0]); |
| | | form.value.yearDay = Number(parts[1]); |
| | | form.value.yearTime = parts[2] || ''; |
| | | } |
| | | } else if (type === 'add') { |
| | | // 新增时设置登记日期为当天 |
| | | form.value.registrationDate = getCurrentDate(); |
| | |
| | | frequencyDetail: '', |
| | | week: '', |
| | | time: '', |
| | | yearMonth: undefined, |
| | | yearDay: undefined, |
| | | yearTime: '', |
| | | deviceModel: undefined, |
| | | registrationDate: '', |
| | | maintenancePerson: '' |
| | |
| | | frequencyDetail = payload.week + ',' + payload.time |
| | | payload.frequencyDetail = frequencyDetail |
| | | } |
| | | |
| | | if (payload.frequencyType === 'YEARLY') { |
| | | const month = Number(payload.yearMonth); |
| | | const day = Number(payload.yearDay); |
| | | const time = payload.yearTime; |
| | | if (!month || month < 1 || month > 12) { |
| | | proxy.$modal.msgError('月份必须在1-12之间'); |
| | | return; |
| | | } |
| | | if (!day || day < 1 || day > 31) { |
| | | proxy.$modal.msgError('日期必须在1-31之间'); |
| | | return; |
| | | } |
| | | if (!time || !/^\d{2}:\d{2}$/.test(time)) { |
| | | proxy.$modal.msgError('时间格式必须为 HH:mm'); |
| | | return; |
| | | } |
| | | payload.frequencyDetail = month + ',' + day + ',' + time; |
| | | } |
| | | |
| | | // 录入日期:直接使用表单里的 registrationDate 字段 |
| | | // 一些默认状态字段 |
| | |
| | | }" |
| | | @selection-change="handleScheduledSelectionChange" |
| | | @pagination="changeScheduledPage"> |
| | | <template #frequencyDetailRef="{ row }"> |
| | | {{ formatFrequencyDetail(row) }} |
| | | </template> |
| | | <template #statusRef="{ row }"> |
| | | <el-tag v-if="row.status === 1" |
| | | type="success">启用</el-tag> |
| | |
| | | WEEKLY: "每周", |
| | | MONTHLY: "每月", |
| | | QUARTERLY: "季度", |
| | | YEARLY: "年度保养", |
| | | }[cell] || ""), |
| | | }, |
| | | { |
| | | prop: "frequencyDetail", |
| | | label: "开始日期与时间", |
| | | minWidth: 150, |
| | | // 同样改用 formatData,PIMTable 内部会把单元格值传进来 |
| | | formatData: cell => { |
| | | if (typeof cell !== "string") return ""; |
| | | let val = cell; |
| | | const replacements = { |
| | | MON: "周一", |
| | | TUE: "周二", |
| | | WED: "周三", |
| | | THU: "周四", |
| | | FRI: "周五", |
| | | SAT: "周六", |
| | | SUN: "周日", |
| | | }; |
| | | // 使用正则一次性替换所有匹配项 |
| | | return val.replace( |
| | | /MON|TUE|WED|THU|FRI|SAT|SUN/g, |
| | | match => replacements[match] |
| | | ); |
| | | }, |
| | | dataType: "slot", |
| | | slot: "frequencyDetailRef", |
| | | }, |
| | | { prop: "maintenancePerson", label: "保养人", minWidth: 100 }, |
| | | { prop: "registrant", label: "登记人", minWidth: 100 }, |
| | |
| | | ]); |
| | | |
| | | // 保养记录表格列配置(原设备保养表格列) |
| | | const formatFrequencyDetail = (row) => { |
| | | if (!row) return ""; |
| | | const val = String(row.frequencyDetail || ""); |
| | | if (row.frequencyType === "YEARLY") { |
| | | const parts = val.split(","); |
| | | if (parts.length === 3) { |
| | | return `每年${parts[0]}月${parts[1]}日 ${parts[2]}`; |
| | | } |
| | | return val; |
| | | } |
| | | const replacements = { |
| | | MON: "周一", |
| | | TUE: "周二", |
| | | WED: "周三", |
| | | THU: "周四", |
| | | FRI: "周五", |
| | | SAT: "周六", |
| | | SUN: "周日", |
| | | }; |
| | | return val.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, (m) => replacements[m]); |
| | | }; |
| | | |
| | | const columns = ref([ |
| | | { |
| | | label: "设备名称", |
| | |
| | | } |
| | | }, |
| | | { |
| | | label: '退款储量', |
| | | label: '采购金额', |
| | | prop: 'purchaseAmount', |
| | | formatData: (val) => { |
| | | return val ? `¥${parseFloat(val).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : '¥0.00' |
| | |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | :disabled="!canShip(scope.row)" |
| | | :disabled="!canShip(scope.row) || props.row.contractType === 2" |
| | | @click="openDeliveryForm(scope.row)" |
| | | > |
| | | 发货 |
| | |
| | | width="200" |
| | | show-overflow-tooltip |
| | | /> |
| | | <el-table-column fixed="right" label="操作" width="220" align="center"> |
| | | <el-table-column fixed="right" label="操作" width="320" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" @click="openForm('view', scope.row)" |
| | | >详情 |
| | |
| | | !canEditLedger(scope.row) |
| | | " |
| | | >编辑 |
| | | </el-button> |
| | | <el-button |
| | | v-if="scope.row.contractType === 2" |
| | | link |
| | | type="success" |
| | | @click="openFrameworkOrderDialog(scope.row)" |
| | | >创建框架订单 |
| | | </el-button> |
| | | <el-button |
| | | v-if="scope.row.contractType === 2" |
| | | link |
| | | type="primary" |
| | | @click="openFrameworkOrderList(scope.row)" |
| | | >框架订单 |
| | | </el-button> |
| | | <el-button link type="primary" @click="openFileDialog(scope.row)" |
| | | >附件 |
| | |
| | | <el-button type="primary" plain @click="openQuotationDialog"> |
| | | 从销售报价导入 |
| | | </el-button> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="合同类型:" prop="contractType"> |
| | | <el-select |
| | | v-model="form.contractType" |
| | | placeholder="请选择" |
| | | clearable |
| | | :disabled="operationType === 'view' || operationType === 'edit'" |
| | | > |
| | | <el-option label="普通销售合同" :value="1" /> |
| | | <el-option label="框架合同" :value="2" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-row :gutter="30" v-if="!isFrameworkContract"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="交货日期:" prop="entryDate"> |
| | | <el-date-picker |
| | |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-col :span="12" v-if="!isFrameworkContract"> |
| | | <el-form-item label="税率(%):" prop="taxRate"> |
| | | <el-select |
| | | v-model="productForm.taxRate" |
| | |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-col :span="12" v-if="!isFrameworkContract"> |
| | | <el-form-item label="数量:" prop="quantity"> |
| | | <el-input-number |
| | | :step="0.1" |
| | |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-col :span="12" v-if="!isFrameworkContract"> |
| | | <el-form-item label="含税总价(元):" prop="taxInclusiveTotalPrice"> |
| | | <el-input |
| | | v-model="productForm.taxInclusiveTotalPrice" |
| | |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-col :span="12" v-if="!isFrameworkContract"> |
| | | <el-form-item |
| | | label="不含税总价(元):" |
| | | prop="taxExclusiveTotalPrice" |
| | |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-col :span="12" v-if="!isFrameworkContract"> |
| | | <el-form-item label="发票类型:" prop="invoiceType"> |
| | | <el-select |
| | | v-model="productForm.invoiceType" |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-col :span="12" v-if="!isFrameworkContract"> |
| | | <el-form-item label="是否生产:" prop="isProduction"> |
| | | <el-radio-group v-model="productForm.isProduction"> |
| | | <el-radio label="是" :value="true" /> |
| | |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 创建框架订单弹窗 --> |
| | | <el-dialog |
| | | v-model="frameworkOrderDialogVisible" |
| | | title="创建框架订单" |
| | | width="70%" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <el-form label-width="120px" label-position="top"> |
| | | <el-form-item label="父框架合同:"> |
| | | <el-input |
| | | :model-value="currentFrameworkContract?.salesContractNo" |
| | | disabled |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="备注:"> |
| | | <el-input |
| | | v-model="frameworkOrderForm.remarks" |
| | | type="textarea" |
| | | :rows="2" |
| | | placeholder="请输入备注" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="产品(填写交货日期):"> |
| | | <el-table :data="frameworkOrderProducts" border> |
| | | <el-table-column label="产品大类" prop="productCategory" /> |
| | | <el-table-column label="规格型号" prop="specificationModel" /> |
| | | <el-table-column label="单位" prop="unit" /> |
| | | <el-table-column label="交货日期" align="center"> |
| | | <template #default="scope"> |
| | | <el-date-picker |
| | | v-model="scope.row.deliveryDate" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | type="date" |
| | | placeholder="请选择" |
| | | clearable |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button type="primary" @click="submitFrameworkOrder" |
| | | >确认创建 |
| | | </el-button> |
| | | <el-button @click="frameworkOrderDialogVisible = false">取消</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 框架订单列表弹窗 --> |
| | | <el-dialog |
| | | v-model="frameworkOrderListVisible" |
| | | title="框架订单列表" |
| | | width="80%" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <el-alert |
| | | v-if="frameworkContractShippedQty !== null" |
| | | type="info" |
| | | :closable="false" |
| | | style="margin-bottom: 12px" |
| | | :title="'父框架合同累计已发货数量:' + frameworkContractShippedQty" |
| | | /> |
| | | <el-table :data="frameworkOrderList" border :row-key="(row) => row.id"> |
| | | <el-table-column type="expand"> |
| | | <template #default="props"> |
| | | <el-table :data="props.row.products" border size="small"> |
| | | <el-table-column label="产品大类" prop="productCategory" /> |
| | | <el-table-column label="规格型号" prop="specificationModel" /> |
| | | <el-table-column label="单位" prop="unit" /> |
| | | <el-table-column label="操作" width="80" align="center"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | @click="shipFrameworkOrderProduct(scope.row, props.row)" |
| | | >发货 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | label="订单编号" |
| | | prop="salesContractNo" |
| | | min-width="180" |
| | | /> |
| | | <el-table-column label="项目名称" prop="projectName" min-width="160" /> |
| | | <el-table-column label="备注" prop="remarks" min-width="160" /> |
| | | <el-table-column |
| | | label="累计发货数量" |
| | | prop="shippedQuantity" |
| | | width="140" |
| | | align="center" |
| | | /> |
| | | </el-table> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | delProduct, |
| | | delLedgerFile, |
| | | getProductInventory, |
| | | createFrameworkOrder, |
| | | listFrameworkOrders, |
| | | frameworkContractShippedQuantity, |
| | | } from "@/api/salesManagement/salesLedger.js"; |
| | | import { getStockInventoryByModelId } from "@/api/inventoryManagement/stockInventory.js"; |
| | | import { modelList, productTreeList } from "@/api/basicData/product.js"; |
| | |
| | | }, |
| | | form: { |
| | | salesContractNo: "", |
| | | contractType: 2, |
| | | autoGenerateContractNo: true, |
| | | salesman: "", |
| | | customerId: "", |
| | |
| | | customerId: [{ required: true, message: "请选择", trigger: "change" }], |
| | | entryPerson: [{ required: true, message: "请选择", trigger: "change" }], |
| | | entryDate: [{ required: true, message: "请选择", trigger: "change" }], |
| | | deliveryDate: [{ required: true, message: "请选择", trigger: "change" }], |
| | | deliveryDate: [ |
| | | { |
| | | validator: (rule, value, callback) => { |
| | | if (form.value.contractType === 2) return callback(); |
| | | if (!value) return callback(new Error("请选择")); |
| | | callback(); |
| | | }, |
| | | trigger: "change", |
| | | }, |
| | | ], |
| | | executionDate: [{ required: true, message: "请选择", trigger: "change" }], |
| | | }, |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | | const isFrameworkContract = computed(() => form.value.contractType === 2); |
| | | const frameworkOptional = (message, trigger = "blur") => ({ |
| | | validator: (_rule, value, callback) => { |
| | | if (isFrameworkContract.value) return callback(); |
| | | if (value === undefined || value === null || value === "") { |
| | | return callback(new Error(message)); |
| | | } |
| | | callback(); |
| | | }, |
| | | trigger, |
| | | }); |
| | | const { form: searchForm } = useFormData(data.searchForm); |
| | | // 产品表单弹框数据 |
| | | const productFormVisible = ref(false); |
| | |
| | | { required: true, message: "请选择", trigger: "change" }, |
| | | ], |
| | | unit: [{ required: true, message: "请输入", trigger: "blur" }], |
| | | quantity: [{ required: true, message: "请输入", trigger: "blur" }], |
| | | taxInclusiveUnitPrice: [ |
| | | { required: true, message: "请输入", trigger: "blur" }, |
| | | ], |
| | | taxRate: [{ required: true, message: "请选择", trigger: "change" }], |
| | | taxInclusiveTotalPrice: [ |
| | | { required: true, message: "请输入", trigger: "blur" }, |
| | | ], |
| | | taxExclusiveTotalPrice: [ |
| | | { required: true, message: "请输入", trigger: "blur" }, |
| | | ], |
| | | invoiceType: [{ required: true, message: "请选择", trigger: "change" }], |
| | | quantity: [frameworkOptional("请输入")], |
| | | taxInclusiveUnitPrice: [{ required: true, message: "请输入", trigger: "blur" }], |
| | | taxRate: [frameworkOptional("请选择", "change")], |
| | | taxInclusiveTotalPrice: [frameworkOptional("请输入")], |
| | | taxExclusiveTotalPrice: [frameworkOptional("请输入")], |
| | | invoiceType: [frameworkOptional("请选择", "change")], |
| | | isProduction: [{ required: true, message: "请选择", trigger: "change" }], |
| | | }, |
| | | }); |
| | |
| | | form.value.entryPerson = userStore.id; |
| | | if (type === "add") { |
| | | // 新增时设置录入日期为当天 |
| | | form.value.contractType = 2; |
| | | form.value.entryDate = getCurrentDate(); |
| | | // 签订日期默认为当天 |
| | | form.value.executionDate = getCurrentDate(); |
| | |
| | | } |
| | | form.value.storageBlobDTOs = fileList; |
| | | form.value.type = 1; |
| | | if (isFrameworkContract.value) { |
| | | form.value.productData = form.value.productData.map((item) => ({ |
| | | ...item, |
| | | quantity: null, |
| | | deliveryDate: null, |
| | | isProduction: false, |
| | | })); |
| | | } |
| | | if (form.value.autoGenerateContractNo) { |
| | | form.value.salesContractNo = ""; |
| | | } |
| | |
| | | deliveryFormVisible.value = false; |
| | | currentDeliveryRow.value = null; |
| | | }; |
| | | |
| | | // 框架合同 / 框架订单相关 |
| | | const frameworkOrderDialogVisible = ref(false); |
| | | const frameworkOrderListVisible = ref(false); |
| | | const currentFrameworkContract = ref(null); |
| | | const frameworkOrderForm = reactive({ |
| | | remarks: "", |
| | | }); |
| | | const frameworkOrderProducts = ref([]); |
| | | const frameworkOrderList = ref([]); |
| | | const frameworkContractShippedQty = ref(null); |
| | | |
| | | const openFrameworkOrderDialog = async (row) => { |
| | | currentFrameworkContract.value = row; |
| | | frameworkOrderForm.remarks = ""; |
| | | frameworkOrderProducts.value = []; |
| | | try { |
| | | const res = await productList({ salesLedgerId: row.id, type: 1 }); |
| | | frameworkOrderProducts.value = (res.data || []).map((p) => ({ |
| | | parentProductId: p.id, |
| | | productCategory: p.productCategory, |
| | | specificationModel: p.specificationModel, |
| | | unit: p.unit, |
| | | deliveryDate: "", |
| | | })); |
| | | } catch (e) { |
| | | frameworkOrderProducts.value = []; |
| | | } |
| | | frameworkOrderDialogVisible.value = true; |
| | | }; |
| | | |
| | | const submitFrameworkOrder = async () => { |
| | | const selected = frameworkOrderProducts.value.filter((p) => p.deliveryDate); |
| | | if (selected.length === 0) { |
| | | proxy.$modal.msgWarning("请至少为一个产品填写交货日期"); |
| | | return; |
| | | } |
| | | try { |
| | | await createFrameworkOrder({ |
| | | id: currentFrameworkContract.value.id, |
| | | remarks: frameworkOrderForm.remarks, |
| | | productData: selected.map((p) => ({ |
| | | parentProductId: p.parentProductId, |
| | | deliveryDate: p.deliveryDate, |
| | | })), |
| | | }); |
| | | proxy.$modal.msgSuccess("框架订单创建成功"); |
| | | frameworkOrderDialogVisible.value = false; |
| | | } catch (e) { |
| | | // 错误已由请求拦截器统一提示 |
| | | } |
| | | }; |
| | | |
| | | const openFrameworkOrderList = async (row) => { |
| | | currentFrameworkContract.value = row; |
| | | frameworkContractShippedQty.value = null; |
| | | frameworkOrderList.value = []; |
| | | frameworkOrderListVisible.value = true; |
| | | listFrameworkOrders({ parentContractId: row.id }).then(async (res) => { |
| | | const orders = res.data || []; |
| | | const withProducts = []; |
| | | for (const order of orders) { |
| | | let products = []; |
| | | try { |
| | | const productRes = await productList({ salesLedgerId: order.id, type: 1 }); |
| | | products = productRes.data || []; |
| | | } catch (e) { |
| | | products = []; |
| | | } |
| | | withProducts.push({ ...order, products }); |
| | | } |
| | | frameworkOrderList.value = withProducts; |
| | | }); |
| | | frameworkContractShippedQuantity({ parentContractId: row.id }).then((res) => { |
| | | frameworkContractShippedQty.value = res.data ?? 0; |
| | | }); |
| | | }; |
| | | |
| | | const shipFrameworkOrderProduct = (productRow, orderRow) => { |
| | | if (!orderRow || !orderRow.id) return; |
| | | frameworkOrderListVisible.value = false; |
| | | openDeliveryForm({ |
| | | ...productRow, |
| | | salesLedgerId: orderRow.id, |
| | | }); |
| | | }; |
| | | const currentFactoryName = ref(""); |
| | | const getCurrentFactoryName = async () => { |
| | | let res = await userStore.getInfo(); |