天津_建诚恒商贸
1.备件管理-新增独立备件出库功能入口,支持直接完成备件领用出库操作。
2.设备保养-支持按日期分组折叠展示,分组条目可展开查看明细;单条明细行末尾增加【保养完成】操作按钮,可单独办结单条保养任务;折叠汇总状态下,新增【一键保养完成】批量操作按钮,快速办结当前分组全部保养项。
3.设备巡检-按日期分组折叠展示,分组支持展开查看明细;每条巡检明细后增设【巡检完成】按钮,支持单独办结单条巡检记录;分组折叠未展开状态下,提供【一键巡检完成】批量操作,统一办结该分组下所有巡检任务。
4.增值税对比-优化数据展示逻辑,列表展示各订单增值税明细;页面右侧新增柱状可视化图表,直观对比进销项增值税数据。
5.管理驾驶舱-指标修改:将顶部「销售产品数」调整为「销售订单数」;时间筛选:页面左上角新增日期范围下拉筛选控件,所有统计指标标注当前筛选时段;
| | |
| | | }); |
| | | }; |
| | | |
| | | // 备件直接出库 |
| | | export function outboundSpareParts(data) { |
| | | return request({ |
| | | url: '/sparePartsRequisitionRecord/outbound', |
| | | method: 'post', |
| | | data |
| | | }) |
| | | } |
| | | |
| | |
| | | params: params, |
| | | }); |
| | | }; |
| | | // 单条保养完成 |
| | | export function completeMaintenance(id) { |
| | | return request({ |
| | | url: '/device/maintenance/complete/' + id, |
| | | method: 'put' |
| | | }) |
| | | } |
| | | |
| | | // 一键保养完成 |
| | | export function batchCompleteMaintenance(dateStr) { |
| | | return request({ |
| | | url: '/device/maintenance/batchComplete', |
| | | method: 'put', |
| | | params: { dateStr } |
| | | }) |
| | | } |
| | | |
| | | // 设备保养定时任务列表 |
| | | export const deviceMaintenanceTaskDel = (params) => { |
| | | return request({ |
| | |
| | | data: query |
| | | }) |
| | | } |
| | | |
| | | // 单条巡检完成 |
| | | export function completeInspection(id) { |
| | | return request({ |
| | | url: '/inspectionTask/complete/' + id, |
| | | method: 'put' |
| | | }) |
| | | } |
| | | |
| | | // 一键巡检完成 |
| | | export function batchCompleteInspection(dateStr) { |
| | | return request({ |
| | | url: '/inspectionTask/batchComplete', |
| | | method: 'put', |
| | | params: { dateStr } |
| | | }) |
| | | } |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // 分页查询 |
| | | // 分页查询(新增 type 参数) |
| | | export function getTaxList(query) { |
| | | return request({ |
| | | url: "/purchase/report/listVat", |
| | |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 增值税进销项图表数据 |
| | | export function getVatChart(year) { |
| | | return request({ |
| | | url: '/purchase/report/vatChart', |
| | | method: 'get', |
| | | params: { year } |
| | | }) |
| | | } |
| | | |
| | | // 增值税明细 |
| | | export function getVatDetail(query) { |
| | | return request({ |
| | | url: '/purchase/report/listVatDetail', |
| | | method: 'get', |
| | | params: query |
| | | }) |
| | | } |
| | |
| | | }; |
| | | |
| | | // 供应商采购排名 |
| | | export const supplierPurchaseRanking = (query) => { |
| | | export const supplierPurchaseRanking = (params) => { |
| | | return request({ |
| | | url: "/home/supplierPurchaseRanking", |
| | | method: "get", |
| | | params: query, |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | | // 客户金额贡献排名 |
| | | export const customerContributionRanking = (query) => { |
| | | export const customerContributionRanking = (params) => { |
| | | return request({ |
| | | url: "/home/customerContributionRanking", |
| | | method: "get", |
| | | params: query, |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | |
| | | }; |
| | | |
| | | // 产品销售金额分析 |
| | | export const productSalesAnalysis = () => { |
| | | export const productSalesAnalysis = (params) => { |
| | | return request({ |
| | | url: "/home/productSalesAnalysis", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | |
| | | }; |
| | | |
| | | // 原材料采购金额占比 |
| | | export const rawMaterialPurchaseAmountRatio = () => { |
| | | export const rawMaterialPurchaseAmountRatio = (params) => { |
| | | return request({ |
| | | url: "/home/rawMaterialPurchaseAmountRatio", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | |
| | | }; |
| | | |
| | | // 销售/采购/储存产品数 |
| | | export const salesPurchaseStorageProductCount = () => { |
| | | export const salesPurchaseStorageProductCount = (startDate, endDate) => { |
| | | return request({ |
| | | url: "/home/salesPurchaseStorageProductCount", |
| | | method: "get", |
| | | params: { startDate, endDate }, |
| | | }); |
| | | }; |
| | | |
| | |
| | | }; |
| | | |
| | | // 产品周转天数 |
| | | export const productTurnoverDays = () => { |
| | | export const productTurnoverDays = (params) => { |
| | | return request({ |
| | | url: "/home/productTurnoverDays", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | |
| | | :label="tab.label" |
| | | :value="tab.name" /> |
| | | </el-radio-group> |
| | | <!-- 操作按钮区 --> |
| | | <el-space v-if="activeRadio !== 'task'"> |
| | | <el-button type="primary" |
| | | :icon="Plus" |
| | |
| | | <el-button @click="handleOut">导出</el-button> |
| | | </el-space> |
| | | </div> |
| | | <div> |
| | | |
| | | <!-- 巡检任务 --> |
| | | <div v-show="activeRadio === 'taskManage'"> |
| | | <PIMTable :table-loading="tableLoading" |
| | | :table-data="tableData" |
| | | :column="tableColumns" |
| | |
| | | :table-style="{ width: '100%' }"> |
| | | <template #inspector="{ row }"> |
| | | <div class="person-tags"> |
| | | <!-- 调试信息,上线时删除 --> |
| | | <!-- {{ console.log('inspector data:', row.inspector) }} --> |
| | | <template v-if="row.inspector && row.inspector.length > 0"> |
| | | <el-tag v-for="(person, index) in row.inspector" |
| | | :key="index" |
| | |
| | | {{ person }} |
| | | </el-tag> |
| | | </template> |
| | | <span v-else |
| | | class="no-data">--</span> |
| | | <span v-else class="no-data">--</span> |
| | | </div> |
| | | </template> |
| | | <template #isEnabled="{ row }"> |
| | |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | |
| | | <!-- 巡检记录 - 日期分组折叠 --> |
| | | <div v-show="activeRadio === 'task'"> |
| | | <el-table |
| | | :data="groupedInspectionList" |
| | | border |
| | | v-loading="tableLoading" |
| | | :expand-row-keys="inspectionExpandedRowKeys" |
| | | :row-key="(row) => row.date" |
| | | @expand-change="onInspectionExpandChange" |
| | | height="calc(100vh - 23em)" |
| | | > |
| | | <el-table-column type="expand"> |
| | | <template #default="props"> |
| | | <el-table :data="props.row.children" border row-key="id"> |
| | | <el-table-column label="巡检任务名称" prop="taskName" align="center" min-width="180" /> |
| | | <el-table-column label="巡检项目" prop="inspectionProject" align="center" min-width="160" /> |
| | | <el-table-column label="巡检人" align="center" min-width="150"> |
| | | <template #default="{ row }"> |
| | | <template v-if="row.inspector && row.inspector.length > 0"> |
| | | <el-tag v-for="(p, i) in row.inspector" :key="i" size="small" type="primary" class="person-tag">{{ p }}</el-tag> |
| | | </template> |
| | | <span v-else>--</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="巡检状态" align="center" width="100"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="row.status === 1 ? 'success' : 'warning'" size="small"> |
| | | {{ row.status === 1 ? '已完成' : '待巡检' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="巡检结果" prop="inspectionResult" align="center" min-width="120" /> |
| | | <el-table-column label="备注" prop="remarks" align="center" min-width="150" /> |
| | | <el-table-column label="异常描述" prop="abnormalDescription" align="center" min-width="150" /> |
| | | <el-table-column label="操作" align="center" width="200"> |
| | | <template #default="{ row }"> |
| | | <el-button type="primary" link @click="openUploadDialog(row)">上传</el-button> |
| | | <el-button type="success" link @click="viewFile(row)">查看附件</el-button> |
| | | <el-button type="warning" link v-if="row.status === 0" @click="handleSingleComplete(row)">巡检完成</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="巡检日期" prop="date" align="center" /> |
| | | <el-table-column label="巡检记录数" prop="count" align="center" /> |
| | | <el-table-column label="待巡检数" align="center" width="120"> |
| | | <template #default="{ row }"> |
| | | {{ row.children.filter(c => c.status === 0).length }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" align="center" width="200"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | type="primary" |
| | | size="small" |
| | | :disabled="!row.children.some(c => c.status === 0)" |
| | | @click="handleBatchInspection(row.date)" |
| | | > |
| | | 一键巡检完成 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <Pagination |
| | | v-show="total > 0" |
| | | :total="total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page="pageNum" |
| | | :limit="pageSize" |
| | | @pagination="handlePagination" |
| | | /> |
| | | </div> |
| | | </el-card> |
| | | <form-dia ref="formDia" |
| | | @closeDia="handleQuery"></form-dia> |
| | |
| | | <upload-files ref="uploadFiles" |
| | | @success="handleQuery" |
| | | @closeDia="handleQuery"></upload-files> |
| | | |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Delete, Plus } from "@element-plus/icons-vue"; |
| | | import { onMounted, ref, reactive, getCurrentInstance, nextTick } from "vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { onMounted, ref, reactive, getCurrentInstance, nextTick, computed } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | // 组件引入 |
| | | import PIMTable from "@/components/PIMTable/PIMTable.vue"; |
| | | import FormDia from "@/views/equipmentManagement/inspectionManagement/components/formDia.vue"; |
| | | import UploadFiles from "@/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue"; |
| | | import ViewFiles from "@/views/equipmentManagement/inspectionManagement/components/viewFiles.vue"; |
| | | |
| | | // 接口引入 |
| | | import { |
| | | delTimingTask, |
| | | inspectionTaskList, |
| | | timingTaskList, |
| | | completeInspection, |
| | | batchCompleteInspection, |
| | | } from "@/api/inspectionManagement/index.js"; |
| | | import Pagination from "@/components/PIMTable/Pagination.vue"; |
| | | |
| | | // 全局变量 |
| | | const { proxy } = getCurrentInstance(); |
| | | const formDia = ref(); |
| | | const viewFiles = ref(); |
| | | const uploadFiles = ref(); |
| | | |
| | | // 查询参数 |
| | | const queryParams = reactive({ |
| | | taskName: "", |
| | | }); |
| | | const queryParams = reactive({ taskName: "" }); |
| | | |
| | | // 单选框配置 |
| | | const activeRadio = ref("taskManage"); |
| | | const radios = reactive([ |
| | | { name: "taskManage", label: "巡检任务" }, |
| | | { name: "task", label: "巡检记录" }, |
| | | ]); |
| | | |
| | | // 表格数据 |
| | | const selectedRows = ref([]); |
| | | const tableData = ref([]); |
| | | const operationsArr = ref([]); |
| | |
| | | const tableLoading = ref(false); |
| | | const total = ref(0); |
| | | const pageNum = ref(1); |
| | | const pageSize = ref(10); |
| | | const pageSize = ref(100); |
| | | |
| | | // 列配置 |
| | | const columns = ref([ |
| | | { prop: "taskName", label: "巡检任务名称", minWidth: 200 }, |
| | | { prop: "inspectionProject", label: "巡检项目", minWidth: 180 }, |
| | | { prop: "remarks", label: "备注", minWidth: 180 }, |
| | | { prop: "inspector", label: "执行巡检人", minWidth: 180, slot: "inspector" }, |
| | | { prop: "isEnabled", label: "是否启用", minWidth: 100, dataType: "slot", slot: "isEnabled" }, |
| | | { |
| | | prop: "isEnabled", |
| | | label: "是否启用", |
| | | minWidth: 100, |
| | | dataType: "slot", |
| | | slot: "isEnabled", |
| | | prop: "frequencyType", label: "频次", minWidth: 120, |
| | | formatData: params => ({ DAILY: "每日", WEEKLY: "每周", MONTHLY: "每月", QUARTERLY: "季度" }[params] || ""), |
| | | }, |
| | | { |
| | | prop: "frequencyType", |
| | | label: "频次", |
| | | minWidth: 120, |
| | | formatData: params => { |
| | | return params === "DAILY" |
| | | ? "每日" |
| | | : params === "WEEKLY" |
| | | ? "每周" |
| | | : params === "MONTHLY" |
| | | ? "每月" |
| | | : params === "QUARTERLY" |
| | | ? "季度" |
| | | : ""; |
| | | }, |
| | | }, |
| | | { |
| | | prop: "frequencyDetail", |
| | | label: "开始日期与时间", |
| | | minWidth: 200, |
| | | prop: "frequencyDetail", label: "开始日期与时间", minWidth: 200, |
| | | formatter: (row, column, cellValue) => { |
| | | // 先判断是否是字符串 |
| | | if (typeof cellValue !== "string") return ""; |
| | | let val = cellValue; |
| | | const replacements = { |
| | | MON: "周一", |
| | | TUE: "周二", |
| | | WED: "周三", |
| | | THU: "周四", |
| | | FRI: "周五", |
| | | SAT: "周六", |
| | | SUN: "周日", |
| | | }; |
| | | // 使用正则一次性替换所有匹配项 |
| | | return val.replace( |
| | | /MON|TUE|WED|THU|FRI|SAT|SUN/g, |
| | | match => replacements[match] |
| | | ); |
| | | return cellValue.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, match => ({ MON: "周一", TUE: "周二", WED: "周三", THU: "周四", FRI: "周五", SAT: "周六", SUN: "周日" }[match])); |
| | | }, |
| | | }, |
| | | { prop: "registrant", label: "登记人", minWidth: 120 }, |
| | | { |
| | | prop: "createTime", |
| | | label: "登记日期", |
| | | minWidth: 180, |
| | | formatData: cell => { |
| | | if (!cell) return "-"; |
| | | try { |
| | | return dayjs(cell).format("YYYY-MM-DD HH:mm:ss"); |
| | | } catch { |
| | | return cell; |
| | | } |
| | | prop: "createTime", label: "登记日期", minWidth: 180, |
| | | formatData: cell => { if (!cell) return "-"; try { return dayjs(cell).format("YYYY-MM-DD HH:mm:ss"); } catch { return cell; } }, |
| | | }, |
| | | }, |
| | | // { |
| | | // prop: "inspectionResult", |
| | | // label: "巡检结果", |
| | | // minWidth: 100, |
| | | // dataType: "tag", |
| | | // formatData: val => { |
| | | // return val == 1 ? "正常" : "异常"; |
| | | // }, |
| | | // formatType: val => { |
| | | // return val == 1 ? "success" : "danger"; |
| | | // }, |
| | | // }, |
| | | { prop: "abnormalDescription", label: "异常描述", minWidth: 150 }, |
| | | ]); |
| | | |
| | | // 操作列配置 |
| | | const getOperationColumn = operations => { |
| | | if (!operations || operations.length === 0) return null; |
| | | |
| | | const operationConfig = { |
| | | label: "操作", |
| | | width: operations.length > 1 ? 180 : 130, |
| | | fixed: "right", |
| | | align: "center", |
| | | dataType: "action", |
| | | operation: operations |
| | | .map(op => { |
| | | return { |
| | | label: "操作", width: operations.length > 1 ? 180 : 130, fixed: "right", align: "center", dataType: "action", |
| | | operation: operations.map(op => { |
| | | switch (op) { |
| | | case "edit": |
| | | return { |
| | | name: "编辑", |
| | | clickFun: handleAdd, |
| | | color: "#409EFF", |
| | | }; |
| | | case "upload": |
| | | return { |
| | | name: "上传", |
| | | clickFun: openUploadDialog, |
| | | color: "#409EFF", |
| | | }; |
| | | case "viewFile": |
| | | return { |
| | | name: "查看附件", |
| | | clickFun: viewFile, |
| | | color: "#67C23A", |
| | | }; |
| | | default: |
| | | return null; |
| | | case "edit": return { name: "编辑", clickFun: handleAdd, color: "#409EFF" }; |
| | | case "upload": return { name: "上传", clickFun: openUploadDialog, color: "#409EFF" }; |
| | | case "viewFile": return { name: "查看附件", clickFun: viewFile, color: "#67C23A" }; |
| | | default: return null; |
| | | } |
| | | }) |
| | | .filter(Boolean), |
| | | }).filter(Boolean), |
| | | }; |
| | | }; |
| | | |
| | | return operationConfig; |
| | | }; |
| | | // 巡检记录 - 日期分组 |
| | | const inspectionExpandedRowKeys = ref([]); |
| | | |
| | | onMounted(() => { |
| | | radioChange("taskManage"); |
| | | const groupInspectionByDate = (list) => { |
| | | const map = {}; |
| | | list.forEach(item => { |
| | | const date = item.dateStr || (item.createTime ? dayjs(item.createTime).format('YYYY-MM-DD') : '未知日期'); |
| | | if (!map[date]) map[date] = []; |
| | | map[date].push(item); |
| | | }); |
| | | return Object.entries(map).map(([date, children]) => ({ date, children, count: children.length })); |
| | | }; |
| | | |
| | | // 单选变化 |
| | | const groupedInspectionList = computed(() => groupInspectionByDate(tableData.value)); |
| | | |
| | | const onInspectionExpandChange = (row, expandedRows) => { |
| | | inspectionExpandedRowKeys.value = expandedRows.map(r => r.date); |
| | | }; |
| | | |
| | | // 单条巡检完成 |
| | | const handleSingleComplete = async (row) => { |
| | | try { |
| | | await ElMessageBox.confirm('确认将该巡检记录标记为完成?', '提示', { type: 'warning' }); |
| | | const res = await completeInspection(row.id); |
| | | if (res.code === 200) { |
| | | ElMessage.success('巡检完成'); |
| | | getList(); |
| | | } else { |
| | | ElMessage.error(res.msg || '操作失败'); |
| | | } |
| | | } catch { /* 取消 */ } |
| | | }; |
| | | |
| | | // 一键巡检完成 |
| | | const handleBatchInspection = async (dateStr) => { |
| | | try { |
| | | await ElMessageBox.confirm(`确认将 ${dateStr} 的所有待巡检记录标记为完成?`, '提示', { type: 'warning' }); |
| | | const res = await batchCompleteInspection(dateStr); |
| | | if (res.code === 200) { |
| | | ElMessage.success(res.msg || '批量完成成功'); |
| | | getList(); |
| | | } else { |
| | | ElMessage.error(res.msg || '操作失败'); |
| | | } |
| | | } catch { /* 取消 */ } |
| | | }; |
| | | |
| | | onMounted(() => { radioChange("taskManage"); }); |
| | | |
| | | const radioChange = value => { |
| | | inspectionExpandedRowKeys.value = []; |
| | | if (value === "taskManage") { |
| | | const operationColumn = getOperationColumn(["edit"]); |
| | | tableColumns.value = [ |
| | | ...columns.value, |
| | | ...(operationColumn ? [operationColumn] : []), |
| | | ]; |
| | | tableColumns.value = [...columns.value, ...(operationColumn ? [operationColumn] : [])]; |
| | | operationsArr.value = ["edit"]; |
| | | } else if (value === "task") { |
| | | const operationColumn = getOperationColumn(["upload", "viewFile"]); |
| | | // 巡检记录不展示"是否启用"列 |
| | | const taskColumns = columns.value.filter(col => col.prop !== "isEnabled"); |
| | | tableColumns.value = [ |
| | | ...taskColumns, |
| | | ...(operationColumn ? [operationColumn] : []), |
| | | ]; |
| | | tableColumns.value = [...taskColumns, ...(operationColumn ? [operationColumn] : [])]; |
| | | operationsArr.value = ["upload", "viewFile"]; |
| | | } |
| | | pageNum.value = 1; |
| | | pageSize.value = 10; |
| | | getList(); |
| | | }; |
| | | |
| | | // 查询操作 |
| | | const handleQuery = () => { |
| | | pageNum.value = 1; |
| | | pageSize.value = 10; |
| | | getList(); |
| | | }; |
| | | // 分页处理 |
| | | const handlePagination = val => { |
| | | pageNum.value = val.page; |
| | | pageSize.value = val.limit; |
| | | getList(); |
| | | }; |
| | | // 获取列表数据 |
| | | const handleQuery = () => { pageNum.value = 1; getList(); }; |
| | | |
| | | const handlePagination = val => { pageNum.value = val.page; pageSize.value = val.limit; getList(); }; |
| | | |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | |
| | | const params = { |
| | | ...queryParams, |
| | | size: pageSize.value, |
| | | current: pageNum.value, |
| | | }; |
| | | |
| | | let apiCall; |
| | | if (activeRadio.value === "task") { |
| | | apiCall = inspectionTaskList(params); |
| | | } else { |
| | | apiCall = timingTaskList(params); |
| | | } |
| | | |
| | | apiCall |
| | | .then(res => { |
| | | const params = { ...queryParams, size: pageSize.value, current: pageNum.value }; |
| | | const apiCall = activeRadio.value === "task" ? inspectionTaskList(params) : timingTaskList(params); |
| | | apiCall.then(res => { |
| | | const rawData = res.data.records || []; |
| | | // 处理 inspector 字段,将字符串转换为数组(适用于所有情况) |
| | | tableData.value = rawData.map(item => { |
| | | const processedItem = { ...item }; |
| | | processedItem.__raw = { ...item }; |
| | | |
| | | // 处理 inspector 字段 |
| | | if (processedItem.inspector) { |
| | | if (typeof processedItem.inspector === "string") { |
| | | // 字符串按逗号分割 |
| | | processedItem.inspector = processedItem.inspector |
| | | .split(",") |
| | | .map(s => s.trim()) |
| | | .filter(s => s); |
| | | processedItem.inspector = processedItem.inspector.split(",").map(s => s.trim()).filter(s => s); |
| | | } else if (!Array.isArray(processedItem.inspector)) { |
| | | // 非数组转为数组 |
| | | processedItem.inspector = [processedItem.inspector]; |
| | | } |
| | | } else { |
| | | // 空值设为空数组 |
| | | processedItem.inspector = []; |
| | | } |
| | | |
| | | return processedItem; |
| | | }); |
| | | total.value = res.data.total || 0; |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }).finally(() => { tableLoading.value = false; }); |
| | | }; |
| | | |
| | | // 重置查询 |
| | | const resetQuery = () => { |
| | | for (const key in queryParams) { |
| | | if (!["pageNum", "pageSize"].includes(key)) { |
| | | queryParams[key] = ""; |
| | | } |
| | | } |
| | | handleQuery(); |
| | | }; |
| | | const resetQuery = () => { queryParams.taskName = ""; handleQuery(); }; |
| | | |
| | | // 新增 / 编辑 |
| | | const handleAdd = row => { |
| | | const type = row ? "edit" : "add"; |
| | | nextTick(() => { |
| | | formDia.value?.openDialog(type, row); |
| | | }); |
| | | }; |
| | | const handleAdd = row => { nextTick(() => { formDia.value?.openDialog(row ? "edit" : "add", row); }); }; |
| | | const viewFile = row => { nextTick(() => { viewFiles.value?.openDialog(row); }); }; |
| | | const openUploadDialog = row => { nextTick(() => { uploadFiles.value?.openDialog(row); }); }; |
| | | |
| | | // 查看附件 |
| | | const viewFile = row => { |
| | | nextTick(() => { |
| | | viewFiles.value?.openDialog(row); |
| | | }); |
| | | }; |
| | | |
| | | const openUploadDialog = row => { |
| | | nextTick(() => { |
| | | uploadFiles.value?.openDialog(row); |
| | | }); |
| | | }; |
| | | |
| | | // 删除操作 |
| | | const handleDelete = () => { |
| | | if (!selectedRows.value.length) { |
| | | proxy.$modal.msgWarning("请选择要删除的数据"); |
| | | return; |
| | | } |
| | | |
| | | const deleteIds = selectedRows.value.map(item => item.id); |
| | | |
| | | proxy.$modal |
| | | .confirm("是否确认删除所选数据项?") |
| | | .then(() => { |
| | | return delTimingTask(deleteIds); |
| | | }) |
| | | .then(() => { |
| | | proxy.$modal.msgSuccess("删除成功"); |
| | | handleQuery(); |
| | | }) |
| | | .catch(() => {}); |
| | | if (!selectedRows.value.length) { proxy.$modal.msgWarning("请选择要删除的数据"); return; } |
| | | proxy.$modal.confirm("是否确认删除所选数据项?").then(() => delTimingTask(selectedRows.value.map(item => item.id))) |
| | | .then(() => { proxy.$modal.msgSuccess("删除成功"); handleQuery(); }).catch(() => {}); |
| | | }; |
| | | |
| | | // 多选变更 |
| | | const handleSelectionChange = selection => { |
| | | selectedRows.value = selection; |
| | | }; |
| | | const handleSelectionChange = selection => { selectedRows.value = selection; }; |
| | | |
| | | // 导出 |
| | | const handleOut = () => { |
| | | ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | }) |
| | | ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", { confirmButtonText: "确认", cancelButtonText: "取消", type: "warning" }) |
| | | .then(() => { |
| | | // 根据当前选中的标签页调用不同的导出接口 |
| | | if (activeRadio.value === "taskManage") { |
| | | // 巡检任务 |
| | | proxy.download("/timingTask/export", {}, "巡检任务.xlsx"); |
| | | } else if (activeRadio.value === "task") { |
| | | // 巡检记录 |
| | | proxy.download("/inspectionTask/export", {}, "巡检记录.xlsx"); |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已取消"); |
| | | }); |
| | | if (activeRadio.value === "taskManage") proxy.download("/timingTask/export", {}, "巡检任务.xlsx"); |
| | | else proxy.download("/inspectionTask/export", {}, "巡检记录.xlsx"); |
| | | }).catch(() => { proxy.$modal.msg("已取消"); }); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .person-tags { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .person-tag { |
| | | margin-right: 4px; |
| | | margin-bottom: 2px; |
| | | } |
| | | |
| | | .no-data { |
| | | color: #909399; |
| | | font-size: 14px; |
| | | } |
| | | .person-tags { display: flex; flex-wrap: wrap; gap: 4px; } |
| | | .person-tag { margin-right: 4px; margin-bottom: 2px; } |
| | | .no-data { color: #909399; font-size: 14px; } |
| | | </style> |
| | |
| | | <el-select v-model="usageQuery.sourceType" placeholder="请选择" clearable style="width: 200px"> |
| | | <el-option label="维修" :value="0" /> |
| | | <el-option label="保养" :value="1" /> |
| | | <el-option label="直接出库" :value="2" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | |
| | | <el-button @click="resetUsageQuery">重置</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div> |
| | | <el-button type="primary" @click="openRequisitionDialog">直接出库</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <PIMTable |
| | |
| | | </div> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | |
| | | <!-- 直接出库弹窗 --> |
| | | <el-dialog title="直接出库" v-model="requisitionDialogVisible" width="500px"> |
| | | <el-form :model="requisitionForm" :rules="requisitionRules" ref="requisitionFormRef" label-width="100px"> |
| | | <el-form-item label="设备选择" prop="deviceLedgerId"> |
| | | <el-select |
| | | v-model="requisitionForm.deviceLedgerId" |
| | | placeholder="请选择设备" |
| | | filterable |
| | | clearable |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in requisitionDeviceOptions" |
| | | :key="item.id" |
| | | :label="item.deviceName" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="备件选择" prop="sparePartsId"> |
| | | <el-select |
| | | v-model="requisitionForm.sparePartsId" |
| | | placeholder="请选择备件" |
| | | filterable |
| | | clearable |
| | | style="width: 100%" |
| | | @change="onRequisitionSparePartChange" |
| | | > |
| | | <el-option |
| | | v-for="item in requisitionSparePartsOptions" |
| | | :key="item.id" |
| | | :label="`${item.name}(库存: ${item.quantity})`" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="领用数量" prop="quantity"> |
| | | <el-input-number |
| | | v-model="requisitionForm.quantity" |
| | | :min="1" |
| | | :max="selectedSparePartMaxQty" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="备注" prop="remark"> |
| | | <el-input v-model="requisitionForm.remark" placeholder="请输入备注" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button type="primary" @click="submitRequisition" :loading="requisitionLoading">确定</el-button> |
| | | <el-button @click="requisitionDialogVisible = false">取消</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import { getSparePartsList, addSparePart, editSparePart, delSparePart } from "@/api/equipmentManagement/spareParts"; |
| | | import { getDeviceLedger } from "@/api/equipmentManagement/ledger"; |
| | | import PIMTable from "@/components/PIMTable/PIMTable.vue"; |
| | | import { getSparePartsUsagePage } from "@/api/equipmentManagement/sparePartsUsage"; |
| | | import { getSparePartsUsagePage, outboundSpareParts } from "@/api/equipmentManagement/sparePartsUsage"; |
| | | |
| | | // 加载状态 |
| | | const loading = ref(false); |
| | |
| | | } |
| | | }; |
| | | |
| | | // 直接出库 |
| | | const requisitionDialogVisible = ref(false); |
| | | const requisitionLoading = ref(false); |
| | | const requisitionFormRef = ref(null); |
| | | const requisitionDeviceOptions = ref([]); |
| | | const requisitionSparePartsOptions = ref([]); |
| | | const selectedSparePartMaxQty = ref(1); |
| | | const requisitionForm = reactive({ |
| | | deviceLedgerId: null, |
| | | sparePartsId: null, |
| | | quantity: 1, |
| | | remark: '', |
| | | }); |
| | | const requisitionRules = { |
| | | sparePartsId: [{ required: true, message: '请选择备件', trigger: 'change' }], |
| | | quantity: [{ required: true, message: '请输入领用数量', trigger: 'blur' }], |
| | | }; |
| | | |
| | | const loadRequisitionOptions = async () => { |
| | | try { |
| | | const [deviceRes, spareRes] = await Promise.all([ |
| | | getDeviceLedger(), |
| | | getSparePartsList({ current: 1, size: 9999 }), |
| | | ]); |
| | | requisitionDeviceOptions.value = deviceRes.data || []; |
| | | requisitionSparePartsOptions.value = spareRes.data?.records || []; |
| | | } catch (e) { |
| | | ElMessage.error('加载选项失败'); |
| | | } |
| | | }; |
| | | |
| | | const openRequisitionDialog = async () => { |
| | | requisitionForm.deviceLedgerId = null; |
| | | requisitionForm.sparePartsId = null; |
| | | requisitionForm.quantity = 1; |
| | | requisitionForm.remark = ''; |
| | | selectedSparePartMaxQty.value = 1; |
| | | await loadRequisitionOptions(); |
| | | requisitionDialogVisible.value = true; |
| | | }; |
| | | |
| | | const onRequisitionSparePartChange = (val) => { |
| | | const selected = requisitionSparePartsOptions.value.find(item => item.id === val); |
| | | selectedSparePartMaxQty.value = selected ? selected.quantity : 1; |
| | | if (requisitionForm.quantity > selectedSparePartMaxQty.value) { |
| | | requisitionForm.quantity = selectedSparePartMaxQty.value; |
| | | } |
| | | }; |
| | | |
| | | const submitRequisition = async () => { |
| | | if (!requisitionFormRef.value) return; |
| | | try { |
| | | await requisitionFormRef.value.validate(); |
| | | } catch { |
| | | return; |
| | | } |
| | | if (requisitionForm.quantity <= 0) { |
| | | ElMessage.warning('领用数量必须大于0'); |
| | | return; |
| | | } |
| | | const selected = requisitionSparePartsOptions.value.find(item => item.id === requisitionForm.sparePartsId); |
| | | if (selected && requisitionForm.quantity > selected.quantity) { |
| | | ElMessage.warning(`库存不足,当前库存为 ${selected.quantity}`); |
| | | return; |
| | | } |
| | | requisitionLoading.value = true; |
| | | try { |
| | | const res = await outboundSpareParts({ |
| | | deviceLedgerId: requisitionForm.deviceLedgerId || undefined, |
| | | sparePartsId: requisitionForm.sparePartsId, |
| | | quantity: requisitionForm.quantity, |
| | | remark: requisitionForm.remark || undefined, |
| | | }); |
| | | if (res.code === 200) { |
| | | ElMessage.success('领用成功'); |
| | | requisitionDialogVisible.value = false; |
| | | fetchUsageData(); |
| | | } else { |
| | | ElMessage.error(res.msg || '领用失败'); |
| | | } |
| | | } catch { |
| | | ElMessage.error('领用失败'); |
| | | } finally { |
| | | requisitionLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // 组件挂载时获取列表数据 |
| | | onMounted(() => { |
| | | fetchListData(); |
| | |
| | | </el-button> |
| | | <el-button type="danger" |
| | | icon="Delete" |
| | | :disabled="multipleList.length <= 0 || hasFinishedStatus" |
| | | :disabled="multipleList.length <= 0" |
| | | @click="delRepairByIds(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, |
| | | }" |
| | | <el-table |
| | | :data="groupedDataList" |
| | | border |
| | | v-loading="tableLoading" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination="changePage"> |
| | | <template #maintenanceResultRef="{ row }"> |
| | | <div>{{ row.maintenanceResult || '-' }}</div> |
| | | </template> |
| | | <template #statusRef="{ row }"> |
| | | <el-tag v-if="row.status === 2" |
| | | type="danger">失败</el-tag> |
| | | <el-tag v-if="row.status === 1" |
| | | type="success">完结</el-tag> |
| | | <el-tag v-if="row.status === 0" |
| | | type="warning">待保养</el-tag> |
| | | </template> |
| | | <template #operation="{ row }"> |
| | | <!-- 这个功能跟新增保养功能一模一样,有啥意义? --> |
| | | <!-- <el-button |
| | | type="primary" |
| | | text |
| | | @click="addMaintain(row)" |
| | | :expand-row-keys="expandedRowKeys" |
| | | :row-key="(row) => row.date" |
| | | @expand-change="onExpandChange" |
| | | > |
| | | 新增保养 |
| | | </el-button> --> |
| | | <el-button type="primary" |
| | | link |
| | | :disabled="row.status === 1" |
| | | @click="editPlan(row.id)"> |
| | | 编辑 |
| | | </el-button> |
| | | <el-button type="success" |
| | | link |
| | | :disabled="row.status === 1" |
| | | @click="addMaintain(row)"> |
| | | 保养 |
| | | </el-button> |
| | | <el-button type="danger" |
| | | link |
| | | :disabled="row.status === 1" |
| | | @click="delRepairByIds(row.id)"> |
| | | 删除 |
| | | </el-button> |
| | | <el-button type="primary" |
| | | link |
| | | @click="openFileDialog(row)"> |
| | | 附件 |
| | | <el-table-column type="selection" width="55" /> |
| | | <el-table-column type="expand"> |
| | | <template #default="props"> |
| | | <el-table :data="props.row.children" border row-key="id"> |
| | | <el-table-column label="设备名称" prop="deviceName" align="center" /> |
| | | <el-table-column label="规格型号" prop="deviceModel" align="center" /> |
| | | <el-table-column label="录入人" prop="createUserName" align="center" /> |
| | | <el-table-column label="保养项目" prop="machineryCategory" align="center"> |
| | | <template #default="{ row }"> |
| | | {{ row.machineryCategory || '--' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="实际保养人" prop="maintenanceActuallyName" align="center" /> |
| | | <el-table-column label="实际保养日期" align="center"> |
| | | <template #default="{ row }"> |
| | | {{ row.maintenanceActuallyTime ? dayjs(row.maintenanceActuallyTime).format('YYYY-MM-DD HH:mm:ss') : '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="保养结果" align="center"> |
| | | <template #default="{ row }"> |
| | | {{ row.maintenanceResult || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="状态" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-tag v-if="row.status === 2" type="danger">失败</el-tag> |
| | | <el-tag v-if="row.status === 1" type="success">完结</el-tag> |
| | | <el-tag v-if="row.status === 0" type="warning">待保养</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" align="center" width="300"> |
| | | <template #default="{ row }"> |
| | | <el-button type="primary" link :disabled="row.status === 1" @click="editPlan(row.id)">编辑</el-button> |
| | | <el-button type="success" link :disabled="row.status !== 0" @click="handleSingleComplete(row.id)">保养完成</el-button> |
| | | <el-button type="success" link :disabled="row.status === 1" @click="addMaintain(row)">保养</el-button> |
| | | <el-button type="danger" link :disabled="row.status === 1" @click="delRepairByIds(row.id)">删除</el-button> |
| | | <el-button type="primary" link @click="openFileDialog(row)">附件</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="计划保养日期" prop="date" align="center" /> |
| | | <el-table-column label="保养记录数" prop="count" align="center" /> |
| | | <el-table-column label="待保养数" align="center" width="120"> |
| | | <template #default="{ row }"> |
| | | {{ row.children.filter(c => c.status === 0).length }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" align="center" width="200"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | type="primary" |
| | | size="small" |
| | | :disabled="!row.children.some(c => c.status === 0)" |
| | | @click="handleBatchComplete(row.date)" |
| | | > |
| | | 一键保养完成 |
| | | </el-button> |
| | | </template> |
| | | </PIMTable> |
| | | </el-table-column> |
| | | </el-table> |
| | | <Pagination |
| | | v-show="pagination.total > 0" |
| | | :total="pagination.total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page="pagination.currentPage" |
| | | :limit="pagination.pageSize" |
| | | @pagination="changePage" |
| | | /> |
| | | </div> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | |
| | | v-model:visible="fileDialogVisible" |
| | | :record-type="'device_maintenance'" |
| | | :record-id="currentMaintenanceTaskId" /> |
| | | |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | delUpkeep, |
| | | deviceMaintenanceTaskList, |
| | | deviceMaintenanceTaskDel, |
| | | batchCompleteMaintenance, |
| | | completeMaintenance, |
| | | } from "@/api/equipmentManagement/upkeep"; |
| | | import dayjs from "dayjs"; |
| | | import Pagination from "@/components/PIMTable/Pagination.vue"; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | const FileList = defineAsyncComponent(() => |
| | |
| | | const dataList = ref([]); |
| | | const pagination = ref({ |
| | | currentPage: 1, |
| | | pageSize: 10, |
| | | pageSize: 100, |
| | | total: 0, |
| | | }); |
| | | const multipleList = ref([]); |
| | | const tableLoading = ref(false); |
| | | const expandedRowKeys = ref([]); |
| | | |
| | | const groupByDate = (list) => { |
| | | const map = {}; |
| | | list.forEach(item => { |
| | | const date = item.dateStr || (item.maintenancePlanTime ? dayjs(item.maintenancePlanTime).format('YYYY-MM-DD') : (item.createTime ? dayjs(item.createTime).format('YYYY-MM-DD') : '未知日期')); |
| | | if (!map[date]) map[date] = []; |
| | | map[date].push(item); |
| | | }); |
| | | return Object.entries(map).map(([date, children]) => ({ date, children, count: children.length })); |
| | | }; |
| | | |
| | | const groupedDataList = computed(() => groupByDate(dataList.value)); |
| | | |
| | | // 单条保养完成 |
| | | const handleSingleComplete = async (id) => { |
| | | try { |
| | | await ElMessageBox.confirm('确认将该保养记录标记为完成?', '提示', { type: 'warning' }); |
| | | const res = await completeMaintenance(id); |
| | | if (res.code === 200) { |
| | | ElMessage.success('保养完成'); |
| | | getTableData(); |
| | | } else { |
| | | ElMessage.error(res.msg || '操作失败'); |
| | | } |
| | | } catch { /* 取消 */ } |
| | | }; |
| | | |
| | | // 一键保养完成 |
| | | const handleBatchComplete = async (dateStr) => { |
| | | try { |
| | | await ElMessageBox.confirm(`确认将 ${dateStr} 的所有待保养记录标记为完成?`, '提示', { type: 'warning' }); |
| | | const res = await batchCompleteMaintenance(dateStr); |
| | | if (res.code === 200) { |
| | | ElMessage.success(res.msg || '批量保养完成成功'); |
| | | getTableData(); |
| | | } else { |
| | | ElMessage.error(res.msg || '批量保养完成失败'); |
| | | } |
| | | } catch { /* 取消 */ } |
| | | }; |
| | | |
| | | const onExpandChange = (row, expandedRows) => { |
| | | expandedRowKeys.value = expandedRows.map(r => r.date); |
| | | }; |
| | | |
| | | // 保养任务tab相关变量 |
| | | const scheduledFilters = reactive({ |
| | |
| | | |
| | | // 保养记录相关方法(原设备保养页面方法) |
| | | const getTableData = async () => { |
| | | tableLoading.value = true; |
| | | try { |
| | | const params = { |
| | | current: pagination.value.currentPage, |
| | |
| | | } |
| | | } catch (error) { |
| | | console.log(error); |
| | | } finally { |
| | | tableLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | |
| | | }; |
| | | |
| | | const handleSelectionChange = selection => { |
| | | multipleList.value = selection; |
| | | }; |
| | | |
| | | // 检查选中的记录中是否有完结状态的 |
| | | const hasFinishedStatus = computed(() => { |
| | | return multipleList.value.some(item => item.status === 1); |
| | | // 从选中的分组行中收集所有子记录ID |
| | | const ids = []; |
| | | selection.forEach(group => { |
| | | if (group.children) { |
| | | group.children.forEach(child => ids.push(child.id)); |
| | | } |
| | | }); |
| | | multipleList.value = ids; |
| | | }; |
| | | |
| | | const changePage = page => { |
| | | pagination.value.currentPage = page.page; |
| | |
| | | }; |
| | | |
| | | const delRepairByIds = async ids => { |
| | | // 检查是否有完结状态的记录 |
| | | const hasFinished = multipleList.value.some(item => item.status === 1); |
| | | if (hasFinished) { |
| | | ElMessage.warning("不能删除状态为完结的记录"); |
| | | return; |
| | | } |
| | | const idList = Array.isArray(ids) ? ids : [ids]; |
| | | if (idList.length === 0) return; |
| | | |
| | | try { |
| | | await ElMessageBox.confirm("确认删除保养数据, 此操作不可逆?", "警告", { |
| | |
| | | type: "warning", |
| | | }); |
| | | |
| | | const { code } = await delUpkeep(ids); |
| | | const { code } = await delUpkeep(idList); |
| | | if (code === 200) { |
| | | ElMessage.success("删除成功"); |
| | | getTableData(); |
| | |
| | | }, |
| | | } |
| | | |
| | | const dateRange = inject('psiDateRange', null) |
| | | |
| | | const fetchData = () => { |
| | | productInOutAnalysis({ type: productType.value }) |
| | | const params = { type: productType.value, ...(dateRange?.value || {}) } |
| | | productInOutAnalysis(params) |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | const list = res.data |
| | |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import { productTurnoverDays } from '@/api/viewIndex.js' |
| | | |
| | | const dateRange = inject('psiDateRange', null) |
| | | |
| | | const chartStyle = { width: '100%', height: '100%' } |
| | | const grid = { left: '3%', right: '4%', bottom: '3%', top: '4%', containLabel: true } |
| | | const barLegend = { show: false, textStyle: { color: '#B8C8E0' }, data: ['周转天数'] } |
| | |
| | | const yAxis1 = [{ type: 'value', axisLabel: { color: '#B8C8E0' } }] |
| | | |
| | | const fetchData = () => { |
| | | productTurnoverDays() |
| | | productTurnoverDays(dateRange?.value || undefined) |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | const list = res.data |
| | |
| | | |
| | | const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down') |
| | | |
| | | const dateRange = inject('psiDateRange', null) |
| | | |
| | | const fetchData = () => { |
| | | salesPurchaseStorageProductCount() |
| | | const params = dateRange?.value || {} |
| | | salesPurchaseStorageProductCount(params.startDate, params.endDate) |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | statItems.value = res.data.map((item) => ({ |
| | | name: item.name, |
| | | name: item.name === '销售产品数' ? '销售订单数' : item.name, |
| | | value: item.value, |
| | | rate: item.rate, |
| | | })) |
| | |
| | | import { rawMaterialPurchaseAmountRatio } from '@/api/viewIndex.js' |
| | | import { useChartBackground } from '@/hooks/useChartBackground.js' |
| | | |
| | | const dateRange = inject('psiDateRange', null) |
| | | |
| | | const pieWrapperRef = ref(null) |
| | | const pieBackgroundRef = ref(null) |
| | | |
| | |
| | | }) |
| | | |
| | | const fetchData = () => { |
| | | rawMaterialPurchaseAmountRatio() |
| | | rawMaterialPurchaseAmountRatio(dateRange?.value || undefined) |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | const items = res.data |
| | |
| | | import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue' |
| | | import { productSalesAnalysis } from '@/api/viewIndex.js' |
| | | import PanelHeader from './PanelHeader.vue' |
| | | |
| | | const dateRange = inject('psiDateRange', null) |
| | | import CarouselCards from './CarouselCards.vue' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import { useChartBackground } from '@/hooks/useChartBackground.js' |
| | |
| | | }) |
| | | |
| | | const fetchData = () => { |
| | | productSalesAnalysis() |
| | | productSalesAnalysis(dateRange?.value || undefined) |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | const items = res.data |
| | |
| | | |
| | | <!-- 顶部标题栏 --> |
| | | <div class="dashboard-header"> |
| | | <div class="header-left"></div> |
| | | <div class="factory-name">PSI 数据分析</div> |
| | | <div class="header-right"> |
| | | <div class="date-picker-wrapper"> |
| | | <el-date-picker |
| | | v-model="psiDateRange" |
| | | type="daterange" |
| | | range-separator="至" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | | value-format="YYYY-MM-DD" |
| | | :shortcuts="dateShortcuts" |
| | | @change="onDateRangeChange" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 主要内容区域 --> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, onBeforeUnmount, nextTick, provide } from 'vue' |
| | | import { ref, onMounted, onBeforeUnmount, nextTick, provide, reactive } from 'vue' |
| | | import autofit from 'autofit.js' |
| | | import dayjs from 'dayjs' |
| | | import LeftBottom from './components/left-bottom.vue' |
| | | import CenterCenter from './components/center-center.vue' |
| | | import RightTop from '../dataDashboard/components/basic/right-top.vue' |
| | |
| | | |
| | | // 用户store |
| | | const userStore = useUserStore() |
| | | |
| | | // 日期范围筛选 |
| | | const psiDateRange = ref([]) |
| | | const psiDateRangeParams = ref({}) |
| | | const dateShortcuts = [ |
| | | { text: '本月', value: () => { const start = dayjs().startOf('month').format('YYYY-MM-DD'); const end = dayjs().format('YYYY-MM-DD'); return [start, end] } }, |
| | | { text: '近7天', value: () => { const start = dayjs().subtract(6, 'day').format('YYYY-MM-DD'); const end = dayjs().format('YYYY-MM-DD'); return [start, end] } }, |
| | | { text: '近30天', value: () => { const start = dayjs().subtract(29, 'day').format('YYYY-MM-DD'); const end = dayjs().format('YYYY-MM-DD'); return [start, end] } }, |
| | | { text: '近90天', value: () => { const start = dayjs().subtract(89, 'day').format('YYYY-MM-DD'); const end = dayjs().format('YYYY-MM-DD'); return [start, end] } }, |
| | | ] |
| | | |
| | | const onDateRangeChange = (val) => { |
| | | if (val && val.length === 2) { |
| | | psiDateRangeParams.value = { startDate: val[0], endDate: val[1] } |
| | | } else { |
| | | psiDateRangeParams.value = {} |
| | | } |
| | | dataDashboardRefreshTick.value++ |
| | | } |
| | | |
| | | provide('psiDateRange', psiDateRangeParams) |
| | | |
| | | /** 与 dataDashboard 共用注入名,子组件(含复用的 right-top/right-bottom)每分钟刷新 */ |
| | | const DASHBOARD_REFRESH_MS = 60 * 1000 |
| | |
| | | background-repeat: no-repeat; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .header-left { |
| | | width: 300px; |
| | | padding-left: 20px; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .header-right { |
| | | width: 300px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | padding-right: 20px; |
| | | } |
| | | |
| | | .date-picker-wrapper { |
| | | /* 日期选择器在大屏上的样式 */ |
| | | --el-fill-color-blank: rgba(0, 20, 60, 0.8); |
| | | --el-border-color: rgba(0, 212, 255, 0.3); |
| | | --el-text-color-regular: #00d4ff; |
| | | --el-color-primary: #00d4ff; |
| | | } |
| | | |
| | | .factory-name { |
| | | font-weight: 600; |
| | | font-size: 52px; |
| | | color: #FFFFFF; |
| | | top: 16px; |
| | | position: absolute; |
| | | } |
| | | |
| | | .fullscreen-btn { |
| | |
| | | import DateTypeSwitch from '../DateTypeSwitch.vue' |
| | | import { customerContributionRanking } from '@/api/viewIndex.js' |
| | | |
| | | const dateRange = inject('psiDateRange', null) |
| | | |
| | | const chartStyle = { |
| | | width: '100%', |
| | | height: '100%', |
| | |
| | | } |
| | | |
| | | const fetchCustomerRanking = () => { |
| | | customerContributionRanking({ type: dateType.value }) |
| | | customerContributionRanking({ type: dateType.value, ...(dateRange?.value || {}) }) |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | dataArr.value = res.data.map(item => ({ |
| | |
| | | import DateTypeSwitch from '../DateTypeSwitch.vue' |
| | | import { supplierPurchaseRanking } from '@/api/viewIndex.js' |
| | | |
| | | const dateRange = inject('psiDateRange', null) |
| | | |
| | | const chartStyle = { |
| | | width: '100%', |
| | | height: '100%', |
| | |
| | | |
| | | // 供应商采购排名 |
| | | const fetchSupplierRanking = () => { |
| | | supplierPurchaseRanking({ type: radio1.value }) |
| | | supplierPurchaseRanking({ type: radio1.value, ...(dateRange?.value || {}) }) |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | dataArr.value = res.data.map(item => ({ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-form :model="filters" :inline="true"> |
| | | <el-form-item label="日期"> |
| | | <el-date-picker |
| | | style="width: 240px" |
| | | v-model="filters.month" |
| | | value-format="YYYY-MM" |
| | | format="YYYY-MM" |
| | | type="month" |
| | | placeholder="选择月份" |
| | | clearable |
| | | @change="getTableData" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="getTableData"> 搜索 </el-button> |
| | | <el-button @click="resetFilters"> 重置 </el-button> |
| | | <el-button @click="handleOut"> 导出 </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | | total: pagination.total, |
| | | }" |
| | | @pagination="changePage" |
| | | /> |
| | | <div class="app-container vat-page"> |
| | | <el-row :gutter="20" class="vat-row"> |
| | | <!-- 左侧:增值税明细列表 --> |
| | | <el-col :span="15" class="vat-col"> |
| | | <el-card shadow="never" class="vat-card"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span class="card-title">增值税明细</span> |
| | | <el-select v-model="queryMonth" placeholder="选择月份" clearable style="width: 150px;" @change="handleMonthChange"> |
| | | <el-option v-for="m in monthOptions" :key="m" :label="m" :value="m" /> |
| | | </el-select> |
| | | </div> |
| | | </template> |
| | | <template v-if="!loading && vatDetailList.length === 0"> |
| | | <div class="empty-full"> |
| | | <el-empty description="暂无增值税明细数据" :image-size="160" /> |
| | | </div> |
| | | </template> |
| | | <template v-else> |
| | | <div class="table-wrapper"> |
| | | <el-table :data="vatDetailList" border v-loading="loading" stripe height="100%"> |
| | | <el-table-column prop="orderType" label="类型" width="65" align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.orderType === '进项' ? '' : 'warning'" size="small" effect="plain"> |
| | | {{ scope.row.orderType }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="invoiceNo" label="发票号" min-width="140" show-overflow-tooltip /> |
| | | <el-table-column prop="salesContractNo" label="合同号" min-width="140" show-overflow-tooltip /> |
| | | <el-table-column prop="supplierName" label="供应商" min-width="100" show-overflow-tooltip /> |
| | | <el-table-column prop="customerName" label="客户" min-width="100" show-overflow-tooltip /> |
| | | <el-table-column prop="invoiceDate" label="开票日期" width="110" align="center" /> |
| | | <el-table-column prop="taxRate" label="税率" width="75" align="center"> |
| | | <template #default="scope"> |
| | | {{ scope.row.taxRate }}% |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="taxAmount" label="税额" width="110" align="right"> |
| | | <template #default="scope"> |
| | | <span class="tax-amount" :class="scope.row.orderType === '进项' ? 'input-tax' : 'output-tax'"> |
| | | ¥{{ formatNumber(scope.row.taxAmount) }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | <el-pagination |
| | | v-if="page.total > 0" |
| | | class="vat-pagination" |
| | | background |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :current-page="page.current" |
| | | :page-sizes="[10, 20, 50]" |
| | | :page-size="page.size" |
| | | :total="page.total" |
| | | @size-change="handleSizeChange" |
| | | @current-change="handleCurrentChange" /> |
| | | </template> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <!-- 右侧:柱状对比图 --> |
| | | <el-col :span="9" class="vat-col"> |
| | | <el-card shadow="never" class="vat-card"> |
| | | <template #header> |
| | | <span class="card-title">进销项增值税对比</span> |
| | | </template> |
| | | <div class="summary-row"> |
| | | <div class="summary-item input-bg"> |
| | | <div class="summary-label">进项税额合计</div> |
| | | <div class="summary-value">¥{{ formatNumber(inputTotal) }}</div> |
| | | </div> |
| | | <div class="summary-item output-bg"> |
| | | <div class="summary-label">销项税额合计</div> |
| | | <div class="summary-value">¥{{ formatNumber(outputTotal) }}</div> |
| | | </div> |
| | | <div class="summary-item diff-bg"> |
| | | <div class="summary-label">差额</div> |
| | | <div class="summary-value" :class="diffValue >= 0 ? 'positive' : 'negative'"> |
| | | {{ diffValue >= 0 ? '+' : '' }}¥{{ formatNumber(diffValue) }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div v-if="vatDetailList.length > 0" ref="vatChart" class="chart-container"></div> |
| | | <div v-else class="chart-empty">暂无数据</div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { usePaginationApi } from "@/hooks/usePaginationApi"; |
| | | import { onMounted, getCurrentInstance } from "vue"; |
| | | import { getTaxList } from "@/api/procurementManagement/taxComparison"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | import { ref, reactive, computed, onMounted, nextTick, onBeforeUnmount } from "vue"; |
| | | import * as echarts from 'echarts'; |
| | | import { getVatDetail } from "@/api/procurementManagement/taxComparison"; |
| | | |
| | | defineOptions({ |
| | | name: "增值税比对", |
| | | }); |
| | | |
| | | const { |
| | | loading, |
| | | filters, |
| | | columns, |
| | | dataList, |
| | | pagination, |
| | | getTableData, |
| | | resetFilters, |
| | | onCurrentChange, |
| | | } = usePaginationApi( |
| | | getTaxList, |
| | | { |
| | | month: [], // 来票日期 |
| | | }, |
| | | [ |
| | | { |
| | | label: "月份", |
| | | prop: "month", |
| | | align: "center", |
| | | }, |
| | | { |
| | | label: "销项税额", |
| | | prop: "jtaxAmount", |
| | | align: "center", |
| | | }, |
| | | { |
| | | label: "进项税额", |
| | | prop: "xtaxAmount", |
| | | align: "center", |
| | | }, |
| | | { |
| | | label: "销-进", |
| | | prop: "taxAmount", |
| | | align: "center", |
| | | }, |
| | | ], |
| | | {} |
| | | ); |
| | | const vatChart = ref(null); |
| | | let chartInstance = null; |
| | | |
| | | const changePage = ({ page }) => { |
| | | pagination.currentPage = page; |
| | | onCurrentChange(page); |
| | | const queryMonth = ref(new Date().toISOString().slice(0, 7)); |
| | | const monthOptions = ref([]); |
| | | const vatDetailList = ref([]); |
| | | const loading = ref(false); |
| | | const page = reactive({ current: 1, size: 20, total: 0 }); |
| | | |
| | | // 汇总额 |
| | | const inputTotal = computed(() => { |
| | | return vatDetailList.value |
| | | .filter(i => i.orderType === '进项') |
| | | .reduce((sum, i) => sum + (Number(i.taxAmount) || 0), 0); |
| | | }); |
| | | const outputTotal = computed(() => { |
| | | return vatDetailList.value |
| | | .filter(i => i.orderType === '销项') |
| | | .reduce((sum, i) => sum + (Number(i.taxAmount) || 0), 0); |
| | | }); |
| | | const diffValue = computed(() => outputTotal.value - inputTotal.value); |
| | | |
| | | const formatNumber = (val) => { |
| | | const num = Number(val); |
| | | if (isNaN(num)) return '0.00'; |
| | | return num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | }; |
| | | |
| | | // 导出 |
| | | const handleOut = () => { |
| | | ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | const loadVatDetail = () => { |
| | | loading.value = true; |
| | | const params = { |
| | | current: page.current, |
| | | size: page.size, |
| | | month: queryMonth.value || undefined, |
| | | }; |
| | | getVatDetail(params) |
| | | .then((res) => { |
| | | if (res.code === 200) { |
| | | vatDetailList.value = res.data.records || []; |
| | | page.total = res.data.total || 0; |
| | | nextTick(() => renderChart()); |
| | | } |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/purchase/report/exportTwo", {}, "增值税比对.xlsx"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已取消"); |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const renderChart = () => { |
| | | if (!vatChart.value) return; |
| | | if (chartInstance) { |
| | | chartInstance.dispose(); |
| | | } |
| | | chartInstance = echarts.init(vatChart.value); |
| | | |
| | | const data = vatDetailList.value; |
| | | const invoices = data.map(i => i.invoiceNo); |
| | | |
| | | chartInstance.setOption({ |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | backgroundColor: 'rgba(255,255,255,0.95)', |
| | | borderColor: '#e0e0e0', |
| | | borderWidth: 1, |
| | | textStyle: { color: '#333', fontSize: 13 }, |
| | | formatter: function (params) { |
| | | let html = `<b>${params[0].axisValue}</b><br/>`; |
| | | params.forEach(p => { |
| | | if (p.value > 0) { |
| | | html += `${p.marker} ${p.seriesName}: ¥${formatNumber(p.value)}<br/>`; |
| | | } |
| | | }); |
| | | return html; |
| | | }, |
| | | }, |
| | | legend: { |
| | | data: ['进项税额', '销项税额'], |
| | | bottom: 0, |
| | | textStyle: { fontSize: 12 }, |
| | | }, |
| | | grid: { |
| | | left: '10%', |
| | | right: '8%', |
| | | top: '8%', |
| | | bottom: '12%', |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: invoices, |
| | | axisLabel: { |
| | | rotate: 45, |
| | | fontSize: 10, |
| | | interval: 0, |
| | | }, |
| | | axisTick: { alignWithLabel: true }, |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: '税额(元)', |
| | | nameTextStyle: { fontSize: 11 }, |
| | | axisLabel: { |
| | | formatter: (val) => val >= 10000 ? `${(val / 10000).toFixed(1)}万` : val, |
| | | }, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: '进项税额', |
| | | type: 'bar', |
| | | barWidth: '35%', |
| | | data: data.map(i => i.orderType === '进项' ? Number(i.taxAmount) || 0 : null), |
| | | itemStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: '#667eea' }, |
| | | { offset: 1, color: '#764ba2' }, |
| | | ]), |
| | | borderRadius: [4, 4, 0, 0], |
| | | }, |
| | | emphasis: { |
| | | itemStyle: { color: '#667eea' }, |
| | | }, |
| | | }, |
| | | { |
| | | name: '销项税额', |
| | | type: 'bar', |
| | | barWidth: '35%', |
| | | data: data.map(i => i.orderType === '销项' ? Number(i.taxAmount) || 0 : null), |
| | | itemStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: '#f093fb' }, |
| | | { offset: 1, color: '#f5576c' }, |
| | | ]), |
| | | borderRadius: [4, 4, 0, 0], |
| | | }, |
| | | emphasis: { |
| | | itemStyle: { color: '#f093fb' }, |
| | | }, |
| | | }, |
| | | ], |
| | | }); |
| | | }; |
| | | |
| | | const handleMonthChange = () => { |
| | | page.current = 1; |
| | | loadVatDetail(); |
| | | }; |
| | | |
| | | const handleSizeChange = (val) => { |
| | | page.size = val; |
| | | loadVatDetail(); |
| | | }; |
| | | |
| | | const handleCurrentChange = (val) => { |
| | | page.current = val; |
| | | loadVatDetail(); |
| | | }; |
| | | |
| | | const generateMonthOptions = () => { |
| | | const now = new Date(); |
| | | const options = []; |
| | | for (let i = 11; i >= 0; i--) { |
| | | const d = new Date(now.getFullYear(), now.getMonth() - i, 1); |
| | | const y = d.getFullYear(); |
| | | const m = String(d.getMonth() + 1).padStart(2, '0'); |
| | | options.push(`${y}-${m}`); |
| | | } |
| | | monthOptions.value = options; |
| | | }; |
| | | |
| | | const handleResize = () => { |
| | | if (chartInstance) { |
| | | chartInstance.resize(); |
| | | } |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getTableData(); |
| | | generateMonthOptions(); |
| | | loadVatDetail(); |
| | | window.addEventListener('resize', handleResize); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | window.removeEventListener('resize', handleResize); |
| | | if (chartInstance) { |
| | | chartInstance.dispose(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .table_list { |
| | | margin-top: unset; |
| | | .vat-page { |
| | | height: calc(100vh - 84px); |
| | | } |
| | | |
| | | .vat-row { |
| | | height: 100%; |
| | | } |
| | | |
| | | .vat-col { |
| | | height: 100%; |
| | | } |
| | | |
| | | .vat-card { |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | :deep(.el-card__header) { |
| | | flex-shrink: 0; |
| | | } |
| | | :deep(.el-card__body) { |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | overflow: hidden; |
| | | } |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .card-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .table-wrapper { |
| | | flex: 1; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .vat-pagination { |
| | | flex-shrink: 0; |
| | | margin-top: 12px; |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | .tax-amount { |
| | | font-weight: 600; |
| | | font-family: 'Monaco', 'Menlo', monospace; |
| | | &.input-tax { color: #667eea; } |
| | | &.output-tax { color: #f5576c; } |
| | | } |
| | | |
| | | .summary-row { |
| | | display: flex; |
| | | gap: 12px; |
| | | margin-bottom: 16px; |
| | | flex-shrink: 0; |
| | | |
| | | .summary-item { |
| | | flex: 1; |
| | | padding: 12px 14px; |
| | | border-radius: 8px; |
| | | text-align: center; |
| | | |
| | | &.input-bg { |
| | | background: linear-gradient(135deg, #f3f0ff 0%, #e8e5ff 100%); |
| | | .summary-value { color: #667eea; } |
| | | } |
| | | &.output-bg { |
| | | background: linear-gradient(135deg, #fff0f3 0%, #ffe0e6 100%); |
| | | .summary-value { color: #f5576c; } |
| | | } |
| | | &.diff-bg { |
| | | background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); |
| | | .summary-value { |
| | | &.positive { color: #f5576c; } |
| | | &.negative { color: #22c55e; } |
| | | } |
| | | } |
| | | |
| | | .summary-label { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-bottom: 4px; |
| | | } |
| | | .summary-value { |
| | | font-size: 18px; |
| | | font-weight: 700; |
| | | font-family: 'Monaco', 'Menlo', monospace; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .chart-container { |
| | | flex: 1; |
| | | min-height: 0; |
| | | } |
| | | |
| | | .empty-full { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .chart-empty { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: #c0c4cc; |
| | | font-size: 13px; |
| | | } |
| | | </style> |