src/api/equipmentManagement/measurementEquipment.js
@@ -52,4 +52,31 @@ method:"post", data }) } // 通用附件查询 export function getStorageAttachmentList(query) { return request({ url: "/storageAttachment/list", method: "get", params: query, }); } // 通用附件保存 export function addStorageAttachment(data) { return request({ url: "/storageAttachment/add", method: "post", data: data, }); } // 通用附件删除 export function delStorageAttachment(ids) { return request({ url: "/storageAttachment/delete", method: "delete", data: ids, }); } src/api/financialManagement/financialStatements.js
@@ -1,31 +1,13 @@ import request from "@/utils/request"; // 根据日期查询 export const reportForms = (params) => { console.log(params); /** * 获取财务报表月度明细 * @param {Object} params { entryDateStart, entryDateEnd } */ export function accountStatementDetailsByMonth(params) { return request({ url: "/account/accountExpense/report/forms", url: "/accounting/accountStatementDetailsByMonth", method: "get", params, }); }; // 查询每月数据-收入 export const reportIncome = (params) => { console.log(params); return request({ url: "/account/accountExpense/report/income", method: "get", params, }); }; // 查询每月数据-支出 export const reportExpense = (params) => { console.log(params); return request({ url: "/account/accountExpense/report/expense", method: "get", params, }); }; } src/api/inventoryManagement/stockInRecord.js
@@ -41,4 +41,13 @@ method: "post", data, }); }; // 批量反审入库记录(仅驳回状态可反审) export const batchUnapproveStockInRecords = (data) => { return request({ url: "/stockInRecord/reAudit", method: "post", data, }); }; src/components/AIChatSidebar/assistants/financeAssistant.js
@@ -14,13 +14,13 @@ allowFileUpload: false, emptySessionText: '暂无财务会话', quickPrompts: [ '生成本周经营周报(利润与现金流)', '分析本月利润下降原因', '近30天哪个客户利润贡献最高', '查看本月经营驾驶舱', '查询近30天亏损订单', '分析近30天库存资金占用', '预测未来3个月现金流', '生成本周经营周报', '为什么利润下降', '哪个客户最赚钱', '哪个工序成本最高' ] } src/components/AIChatSidebar/index.vue
@@ -227,6 +227,12 @@ :id="`ai-chart-${index}-${key}`" ></div> </div> <div v-else-if="message.chartMarkdownParseFailed" class="chart-empty-state" > 图表解析失败,请稍后重试。 </div> <!-- 表格内容 --> <div v-if="message.type === 'todo_list' && message.tableData" class="table-wrapper"> @@ -2112,6 +2118,7 @@ purchaseData: null, purchaseIntentData: null, financeData: null, chartMarkdownParseFailed: false, localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : [] } @@ -2361,6 +2368,7 @@ messageObj.purchaseData = null messageObj.purchaseIntentData = null messageObj.financeData = null messageObj.chartMarkdownParseFailed = false if (isPurchaseIntentNotRecognized) { messageObj.purchaseIntentData = normalizePurchaseIntentNotRecognizedData(parsedData) @@ -3615,7 +3623,8 @@ salesData: null, purchaseData: null, purchaseIntentData: null, financeData: null financeData: null, chartMarkdownParseFailed: false }) outputState.value[botMsgIndex] = { @@ -3671,6 +3680,7 @@ if (extracted) { applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart) } currentMsg.htmlContent = convertStreamOutput(currentMsg.content || '', botMsgIndex) // 最终解析确保图表渲染 if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) { @@ -3744,7 +3754,8 @@ salesData: null, purchaseData: null, purchaseIntentData: null, financeData: null financeData: null, chartMarkdownParseFailed: false } messages.value.push(botMsg) @@ -3794,6 +3805,7 @@ if (extracted) { applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex) } currentMsg.htmlContent = convertStreamOutput(currentMsg.content || '', botMsgIndex) }).catch(err => { if (err.name === 'CanceledError' || err.name === 'AbortError') { console.log('Request aborted by user') @@ -3835,6 +3847,126 @@ .replace(/</g, '<') .replace(/>/g, '>') .replace(/\n/g, '<br>') } const localChartMarkdownImagePattern = /!\[[^\]]*]\((https?:\/\/local\/generate_chart\?[^)\s]+)\)/gi const parseLocalChartOptionText = (optionText = '') => { const text = String(optionText || '').trim() if (!text) return null const parseCandidates = [text] try { const decoded = decodeURIComponent(text) if (decoded && decoded !== text) { parseCandidates.push(decoded) } } catch (err) { // Keep original text candidate. } for (const candidate of parseCandidates) { try { const parsed = JSON.parse(candidate) if (isPlainObject(parsed)) { return parsed } } catch (err) { continue } } return null } const parseLocalChartOptionFromUrl = (urlText = '') => { try { const url = new URL(String(urlText || '').trim()) if (String(url.hostname || '').toLowerCase() !== 'local' || !String(url.pathname || '').includes('/generate_chart')) { return null } const optionText = url.searchParams.get('options') return parseLocalChartOptionText(optionText) } catch (err) { return null } } const extractLocalChartMarkdown = (text = '') => { const sourceText = String(text || '') if (!sourceText) { return { cleanedText: '', hasLocalChartMarkdown: false, chartOptions: null, parseFailed: false } } let hasLocalChartMarkdown = false let chartIndex = 0 const chartOptions = {} const cleanedText = sourceText.replace(localChartMarkdownImagePattern, (fullMatch, chartUrl) => { hasLocalChartMarkdown = true const option = parseLocalChartOptionFromUrl(chartUrl) if (option) { chartOptions[`markdownChart_${chartIndex++}`] = option } return '' }) const normalizedText = cleanedText .replace(/\n[ \t]*\n[ \t]*\n+/g, '\n\n') .trim() const hasParsedCharts = Object.keys(chartOptions).length > 0 return { cleanedText: normalizedText, hasLocalChartMarkdown, chartOptions: hasParsedCharts ? chartOptions : null, parseFailed: hasLocalChartMarkdown && !hasParsedCharts } } const applyLocalChartMarkdownFallback = (displayText, msgIndex) => { const messageObj = messages.value[msgIndex] if (!messageObj || messageObj.isUser) return displayText const { cleanedText, hasLocalChartMarkdown, chartOptions, parseFailed } = extractLocalChartMarkdown(displayText) if (!hasLocalChartMarkdown) { return displayText } if (chartOptions) { messageObj.chartOptions = chartOptions messageObj.chartRenderReady = true messageObj.chartMarkdownParseFailed = false const streamState = outputState.value[msgIndex] if (!streamState || !streamState.hasRenderedChart) { renderCharts(msgIndex, chartOptions) if (streamState) { streamState.hasRenderedChart = true } } return cleanedText || '已为您生成分析图表。' } if (!messageObj.chartOptions || !messageObj.chartRenderReady) { messageObj.chartOptions = null messageObj.chartRenderReady = false messageObj.chartMarkdownParseFailed = parseFailed } return cleanedText || '图表解析失败,请稍后重试。' } const convertStreamOutput = (output, msgIndex) => { @@ -3902,6 +4034,7 @@ } } display = applyLocalChartMarkdownFallback(display, msgIndex) let html = convertTextToHtml(display) // 还原代码块 @@ -4884,6 +5017,18 @@ margin-bottom: 12px; } .chart-empty-state { margin-top: 12px; width: 100%; border-radius: 10px; border: 1px dashed rgba(148, 163, 184, 0.6); background: #f8fafc; color: #64748b; font-size: 13px; line-height: 1.6; padding: 12px; } .table-wrapper { margin-top: 12px; background: #fff; src/views/equipmentManagement/inspectionManagement/components/formDia.vue
@@ -27,22 +27,22 @@ <el-row> <el-col :span="12"> <el-form-item label="巡检项目" prop="inspectionProject"> <el-input v-model="form.inspectionProject" placeholder="请输入巡检项目" /> <el-input v-model="form.inspectionProject" placeholder="请输入巡检项目" type="textarea" :autosize="{ minRows: 2, maxRows: 6 }" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="备注" prop="remarks"> <el-input v-model="form.remarks" placeholder="请输入备注" type="textarea" :autosize="{ minRows: 2, maxRows: 6 }" /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="是否启用" prop="isEnabled"> <el-radio-group v-model="form.isEnabled"> <el-radio :value="1">是</el-radio> <el-radio :value="0">否</el-radio> </el-radio-group> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="备注" prop="remarks"> <el-input v-model="form.remarks" placeholder="请输入备注" type="textarea" /> </el-form-item> </el-col> </el-row> src/views/equipmentManagement/inspectionManagement/index.vue
@@ -92,6 +92,7 @@ import { Delete, Plus } from "@element-plus/icons-vue"; import { onMounted, ref, reactive, getCurrentInstance, nextTick } from "vue"; import { ElMessageBox } from "element-plus"; import dayjs from "dayjs"; // 组件引入 import PIMTable from "@/components/PIMTable/PIMTable.vue"; @@ -194,7 +195,19 @@ }, }, { prop: "registrant", label: "登记人", minWidth: 100 }, { prop: "createTime", label: "登记日期", minWidth: 100 }, { prop: "createTime", label: "登记日期", minWidth: 100, formatData: cell => { if (!cell) return "-"; try { return dayjs(cell).format("YYYY-MM-DD HH:mm:ss"); } catch { return cell; } }, }, { prop: "inspectionResult", label: "巡检结果", src/views/equipmentManagement/ledger/Form.vue
@@ -156,6 +156,16 @@ /> </el-form-item> </el-col> <el-col :span="24"> <el-form-item label="设备图片" prop="storageBlobDTOs"> <AttachmentUploadImage v-model:fileList="fileList" :limit="20" :fileSize="5" :buttonText="'上传图片'" /> </el-form-item> </el-col> </el-row> </el-form> </template> @@ -170,7 +180,8 @@ calculateTaxExclusiveTotalPrice, } from "@/utils/summarizeTable"; import { ElMessage } from "element-plus"; import {ref, getCurrentInstance} from "vue"; import { ref, getCurrentInstance, computed } from "vue"; import AttachmentUploadImage from '@/components/AttachmentUpload/image/index.vue'; const { proxy } = getCurrentInstance(); const { tax_rate } = proxy.useDict("tax_rate"); @@ -230,6 +241,18 @@ // createUser: useUserStore().nickName, // 录入人 createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // 录入日期 planRuntimeTime: dayjs().format("YYYY-MM-DD"), // 录入日期 storageBlobDTOs: undefined, // 设备图片提交 storageBlobVOs: undefined, // 设备图片展示 }); const fileList = computed({ get() { return form.storageBlobVOs || []; }, set(val) { form.storageBlobDTOs = val; form.storageBlobVOs = val; } }); const loadForm = async (id) => { @@ -259,6 +282,8 @@ } else { form.planRuntimeTime = undefined; } form.storageBlobVOs = data.storageBlobVOs; form.storageBlobDTOs = data.storageBlobVOs; } }; src/views/equipmentManagement/ledger/index.vue
@@ -109,13 +109,52 @@ </div> </template> </el-dialog> <!-- 详情对话框 --> <el-dialog v-model="detailDialogVisible" title="设备台账详情" width="60%" draggable> <el-descriptions :column="2" border> <el-descriptions-item label="设备名称">{{ detailData.deviceName }}</el-descriptions-item> <el-descriptions-item label="规格型号">{{ detailData.deviceModel }}</el-descriptions-item> <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.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.isDepr === 1 ? '是' : '否' }}</el-descriptions-item> <el-descriptions-item label="每年折旧金额">{{ detailData.annualDepreciationAmount }}</el-descriptions-item> <el-descriptions-item label="含税单价">{{ detailData.taxIncludingPriceUnit }}</el-descriptions-item> <el-descriptions-item label="含税总价">{{ detailData.taxIncludingPriceTotal }}</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="设备图片" :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)" :initial-index="index" style="width: 100px; height: 100px" fit="cover" /> </div> <span v-else>无图片</span> </el-descriptions-item> </el-descriptions> <template #footer> <el-button @click="detailDialogVisible = 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 } from "@/api/equipmentManagement/ledger"; import { getLedgerPage, delLedger, getLedgerById } from "@/api/equipmentManagement/ledger"; import { onMounted, getCurrentInstance, ref, reactive } from "vue"; import Modal from "./Modal.vue"; import { ElMessageBox, ElMessage } from "element-plus"; @@ -135,6 +174,9 @@ const qrDialogVisible = ref(false); const qrCodeUrl = ref(""); const qrRowData = ref(null); const detailDialogVisible = ref(false); const detailData = ref({}); // 导入相关 const uploadRef = ref(null) @@ -218,8 +260,14 @@ label: "操作", align: "center", fixed: 'right', width: 150, width: 180, operation: [ { name: "详情", clickFun: (row) => { handleDetail(row); }, }, { name: "编辑", clickFun: (row) => { @@ -227,7 +275,7 @@ }, }, { name: "生成二维码", name: "二维码", clickFun: (row) => { showQRCode(row) }, @@ -248,6 +296,13 @@ const edit = (id) => { modalRef.value.loadForm(id); }; const handleDetail = async (row) => { const { code, data } = await getLedgerById(row.id); if (code == 200) { detailData.value = data; detailDialogVisible.value = true; } }; const changePage = ({ page, limit }) => { pagination.currentPage = page; pagination.pageSize = limit; src/views/equipmentManagement/measurementEquipment/filesDia.vue
@@ -13,7 +13,7 @@ :action="uploadUrl" :on-success="handleUploadSuccess" :on-error="handleUploadError" name="file" name="files" :show-file-list="false" :headers="headers" style="display: inline;margin-right: 10px" @@ -51,10 +51,10 @@ import filePreview from '@/components/filePreview/index.vue' import PIMTable from "@/components/PIMTable/PIMTable.vue"; import { fileAdd, fileDel, fileListPage } from "@/api/financialManagement/revenueManagement.js"; addStorageAttachment, delStorageAttachment, getStorageAttachmentList } from "@/api/equipmentManagement/measurementEquipment.js"; const { proxy } = getCurrentInstance() const emit = defineEmits(['close']) @@ -101,7 +101,7 @@ const headers = ref({ Authorization: "Bearer " + getToken(), }); const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); // 上传的图片服务器地址 const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // 上传的服务器地址 // 打开弹框 const openDialog = (row,type) => { @@ -113,12 +113,13 @@ const paginationSearch = (obj) => { page.current = obj.page; page.size = obj.limit; // 前端分页暂不处理,直接调用获取全量列表 getList(); }; const getList = () => { fileListPage({accountId: currentId.value,accountType:accountType.value, ...page}).then(res => { tableData.value = res.data.records; page.total = res.data.total; getStorageAttachmentList({recordId: currentId.value, recordType: accountType.value}).then(res => { tableData.value = res.data; page.total = res.data ? res.data.length : 0; }) } // 表格选择数据 @@ -134,22 +135,21 @@ // 上传成功处理 function handleUploadSuccess(res, file) { // 如果上传成功 if (res.code == 200) { const fileRow = {} fileRow.name = res.data.originalName fileRow.url = res.data.tempPath uploadFile(fileRow) if (res.code == 200 && res.data && res.data.length > 0) { const mergedFiles = [...tableData.value, ...res.data]; const storageAttachmentDTO = { recordType: accountType.value, recordId: currentId.value, application: "file", storageBlobDTOs: mergedFiles }; addStorageAttachment(storageAttachmentDTO).then(r => { proxy.$modal.msgSuccess("文件上传成功"); getList() }) } else { proxy.$modal.msgError("文件上传失败"); } } function uploadFile(file) { file.accountId = currentId.value; file.accountType = accountType.value; fileAdd(file).then(res => { proxy.$modal.msgSuccess("文件上传成功"); getList() }) } // 上传失败处理 function handleUploadError() { @@ -163,7 +163,7 @@ const handleDelete = () => { let ids = []; if (selectedRows.value.length > 0) { ids = selectedRows.value.map((item) => item.id); ids = selectedRows.value.map((item) => item.storageAttachmentId); } else { proxy.$modal.msgWarning("请选择数据"); return; @@ -173,7 +173,7 @@ cancelButtonText: "取消", type: "warning", }).then(() => { fileDel(ids).then((res) => { delStorageAttachment(ids).then((res) => { proxy.$modal.msgSuccess("删除成功"); getList(); }); src/views/equipmentManagement/measurementEquipment/index.vue
@@ -177,7 +177,7 @@ // 打开附件弹框 const openFilesFormDia = (row) => { filesDia.value?.openDialog(row,'计量器具台账') filesDia.value?.openDialog(row,'measuring_instrument_ledger') }; const dbRowClick = (row)=>{ src/views/equipmentManagement/upkeep/index.vue
@@ -344,7 +344,13 @@ }, { prop: "maintenancePerson", label: "保养人", minWidth: 100 }, { prop: "registrant", label: "登记人", minWidth: 100 }, { prop: "registrationDate", label: "登记日期", minWidth: 100 }, { prop: "registrationDate", label: "登记日期", minWidth: 100, formatData: cell => cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-", }, { fixed: "right", label: "操作", src/views/financialManagement/financialStatements/index.vue
@@ -1,160 +1,186 @@ <template> <template> <div style="padding: 20px;"> <!-- 页面标题和月份筛选 --> <div class="w-full md:w-auto flex items-center gap-3" style="margin-bottom: 20px;"> <el-date-picker v-model="dateRange" type="monthrange" format="YYYY-MM" value-format="YYYY-MM" range-separator="至" start-placeholder="开始月份" end-placeholder="结束月份" :disabled-date="disabledDate" @change="handleDateChange" class="w-full md:w-auto" style="margin-right: 30px;" /> <el-button type="primary" icon="Refresh" @click="resetDateRange" size="default" > <div class="w-full md:w-auto flex items-center gap-3" style="margin-bottom: 20px;"> <el-date-picker v-model="dateRange" type="monthrange" format="YYYY-MM" value-format="YYYY-MM" range-separator="至" start-placeholder="开始月份" end-placeholder="结束月份" :disabled-date="disabledDate" @change="handleDateChange" class="w-full md:w-auto" style="margin-right: 30px;" /> <el-button type="primary" icon="Refresh" @click="resetDateRange" size="default"> 重置 </el-button> </div> <main class="container mx-auto px-4 pb-10"> <!-- 财务指标卡片 --> <div class="stats-cards"> <!-- 总营收 --> <div class="stat-card stat-card-blue"> <div class="stat-icon"> <img src="@/assets/icons/png/walletBlue@2x.png" alt="总营收" /> </div> <div class="stat-icon"><img src="@/assets/icons/png/walletBlue@2x.png" alt="总营收" /></div> <div class="stat-content"> <div class="stat-label">总营收</div> <div class="stat-value">{{ formatMoney(pageInfo.totalIncome || 0) }} 元</div> <div class="stat-value">{{ formatMoney(pageInfo.totalIncome || 0) }}{{ Math.abs(pageInfo.totalIncome) < 10000 ? ' 元' : '' }}</div> </div> </div> <!-- 总支出 --> <div class="stat-card stat-card-orange"> <div class="stat-icon"> <img src="@/assets/icons/png/walletOrange@2x.png" alt="总支出" /> </div> <div class="stat-icon"><img src="@/assets/icons/png/walletOrange@2x.png" alt="总支出" /></div> <div class="stat-content"> <div class="stat-label">总支出</div> <div class="stat-value">{{ formatMoney(pageInfo.totalExpense || 0) }} 元</div> <div class="stat-value">{{ formatMoney(pageInfo.totalExpense || 0) }}{{ Math.abs(pageInfo.totalExpense) < 10000 ? ' 元' : '' }}</div> </div> </div> <!-- 总收入笔数 --> <div class="stat-card stat-card-green"> <div class="stat-icon"> <img src="@/assets/icons/png/walletGreen@2x.png" alt="总收入笔数" /> </div> <div class="stat-icon"><img src="@/assets/icons/png/walletGreen@2x.png" alt="应收账款" /></div> <div class="stat-content"> <div class="stat-label">总收入笔数</div> <div class="stat-value">{{ pageInfo.incomeNumber || 0 }} 笔</div> <div class="stat-label">应收账款</div> <div class="stat-value">{{ formatMoney(pageInfo.totalReceivable || 0) }}{{ Math.abs(pageInfo.totalReceivable) < 10000 ? ' 元' : '' }}</div> </div> </div> <!-- 总支出笔数 --> <div class="stat-card stat-card-red"> <div class="stat-icon"> <img src="@/assets/icons/png/walletRed@2x.png" alt="总支出笔数" /> </div> <div class="stat-icon"><img src="@/assets/icons/png/walletRed@2x.png" alt="应付账款" /></div> <div class="stat-content"> <div class="stat-label">总支出笔数</div> <div class="stat-value">{{ pageInfo.expenseNumber || 0 }} 笔</div> <div class="stat-label">应付账款</div> <div class="stat-value">{{ formatMoney(pageInfo.totalPayable || 0) }}{{ Math.abs(pageInfo.totalPayable) < 10000 ? ' 元' : '' }}</div> </div> </div> <!-- 净收入 --> <div class="stat-card stat-card-yellow"> <div class="stat-icon"> <img src="@/assets/icons/png/walletYellow@2x.png" alt="净收入" /> </div> <div class="stat-icon"><img src="@/assets/icons/png/walletYellow@2x.png" alt="净利润" /></div> <div class="stat-content"> <div class="stat-label">净收入</div> <div class="stat-value">{{ formatMoney(pageInfo.netRevenue || 0) }} 元</div> <div class="stat-label">净利润</div> <div class="stat-value">{{ formatMoney(pageInfo.netRevenue || 0) }}{{ Math.abs(pageInfo.netRevenue) < 10000 ? ' 元' : '' }}</div> </div> </div> </div> <!-- 中间图表区域 --> <!-- 图表区域 --> <div class="charts-row"> <!-- 左侧:收入支出分析 --> <!-- 1. 收支构成分析 (双环形图 + 净利中心) --> <el-card class="chart-card"> <h2 class="section-title">收入支出分析</h2> <div class="pie-chart-container"> <Echarts :legend="pieLegendIncomeExpense" :chartStyle="chartStylePie" :series="pieSeriesIncomeExpense" :tooltip="pieTooltipIncomeExpense" style="height: 320px; width: 100%;"> </Echarts> <div class="pie-stats"> <div class="bar-stat-item"> <span class="bar-stat-label">收入数量</span> <span class="bar-stat-value">{{ pageInfo.incomeNumber || 0 }}</span> <template #header> <div class="card-header"> <span class="header-title">收支构成及净利分析</span> <el-tooltip content="左侧为收入构成,右侧为支出构成,中间展示盈亏净额" placement="top"> <el-icon> <QuestionFilled /> </el-icon> </el-tooltip> </div> </template> <div class="financial-overview-container"> <!-- 收入展示 (左侧) --> <div style="width:60%"> <div class="overview-item income" style="margin-bottom: 20px;"> <div class="overview-box"> <div class="icon-circle"> <el-icon> <TrendCharts /> </el-icon> </div> <div class="data-content"> <div class="label">本期总收入</div> <div class="value">{{ formatMoney(pageInfo.totalIncome) }}</div> <div class="unit">RMB{{ Math.abs(pageInfo.totalIncome) < 10000 ? ' / 元' : '' }}</div> </div> <div class="bg-decoration">INCOME</div> </div> </div> <div class="bar-stat-item"> <span class="bar-stat-label">支出数量</span> <span class="bar-stat-value">{{ pageInfo.expenseNumber || 0 }}</span> <div class="overview-item expense"> <div class="overview-box"> <div class="icon-circle"> <el-icon> <Sell /> </el-icon> </div> <div class="data-content"> <div class="label">本期总支出</div> <div class="value">{{ formatMoney(pageInfo.totalExpense) }}</div> <div class="unit">RMB{{ Math.abs(pageInfo.totalExpense) < 10000 ? ' / 元' : '' }}</div> </div> <div class="bg-decoration">EXPENSE</div> </div> </div> </div> <!-- 净利润核心指示 (中间) --> <div class="profit-indicator"> <div class="profit-gauge-wrapper"> <Echarts :chartStyle="chartStylePie" :series="profitGaugeSeries" :tooltip="gaugeTooltip" style="height: 200px; width: 100%; max-width: 200px;"> </Echarts> <div class="profit-center-text"> <div class="label">净利润</div> <div class="value" :class="pageInfo.netRevenue >= 0 ? 'plus' : 'minus'"> {{ pageInfo.netRevenue >= 0 ? '+' : '' }}{{ formatMoney(pageInfo.netRevenue) }} </div> <div class="rate">利润率: {{ pageInfo.totalIncome > 0 ? ((pageInfo.netRevenue / pageInfo.totalIncome) * 100).toFixed(1) : 0 }}%</div> </div> </div> </div> <!-- 支出展示 (右侧) --> </div> </el-card> <!-- 右侧:行项盈利分析 --> <!-- 2. 应收/应付对冲分析 (柱状图) --> <el-card class="chart-card"> <h2 class="section-title">行项盈利分析</h2> <div class="bar-chart-header"> <div class="bar-stat-item"> <span class="bar-stat-label">当前总个数</span> <span class="bar-stat-value">{{ allBarTypes.value?.length || 0 }}</span> <template #header> <div class="card-header"> <span class="header-title">应收/应付概览</span> <el-tooltip content="对比当前各月份的应收账款与应付账款" placement="top"> <el-icon> <QuestionFilled /> </el-icon> </el-tooltip> </div> <div class="bar-stat-item"> <span class="bar-stat-label">支出金额</span> <span class="bar-stat-value">{{ formatMoney(pageInfo.totalExpense || 0) }}</span> </div> <div class="bar-stat-item"> <span class="bar-stat-label">收入金额</span> <span class="bar-stat-value">{{ formatMoney(pageInfo.totalIncome || 0) }}</span> </div> </div> <Echarts ref="barChart" :chartStyle="chartStyle" :grid="barGrid" :legend="barLegend" :series="barSeries" :tooltip="barTooltip" :xAxis="barXAxis" :yAxis="barYAxis" style="height: 300px; width: 100%;"> </template> <Echarts :chartStyle="chartStyle" :grid="barGrid" :legend="barLegend" :series="barSeries" :tooltip="barTooltip" :xAxis="barXAxis" :yAxis="barYAxis" style="height: 270px; width: 100%;"> </Echarts> </el-card> </div> <!-- 底部:营收趋势分析 --> <!-- 3. 财务综合趋势分析 (折线图) --> <el-card class="trend-chart-card"> <h2 class="section-title">营收趋势分析</h2> <Echarts ref="trendChart" :chartStyle="chartStyle" :grid="grid" :legend="trendLegend" :series="trendSeries" :tooltip="tooltip" :xAxis="xAxis0" :yAxis="trendYAxis" style="height: 350px; width: 100%;"> <template #header> <div class="card-header"> <span class="header-title">财务绩效综合趋势</span> <el-tooltip content="展示收入、支出及净利润的月度变化趋势" placement="top"> <el-icon> <QuestionFilled /> </el-icon> </el-tooltip> </div> </template> <Echarts :chartStyle="chartStyle" :grid="trendGrid" :legend="trendLegend" :series="trendSeries" :tooltip="trendTooltip" :xAxis="trendXAxis" :yAxis="trendYAxis" style="height: 350px; width: 100%;"> </Echarts> </el-card> </main> @@ -162,833 +188,461 @@ </template> <script setup> import { ref, computed, onMounted, reactive, nextTick, getCurrentInstance } from 'vue'; import 'element-plus/dist/index.css'; import Echarts from "@/components/Echarts/echarts.vue"; import { reportForms,reportIncome,reportExpense } from "@/api/financialManagement/financialStatements"; import dayjs from "dayjs"; import { ref, computed, onMounted, reactive, nextTick, getCurrentInstance, } from "vue"; import { QuestionFilled, TrendCharts, Sell } from "@element-plus/icons-vue"; import Echarts from "@/components/Echarts/echarts.vue"; import { accountStatementDetailsByMonth } from "@/api/financialManagement/financialStatements"; import dayjs from "dayjs"; // 日期范围 const dateRange = ref(null); const { proxy } = getCurrentInstance(); const chartStyle = { width: '100%', height: '100%', // 设置图表容器的高度 position:'relative', } const grid = { left: '3%', right: '4%', bottom: '3%', containLabel: true } const lineLegend = { show: false, } // 折线图提示框 const tooltip = reactive({ trigger: 'axis', axisPointer: { type: 'line', lineStyle: { color: '#aaa' } }, // 自定义内容 formatter: function (params) { if (!params || !params.length) return '' const axisLabel = params[0].axisValueLabel || params[0].axisValue || '' const rows = params .map(p => { const colorDot = `<span style="display:inline-block;margin-right:6px;width:8px;height:8px;border-radius:50%;background:${p.color}"></span>` return `${colorDot}${p.seriesName}: ${p.value}` }) .join('<br/>') return `<div>${axisLabel}</div><div>${rows}</div>` } }) const lineSeries0 = ref([]) const lineSeries1 = ref([]) const { proxy } = getCurrentInstance(); const dateRange = ref(null); const pageInfo = reactive({ totalIncome: 0, totalExpense: 0, totalReceivable: 0, totalPayable: 0, netRevenue: 0, }); // 根据月份范围生成 x 轴数据 const generateMonthLabels = (startMonth, endMonth) => { const labels = []; let current = dayjs(startMonth); const end = dayjs(endMonth); while (current.isBefore(end) || current.isSame(end, 'month')) { labels.push(`${current.month() + 1}月`); current = current.add(1, 'month'); } return labels; }; const chartStyle = { width: "100%", height: "100%", position: "relative" }; const chartStylePie = { width: "100%", height: "100%" }; const xAxis0 = ref([ { type: 'category', axisTick: { show: true, alignWithLabel: true }, data: [], }, ]); const xAxis1 = ref([ { type: 'category', axisTick: { show: true, alignWithLabel: true }, data: [], }, ]); const yAxis0 = [ { type: 'value', name: '收入统计', // 左侧y轴 position: 'left', min: 0, // 坐标轴名称样式 nameTextStyle: { color: '#000', fontSize: 14, }, } ] const monthlyTrendList = ref([]); const receivablePayableList = ref([]); const yAxis1 = [ { type: 'value', name: '支出统计', // 左侧y轴 position: 'left', min: 0, // 坐标轴名称样式 nameTextStyle: { color: '#000', fontSize: 14, }, } ] // --- 1. 收支构成分析 (简化版逻辑) --- const gaugeTooltip = { show: false }; const chartStylePie = { width: '100%', height: '100%' // 设置图表容器的高度 } const pieColors = ['#F04864','#FACC14', '#8543E0', '#1890FF', '#13C2C2','#2FC25B']; // 可根据实际调整 const pieData0 = ref([]); const pieData1 = ref([]); const pieLegend0 = computed(() => ({ show: true, top: 'center', left: '60%', orient: 'vertical', icon: 'circle', data: (pieData0.value || []).filter(item => item && item.name).map(item => item.name), formatter: function(name) { if (!name) return ''; const item = pieData0.value.find(i => i && i.name === name); if (!item) return name; return `${name} | ${item.percent} ${item.amount}`; }, textStyle: { color: '#333', fontSize: 14, lineHeight: 26, } })); const pieLegend1 = computed(() => ({ show: true, top: 'center', left: '60%', orient: 'vertical', icon: 'circle', data: (pieData1.value || []).filter(item => item && item.name).map(item => item.name), formatter: function(name) { if (!name) return ''; const item = pieData1.value.find(i => i && i.name === name); if (!item) return name; return `${name} | ${item.percent} ${item.amount}`; }, textStyle: { color: '#333', fontSize: 14, lineHeight: 26, } })); const materialPieSeries0 = computed(() => [ { type: 'pie', radius: ['50%', '65%'], center: ['25%', '50%'], avoidLabelOverlap: false, itemStyle: { borderColor: '#fff', borderWidth: 2 }, label: { show: false }, data: (pieData0.value || []).filter(item => item && item.name), color: pieColors } ]); const materialPieSeries1 = computed(() => [ { type: 'pie', radius: ['50%', '65%'], center: ['25%', '50%'], avoidLabelOverlap: false, itemStyle: { borderColor: '#fff', borderWidth: 2 }, label: { show: false }, data: (pieData1.value || []).filter(item => item && item.name), color: pieColors } ]); const pieTooltip = reactive({ trigger: 'item', formatter: function(params) { // 检查数据是否存在 if (!params.data) return params.name; // 拼接完整内容 return ` <div> <div style="color:${params.color};font-size:16px;">●</div> <div>${params.name}</div> <div>占比:${params.data.percent}</div> <div>金额:${params.data.amount}</div> </div> `; } }) const pageInfo = ref({ }) // 格式化金额 const formatMoney = (value) => { if (!value && value !== 0) return '0'; return Number(value).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }; // 收入支出分析饼图 const pieDataIncomeExpense = computed(() => { const totalIncome = Number(pageInfo.value.totalIncome) || 0; const totalExpense = Number(pageInfo.value.totalExpense) || 0; const total = totalIncome + totalExpense; if (total === 0) { return [ { name: '收入', value: 0, percent: '0%' }, { name: '支出', value: 0, percent: '0%' } ]; } const incomePercent = ((totalIncome / total) * 100).toFixed(0); const expensePercent = ((totalExpense / total) * 100).toFixed(0); return [ { name: '收入', value: totalIncome, percent: `${incomePercent}%` }, { name: '支出', value: totalExpense, percent: `${expensePercent}%` } ]; }); const pieLegendIncomeExpense = computed(() => ({ show: false })); const pieTooltipIncomeExpense = reactive({ trigger: 'item', formatter: function(params) { if (!params.data) return params.name; return `${params.name}占比 ${params.percent}%`; } }); const pieSeriesIncomeExpense = computed(() => [ { type: 'pie', radius: ['0%', '70%'], center: ['50%', '50%'], avoidLabelOverlap: true, itemStyle: { borderColor: '#fff', borderWidth: 2 }, label: { show: true, position: 'outside', formatter: function(params) { return `${params.name}占比 ${params.percent}%`; }, fontSize: 14, color: '#333' }, labelLine: { show: true, length: 15, length2: 10, lineStyle: { color: '#333' } }, emphasis: { label: { show: true, fontSize: 16, fontWeight: 'bold' } }, data: pieDataIncomeExpense.value, color: ['#1890FF', '#FACC14'] } ]); // 行项盈利分析柱状图 const barXAxis = computed(() => { return [{ type: 'category', data: (allBarTypes.value && allBarTypes.value.length > 0) ? allBarTypes.value : ['项目1', '项目2', '项目3', '项目4', '项目5', '项目6', '项目7'], axisTick: { show: true, alignWithLabel: true }, }]; }); const barYAxis = [{ type: 'value', name: '单位: 元', position: 'left', min: 0, nameTextStyle: { color: '#000', fontSize: 14, }, }]; const barGrid = { left: '3%', right: '4%', bottom: '3%', containLabel: true }; const barLegend = { show: true, top: 10, right: 10, }; // 获取所有类型名称 const allBarTypes = computed(() => { const incomeTypes = (lineSeries0.value || []).map(item => item.name || item.typeName).filter(Boolean); const expenseTypes = (lineSeries1.value || []).map(item => item.name || item.typeName).filter(Boolean); return [...new Set([...incomeTypes, ...expenseTypes])]; }); const barSeries = computed(() => { if (allBarTypes.value.length === 0) { const profitGaugeSeries = computed(() => { const rate = pageInfo.totalIncome > 0 ? (pageInfo.netRevenue / pageInfo.totalIncome) * 100 : 0; return [ { name: '支出', type: 'bar', data: [], itemStyle: { color: '#1890FF' } type: "gauge", startAngle: 210, endAngle: -30, min: 0, max: 100, splitNumber: 10, radius: "100%", progress: { show: true, width: 14, itemStyle: { color: pageInfo.netRevenue >= 0 ? "#10b981" : "#f43f5e" }, }, pointer: { show: false }, axisLine: { lineStyle: { width: 14, color: [[1, "#f1f5f9"]] } }, axisTick: { show: false }, splitLine: { show: false }, axisLabel: { show: false }, anchor: { show: false }, title: { show: false }, detail: { show: false }, data: [{ value: Math.max(0, Math.min(100, rate)) }], }, { name: '收入', type: 'bar', data: [], itemStyle: { color: '#13C2C2' } } ]; } // 计算每个项目的总收入(汇总所有月份) const incomeData = allBarTypes.value.map(typeName => { const incomeItem = (lineSeries0.value || []).find(item => (item.name || item.typeName) === typeName); if (incomeItem && incomeItem.data && Array.isArray(incomeItem.data)) { return incomeItem.data.reduce((sum, val) => sum + (Number(val) || 0), 0); } return 0; }); // 计算每个项目的总支出(汇总所有月份) const expenseData = allBarTypes.value.map(typeName => { const expenseItem = (lineSeries1.value || []).find(item => (item.name || item.typeName) === typeName); if (expenseItem && expenseItem.data && Array.isArray(expenseItem.data)) { return expenseItem.data.reduce((sum, val) => sum + (Number(val) || 0), 0); } return 0; }); return [ // --- 2. 应收/应付概览 (柱状图) --- const barGrid = { left: "3%", right: "4%", bottom: "3%", containLabel: true }; const barLegend = { top: "0", right: "center" }; const barXAxis = computed(() => [ { name: '支出', type: 'bar', data: expenseData, itemStyle: { color: '#1890FF' } type: "category", data: receivablePayableList.value.map(item => item.month || ""), axisTick: { alignWithLabel: true }, }, ]); const barYAxis = [{ type: "value", name: "金额 (元)" }]; const barTooltip = { trigger: "axis", axisPointer: { type: "shadow" } }; const barSeries = computed(() => [ { name: "应收账款", type: "bar", barWidth: "30%", data: receivablePayableList.value.map(item => item.receivable || 0), itemStyle: { color: "#10b981" }, }, { name: '收入', type: 'bar', data: incomeData, itemStyle: { color: '#13C2C2' } } ]; }); name: "应付账款", type: "bar", barWidth: "30%", data: receivablePayableList.value.map(item => item.payable || 0), itemStyle: { color: "#ef4444" }, }, ]); const barTooltip = reactive({ trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: function (params) { if (!params || !params.length) return ''; const axisLabel = params[0].axisValueLabel || params[0].axisValue || ''; const rows = params .map(p => { const colorDot = `<span style="display:inline-block;margin-right:6px;width:8px;height:8px;border-radius:50%;background:${p.color}"></span>`; const value = typeof p.value === 'number' ? p.value.toFixed(2) : p.value; return `${colorDot}${p.seriesName} ${value}`; }) .join('<br/>'); return `<div>${axisLabel}</div><div>${rows}</div>`; } }); // 营收趋势分析 const trendLegend = { show: true, top: 10, right: 10, }; const trendYAxis = [{ type: 'value', name: '单位: 元', position: 'left', min: 0, nameTextStyle: { color: '#000', fontSize: 14, }, }]; const trendSeries = computed(() => { // 汇总所有支出类型的数据 let expenseTrend = []; if (lineSeries1.value.length > 0) { const monthCount = Math.max(...lineSeries1.value.map(item => item.data?.length || 0)); expenseTrend = Array(monthCount).fill(0); lineSeries1.value.forEach(item => { if (item.data && Array.isArray(item.data)) { item.data.forEach((val, index) => { if (index < monthCount) { expenseTrend[index] += Number(val) || 0; } }); } }); } // 汇总所有收入类型的数据 let incomeTrend = []; if (lineSeries0.value.length > 0) { const monthCount = Math.max(...lineSeries0.value.map(item => item.data?.length || 0)); incomeTrend = Array(monthCount).fill(0); lineSeries0.value.forEach(item => { if (item.data && Array.isArray(item.data)) { item.data.forEach((val, index) => { if (index < monthCount) { incomeTrend[index] += Number(val) || 0; } }); } }); } return [ // --- 3. 财务综合趋势分析 (折线图) --- const trendGrid = { left: "3%", right: "4%", bottom: "3%", containLabel: true }; const trendLegend = { top: "0", right: "center" }; const trendXAxis = computed(() => [ { name: '支出', type: 'line', data: expenseTrend, itemStyle: { color: '#1890FF' }, smooth: true type: "category", boundaryGap: false, data: monthlyTrendList.value.map(item => item.month || ""), }, ]); const trendYAxis = [{ type: "value", name: "金额 (元)" }]; const trendTooltip = { trigger: "axis" }; const trendSeries = computed(() => [ { name: "总营收", type: "line", smooth: true, data: monthlyTrendList.value.map(item => item.income || 0), itemStyle: { color: "#4f46e5" }, areaStyle: { opacity: 0.1 }, }, { name: '收入', type: 'line', data: incomeTrend, itemStyle: { color: '#13C2C2' }, smooth: true } ]; }); name: "总支出", type: "line", smooth: true, data: monthlyTrendList.value.map(item => item.expense || 0), itemStyle: { color: "#f97316" }, }, { name: "净利润", type: "line", smooth: true, data: monthlyTrendList.value.map(item => item.profit || 0), lineStyle: { width: 4, type: "dashed" }, itemStyle: { color: "#10b981" }, }, ]); // 获取最近六个月的范围 const getLastSixMonths = () => { const endMonth = dayjs().format('YYYY-MM'); const startMonth = dayjs().subtract(5, 'month').format('YYYY-MM'); return [startMonth, endMonth]; }; // --- 公用逻辑 --- const formatMoney = val => { return val; }; const getData = async () => { if (!dateRange.value || !Array.isArray(dateRange.value) || dateRange.value.length !== 2) { return; } const startDateStr = dateRange.value[0]; const endDateStr = dateRange.value[1]; if (!startDateStr || !endDateStr) { return; } // 验证日期格式并转换为完整日期 const startDate = dayjs(startDateStr); const endDate = dayjs(endDateStr); if (!startDate.isValid() || !endDate.isValid()) { console.error('无效的日期格式'); return; } // 更新 x 轴数据 const monthLabels = generateMonthLabels(startDateStr, endDateStr); xAxis0.value[0].data = monthLabels; xAxis1.value[0].data = monthLabels; // 开始月份拼接第一天,结束月份拼接最后一天 const entryDateStart = startDate.startOf('month').format('YYYY-MM-DD'); const entryDateEnd = endDate.endOf('month').format('YYYY-MM-DD'); try { const {code,data} = await reportForms({entryDateStart, entryDateEnd}); if(code === 200 && data) { pageInfo.value = data || {}; // 安全处理数据,过滤掉 null 或 undefined pieData0.value = (data.incomeType || []).filter(item => item && item.typeName).map(item=>({ name:item.typeName || '', value:item.account || 0, percent:`${((item.proportion || 0) * 100).toFixed(2)}%`, amount:`¥${(item.account || 0).toFixed(2)}` })) pieData1.value = (data.expenseType || []).filter(item => item && item.typeName).map(item=>({ name:item.typeName || '', value:item.account || 0, percent:`${((item.proportion || 0) * 100).toFixed(2)}%`, amount:`¥${(item.account || 0).toFixed(2)}` })) } } catch (error) { console.error('获取财务指标数据失败:', error); } try{ const {code,data} = await reportIncome({entryDateStart, entryDateEnd}); if(code==200 && data && Array.isArray(data)){ lineSeries0.value = data.filter(item => item && item.typeName).map(item=>({ name:item.typeName || '', type: 'line', data:(item.account || []).map(val => Number(val) || 0) })) } }catch (error) { console.error('获取财务指标数据失败:', error); } try{ const {code,data} = await reportExpense({entryDateStart, entryDateEnd}); if(code==200 && data && Array.isArray(data)){ lineSeries1.value = data.filter(item => item && item.typeName).map(item=>({ name:item.typeName || '', type: 'line', data:(item.account || []).map(val => Number(val) || 0) })) } }catch (error) { console.error('获取财务指标数据失败:', error); } }; const handleDateChange = val => { if (val) getData(); }; // 初始化 onMounted(() => { // 设置默认值为最近六个月 const defaultRange = getLastSixMonths(); dateRange.value = defaultRange; // 使用 nextTick 确保组件完全渲染后再调用 nextTick(() => { const resetDateRange = () => { dateRange.value = [ dayjs().subtract(5, "month").format("YYYY-MM"), dayjs().format("YYYY-MM"), ]; getData(); }; const disabledDate = time => dayjs(time).isAfter(dayjs(), "month"); const getData = async () => { if (!dateRange.value || dateRange.value.length !== 2) return; const params = { entryDateStart: dayjs(dateRange.value[0]) .startOf("month") .format("YYYY-MM-DD"), entryDateEnd: dayjs(dateRange.value[1]).endOf("month").format("YYYY-MM-DD"), }; try { const res = await accountStatementDetailsByMonth(params); if (res.code === 200 && res.data) { const data = res.data; // 更新顶部汇总卡片数据 pageInfo.totalIncome = data.totalIncome || 0; pageInfo.totalExpense = data.totalExpense || 0; pageInfo.totalReceivable = data.accountsReceivable || 0; pageInfo.totalPayable = data.accountsPayable || 0; pageInfo.netRevenue = data.netRevenue || 0; // 更新图表数据 monthlyTrendList.value = data.monthlyTrendList || []; receivablePayableList.value = data.receivablePayableList || []; } } catch (error) { console.error("获取财务报表数据失败:", error); } }; onMounted(() => { resetDateRange(); }); }); // 限制月份选择范围(最多12个月) const disabledDate = (time) => { // 如果没有选择开始月份,不禁用任何日期 if (!dateRange.value || !Array.isArray(dateRange.value) || !dateRange.value[0]) { return false; } const startMonth = dayjs(dateRange.value[0]); const currentMonth = dayjs(time); // 如果当前月份在开始月份之前,禁用 if (currentMonth.isBefore(startMonth, 'month')) { return true; } // 计算最大允许的月份(开始月份 + 11个月 = 12个月) const maxMonth = startMonth.add(11, 'month'); // 禁用超过12个月的月份 return currentMonth.isAfter(maxMonth, 'month'); }; // 处理月份范围变化 const handleDateChange = (newRange) => { if (!newRange || !Array.isArray(newRange) || newRange.length !== 2) { return; } // 验证月份范围不超过12个月 const startDate = dayjs(newRange[0]); const endDate = dayjs(newRange[1]); const monthDiff = endDate.diff(startDate, 'month'); if (monthDiff > 11) { proxy.$modal.msgWarning('最多只能选择12个月份'); // 自动调整为12个月 const adjustedEnd = startDate.add(11, 'month').format('YYYY-MM'); dateRange.value = [newRange[0], adjustedEnd]; getData(); return; } dateRange.value = newRange; getData(); }; // 重置月份范围 const resetDateRange = () => { // 重置为最近六个月 dateRange.value = getLastSixMonths(); getData(); }; </script> <style scoped lang="scss"> /* 基础样式补充 */ :root { --el-color-primary: #4f46e5; } /* 统计卡片样式 */ .stats-cards { display: grid; grid-template-columns: repeat(5, 1fr); gap: 20px; margin-bottom: 20px; } .stat-card { background: #fff; border: 1px solid #e4e7ed; border-radius: 8px; padding: 20px; display: flex; align-items: center; gap: 15px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); transition: all 0.3s; &:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); transform: translateY(-2px); } .stat-icon { width: 48px; height: 48px; flex-shrink: 0; img { width: 100%; height: 100%; object-fit: contain; } } .stat-content { flex: 1; display: flex; flex-direction: column; gap: 8px; } .stat-label { font-size: 14px; color: #666; line-height: 1.2; } .stat-value { font-size: 24px; font-weight: 600; color: #333; line-height: 1.2; } .stat-trend { font-size: 12px; line-height: 1.2; &.trend-up { color: #f56c6c; } &.trend-down { color: #67c23a; } } } /* 图表行布局 */ .charts-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; } .chart-card { border: 1px solid #e4e7ed; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); :deep(.el-card__body) { padding: 20px !important; } } .trend-chart-card { border: 1px solid #e4e7ed; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); :deep(.el-card__body) { padding: 20px !important; } } /* 饼图容器 */ .pie-chart-container { position: relative; .pie-stats { display: flex; justify-content: space-between; .stats-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin-top: 20px; .bar-stat-item { margin-bottom: 24px; } .stat-card { background: #fff; border: 1px solid #edf2f7; border-radius: 12px; padding: 24px; display: flex; align-items: center; gap: 16px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); &:hover { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); transform: translateY(-4px); } .stat-icon { width: 56px; height: 56px; background: #f7fafc; border-radius: 12px; display: flex; flex-direction: column; align-items: center; justify-content: center; img { width: 32px; height: 32px; } } .stat-content { .stat-label { font-size: 14px; color: #718096; margin-bottom: 4px; } .stat-value { font-size: 20px; font-weight: 700; color: #2d3748; } } } .charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 24px; margin-bottom: 24px; } @media (min-width: 1200px) { .charts-row { grid-template-columns: repeat(2, 1fr); } } .chart-card, .trend-chart-card { border-radius: 16px; border: none; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); .card-header { display: flex; align-items: center; gap: 8px; padding: 15px; background: #f5f7fa; border-radius: 6px; flex: 1; .bar-stat-label { font-size: 14px; color: #666; } .bar-stat-value { font-size: 18px; .header-title { font-size: 16px; font-weight: 600; color: #333; color: #1a202c; } .el-icon { color: #a0aec0; cursor: help; } } } } /* 柱状图头部统计 */ .bar-chart-header { display: flex; justify-content: space-between; gap: 20px; margin-bottom: 20px; .bar-stat-item { .financial-overview-container { display: flex; flex-direction: column; justify-content: space-between; align-items: center; gap: 8px; padding: 15px; background: #f5f7fa; border-radius: 6px; flex: 1; .bar-stat-label { font-size: 14px; color: #666; flex-wrap: nowrap; gap: 10px; padding: 20px 0; width: 100%; overflow: hidden; .overview-item { flex: 1; min-width: 0; // 允许在 flex 容器中缩写,防止内容撑开 display: flex; justify-content: center; .overview-box { position: relative; width: 100%; max-width: 320px; height: 110px; background: #f8fafc; border-radius: 12px; padding: 12px 16px; display: flex; align-items: center; gap: 12px; overflow: hidden; transition: all 0.3s ease; &:hover { transform: translateY(-5px); box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); } .icon-circle { flex-shrink: 0; width: 42px; height: 42px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; z-index: 2; } .data-content { z-index: 2; min-width: 0; .label { font-size: 13px; color: #718096; margin-bottom: 2px; font-weight: 500; white-space: nowrap; } .value { font-size: 18px; font-weight: 800; color: #1a202c; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .unit { font-size: 11px; color: #a0aec0; } } .bg-decoration { position: absolute; right: -5px; bottom: -5px; font-size: 32px; font-weight: 950; color: rgba(0, 0, 0, 0.03); font-style: italic; user-select: none; z-index: 1; } } &.income { .icon-circle { background: #eef2ff; color: #4f46e5; } .overview-box { border-left: 5px solid #4f46e5; } } &.expense { .icon-circle { background: #fff7ed; color: #f97316; } .overview-box { border-left: 5px solid #f97316; } } } .bar-stat-value { font-size: 18px; font-weight: 600; color: #333; .profit-indicator { flex: 0 40%; // 固定宽度,不参与弹性缩放以保证仪表盘完整 display: flex; justify-content: center; align-items: center; .profit-gauge-wrapper { position: relative; display: flex; justify-content: center; align-items: center; width: 100%; // max-width: 180px; .profit-center-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; width: 100%; .label { font-size: 12px; color: #718096; font-weight: 500; } .value { font-size: 20px; font-weight: 800; margin: 2px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; &.plus { color: #10b981; } &.minus { color: #f43f5e; } } .rate { font-size: 11px; color: #a0aec0; font-weight: 500; } } } } // 针对非常窄的屏幕进行整体缩放 @media (max-width: 1400px) { transform-origin: center; // 如果容器太窄,通过缩小内部元素来适应 // 这里不使用 transform: scale 因为会影响布局流,改用内部尺寸微调 .overview-item .overview-box { padding: 10px; gap: 8px; .value { font-size: 16px; } .icon-circle { width: 36px; height: 36px; font-size: 18px; } } .profit-indicator { flex: 0 40%; .profit-gauge-wrapper .value { font-size: 18px; } } } } } /* 标题样式 */ .section-title { position: relative; font-size: 18px; color: #333; padding-left: 12px; margin-bottom: 20px; font-weight: 700; &::before { position: absolute; left: 0; top: 2px; content: ''; width: 4px; height: 18px; background-color: #002FA7; border-radius: 2px; } } /* 响应式设计 */ @media (max-width: 1400px) { .stats-cards { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 1024px) { .stats-cards { grid-template-columns: repeat(2, 1fr); } .charts-row { grid-template-columns: 1fr; } } @media (max-width: 640px) { .stats-cards { grid-template-columns: 1fr; } } </style> src/views/index.vue
@@ -1307,9 +1307,12 @@ const statisticsReceivable = async () => { const res = await statisticsReceivablePayable({ type: 1 }); const data = res?.data || {}; const payableMoney = Number(data.payableMoney ?? 0); const receivableMoney = Number(data.receivableMoney ?? 0); barSeries.value[0].data = [ { value: res.data.payableMoney, itemStyle: { color: barColors2[0] } }, { value: res.data.receivableMoney, itemStyle: { color: barColors2[1] } }, { value: payableMoney, itemStyle: { color: barColors2[0] } }, { value: receivableMoney, itemStyle: { color: barColors2[1] } }, ]; }; src/views/inventoryManagement/receiptManagement/Record.vue
@@ -71,10 +71,15 @@ </el-form> </div> <div class="actions"> <el-button type="primary" @click="handleBatchApprove">审批</el-button> <el-button type="primary" :disabled="!canBatchApprove" @click="handleBatchApprove">审批</el-button> <el-button :disabled="!canReverseApprove" @click="handleReverseApprove">反审</el-button> <el-button @click="handleOut">导出</el-button> <el-button type="danger" plain :disabled="!canDelete" @click="handleDelete">删除 </el-button> </div> @@ -89,7 +94,7 @@ height="calc(100vh - 18.5em)"> <el-table-column align="center" type="selection" :selectable="isRowSelectableForApprove" :selectable="isRowSelectable" width="55"/> <el-table-column align="center" label="序号" @@ -97,7 +102,7 @@ width="60"/> <el-table-column label="入库批次" prop="inboundBatches" width="280" width="200" show-overflow-tooltip/> <el-table-column label="入库时间" prop="createTime" @@ -127,6 +132,16 @@ {{ getRecordType(scope.row.recordType) }} </template> </el-table-column> <el-table-column v-if="showSourceOrderNoColumn" label="源单号" width="150" prop="sourceOrderNo" show-overflow-tooltip> <template #default="scope"> {{ formatSourceOrderNo(scope.row?.sourceOrderNo) }} </template> </el-table-column> <el-table-column label="审批状态" prop="approvalStatus" show-overflow-tooltip> @@ -153,6 +168,7 @@ ref, reactive, toRefs, computed, onMounted, getCurrentInstance, } from "vue"; @@ -161,6 +177,7 @@ getStockInRecordListPage, batchDeletePendingStockInRecords, batchApproveStockInRecords, batchUnapproveStockInRecords, } from "@/api/inventoryManagement/stockInRecord.js"; import { findAllQualifiedStockInRecordTypeOptions, @@ -245,8 +262,33 @@ return status === 0 || status === "0" || status === "pending" || status === "PENDING" || status === null || status === undefined || status === ""; }; const isRowSelectableForApprove = row => { return isPendingApproval(row?.approvalStatus); const isRejectedApproval = status => { return status === 2 || status === "2" || status === "rejected" || status === "REJECTED"; }; const isRowSelectable = row => { return isPendingApproval(row?.approvalStatus) || isRejectedApproval(row?.approvalStatus); }; const canBatchApprove = computed(() => { return selectedRows.value.length > 0 && selectedRows.value.every(row => isPendingApproval(row.approvalStatus)); }); const canReverseApprove = computed(() => { return selectedRows.value.length > 0 && selectedRows.value.every(row => isRejectedApproval(row.approvalStatus)); }); const canDelete = computed(() => canBatchApprove.value); const showSourceOrderNoColumn = computed(() => { const topParentProductId = Number(props.topParentProductId); return topParentProductId === 276 || topParentProductId === 278; }); const formatSourceOrderNo = (value) => { const text = String(value ?? "").trim(); return text || "--"; }; const pageProductChange = obj => { @@ -283,14 +325,40 @@ // 表格选择数据 const handleSelectionChange = selection => { selectedRows.value = selection.filter(item => item.id && isPendingApproval(item.approvalStatus)); selectedRows.value = selection.filter(item => item.id && isRowSelectable(item)); }; const expandedRowKeys = ref([]); const handleReverseApprove = () => { if (!canReverseApprove.value) { proxy.$modal.msgWarning("请选择已驳回的数据"); return; } const ids = selectedRows.value.map(item => item.id); ElMessageBox.confirm("反审后记录将恢复为待审批状态,是否确认反审?", "反审", { confirmButtonText: "确认", cancelButtonText: "取消", type: "warning", }) .then(() => { batchUnapproveStockInRecords({ids}) .then(() => { proxy.$modal.msgSuccess("反审成功"); getList(); }) .catch(() => { proxy.$modal.msgError("反审失败"); }); }) .catch(() => { proxy.$modal.msg("已取消"); }); }; const handleBatchApprove = () => { if (selectedRows.value.length === 0) { proxy.$modal.msgWarning("请选择数据"); if (!canBatchApprove.value) { proxy.$modal.msgWarning("请选择待审批的数据"); return; } const ids = selectedRows.value.map(item => item.id); @@ -344,8 +412,8 @@ // 删除 const handleDelete = () => { if (selectedRows.value.length === 0) { proxy.$modal.msgWarning("请选择数据"); if (!canDelete.value) { proxy.$modal.msgWarning("请选择待审批的数据"); return; } const ids = selectedRows.value.map(item => item.id); @@ -390,4 +458,4 @@ justify-content: flex-end; margin-bottom: 10px; } </style> </style> src/views/procurementManagement/procurementLedger/index.vue
@@ -42,6 +42,20 @@ clearable @change="changeDaterange" /> </el-form-item> <el-form-item label="入库状态:"> <el-select v-model="searchForm.stockInStatus" placeholder="请选择" clearable style="width: 240px" @change="handleQuery"> <el-option label="待入库" value="待入库" /> <el-option label="入库中" value="入库中" /> <el-option label="完全入库" value="完全入库" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="handleQuery"> 搜索 @@ -92,6 +106,16 @@ prop="specificationModel" /> <el-table-column label="单位" prop="unit" /> <el-table-column label="入库审核状态" prop="stockInApprovalStatus" width="120"> <template #default="scope"> <el-tag :type="getStockInApprovalStatusType(scope.row.stockInApprovalStatus)" size="small"> {{ scope.row.stockInApprovalStatus || '--' }} </el-tag> </template> </el-table-column> <el-table-column label="数量" prop="quantity" /> <el-table-column label="可用数量" @@ -143,6 +167,17 @@ </el-tag> </template> </el-table-column> <el-table-column label="入库状态" prop="stockInStatus" width="100" show-overflow-tooltip> <template #default="scope"> <el-tag :type="getStockInStatusType(scope.row.stockInStatus)" size="small"> {{ scope.row.stockInStatus || '--' }} </el-tag> </template> </el-table-column> <el-table-column label="签订日期" prop="executionDate" width="100" @@ -176,7 +211,7 @@ <el-button link type="primary" @click="openForm('edit', scope.row)" :disabled="scope.row.approvalStatus !== 1 && scope.row.approvalStatus !== 4">编辑 :disabled="scope.row.stockInStatus === '完全入库'">编辑 </el-button> <el-button link type="primary" @@ -403,6 +438,16 @@ </el-tag> </template> </el-table-column> <el-table-column label="入库审核状态" prop="stockInApprovalStatus" width="120"> <template #default="scope"> <el-tag :type="getStockInApprovalStatusType(scope.row.stockInApprovalStatus)" size="small"> {{ scope.row.stockInApprovalStatus || '--' }} </el-tag> </template> </el-table-column> <el-table-column fixed="right" label="操作" min-width="60" @@ -410,7 +455,8 @@ <template #default="scope"> <el-button link type="primary" @click="openProductForm('edit', scope.row, scope.$index)">编辑 @click="openProductForm('edit', scope.row, scope.$index)" :disabled="scope.row.stockInApprovalStatus === '完全入库'">编辑 </el-button> </template> </el-table-column> @@ -723,6 +769,26 @@ 2: "warning", // 审批中 - 橙色 3: "success", // 审批通过 - 绿色 4: "danger", // 审批失败 - 红色 }; return typeMap[status] || ""; }; // 获取入库状态标签类型 const getStockInStatusType = status => { const typeMap = { "待入库": "info", // 待入库 - 灰色 "入库中": "warning", // 入库中 - 橙色 "完全入库": "success", // 完全入库 - 绿色 }; return typeMap[status] || ""; }; // 获取入库审核状态标签类型 const getStockInApprovalStatusType = status => { const typeMap = { "待入库": "info", // 待入库 - 灰色 "入库中": "warning", // 入库中 - 橙色 "完全入库": "success", // 完全入库 - 绿色 }; return typeMap[status] || ""; }; @@ -1206,10 +1272,10 @@ }; // 打开弹框 const openForm = async (type, row) => { // 编辑时检查审核状态,只有待审核(1)和审批失败(4)才能编辑 // 编辑时检查入库状态,完全入库时不能编辑 if (type === "edit" && row) { if (row.approvalStatus !== 1 && row.approvalStatus !== 4) { proxy.$modal.msgWarning("只有待审核和审批失败状态的记录才能编辑"); if (row.stockInStatus === '完全入库') { proxy.$modal.msgWarning("完全入库状态的记录不能编辑"); return; } } @@ -1256,9 +1322,11 @@ currentId.value = row.id; try { const purchaseRes = await getPurchaseById({ id: row.id, type: 2 }); form.value = { ...purchaseRes }; productData.value = purchaseRes.productData || []; form.value = { ...purchaseRes, stockInStatus: row.stockInStatus }; fileList.value = purchaseRes.storageBlobVOS || []; // 使用 productList 接口获取产品列表,以获取入库审核状态 const productRes = await productList({ salesLedgerId: row.id, type: 2 }); productData.value = productRes.data || []; } catch (error) { console.error("加载采购台账数据失败:", error); proxy.$modal.msgError("加载数据失败"); @@ -1375,6 +1443,12 @@ }; // 打开产品弹框 const openProductForm = async (type, row, index) => { // 编辑时检查产品入库审核状态,完全入库时不能编辑 if (type === "edit" && row && row.stockInApprovalStatus === '完全入库') { proxy.$modal.msgWarning("完全入库状态的产品不能编辑"); return; } productOperationType.value = type; productOperationIndex.value = index; productForm.value = {}; @@ -1545,8 +1619,9 @@ addOrUpdateSalesLedgerProduct(productForm.value).then(res => { proxy.$modal.msgSuccess("提交成功"); closeProductDia(); getPurchaseById({ id: currentId.value, type: 2 }).then(res => { productData.value = res.productData; // 使用 productList 接口刷新产品列表,以获取入库审核状态 productList({ salesLedgerId: currentId.value, type: 2 }).then(res => { productData.value = res.data || []; }); }); }; @@ -1554,6 +1629,14 @@ const deleteProduct = () => { if (productSelectedRows.value.length === 0) { proxy.$modal.msgWarning("请选择数据"); return; } // 检查选中的产品中是否有完全入库的 const hasFullyStocked = productSelectedRows.value.some( row => row.stockInApprovalStatus === '完全入库' ); if (hasFullyStocked) { proxy.$modal.msgWarning("选中的产品中包含完全入库的产品,无法删除"); return; } if (operationType.value === "add") { @@ -1579,8 +1662,9 @@ delProduct(ids).then(res => { proxy.$modal.msgSuccess("删除成功"); closeProductDia(); getPurchaseById({ id: currentId.value, type: 2 }).then(res => { productData.value = res.productData; // 使用 productList 接口刷新产品列表,以获取入库审核状态 productList({ salesLedgerId: currentId.value, type: 2 }).then(res => { productData.value = res.data || []; }); }); }) src/views/procurementManagement/thePaymentLedger/index.vue
@@ -33,6 +33,7 @@ <script setup> import { ref } from "vue"; import { Search } from "@element-plus/icons-vue"; import dayjs from "dayjs"; // import { registrationList } from "@/api/procurementManagement/paymentLedger.js"; const tableColumn = ref([ { @@ -54,6 +55,8 @@ { label: "登记日期", prop: "registrationtDate", formatData: cell => cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-", }, ]); const tableData = ref([]); src/views/productionManagement/workOrderEdit/index.vue
@@ -73,6 +73,18 @@ title="指定报工人" width="800px"> <div class="assign-reporter-content"> <div class="search-box"> <el-input v-model="employeeSearchKeyword" placeholder="搜索人员姓名" clearable @input="handleEmployeeSearch" style="width: 350px"> <template #prefix> <i class="el-icon-search"></i> </template> </el-input> </div> <div class="selected-tags-box" v-if="selectedEmployeeIds.length > 0"> <div class="tags-label">已选择:</div> @@ -90,7 +102,7 @@ v-loading="employeeTableLoading"> <el-checkbox-group v-model="selectedEmployeeIds"> <div class="employee-grid"> <div v-for="item in employeeTableData" <div v-for="item in filteredEmployeeList" :key="item.userId" class="employee-item"> <el-checkbox :label="item.userId" @@ -103,9 +115,9 @@ </div> </div> </el-checkbox-group> <div v-if="employeeTableData.length === 0" <div v-if="filteredEmployeeList.length === 0" class="empty-text"> 暂无匹配人员 {{ employeeSearchKeyword ? '无匹配人员' : '暂无人员数据' }} </div> </div> </div> @@ -121,7 +133,7 @@ </template> <script setup> import { getCurrentInstance, onMounted, reactive, ref, toRefs } from "vue"; import { getCurrentInstance, onMounted, reactive, ref, toRefs, computed } from "vue"; import { ElMessageBox } from "element-plus"; import { productWorkOrderPage, @@ -253,8 +265,24 @@ const employeeSearchForm = reactive({ staffName: "", }); const employeeSearchKeyword = ref(""); const selectedEmployeeIds = ref([]); const currentWorkOrder = ref(null); const filteredEmployeeList = computed(() => { const keyword = employeeSearchKeyword.value.trim().toLowerCase(); if (!keyword) { return employeeTableData.value; } return employeeTableData.value.filter(item => { const name = (item.nickName || "").toLowerCase(); const dept = (item.dept?.deptName || "").toLowerCase(); return name.includes(keyword) || dept.includes(keyword); }); }); const handleEmployeeSearch = () => { }; const data = reactive({ searchForm: { @@ -416,6 +444,12 @@ } .assign-reporter-content { .search-box { margin-bottom: 16px; display: flex; justify-content: center; } .selected-tags-box { margin-bottom: 16px; padding: 12px; src/views/reportAnalysis/dataDashboard/index0.vue
@@ -925,15 +925,18 @@ }) } // 应付应收统计 const statisticsReceivable = (type) => { statisticsReceivablePayable({type: radio1.value}).then((res) => { const statisticsReceivable = (type = radio1.value) => { statisticsReceivablePayable({ type }).then((res) => { const data = res?.data || {} const payableMoney = Number(data.payableMoney ?? 0) const receivableMoney = Number(data.receivableMoney ?? 0) // 设置应付金额数据 barSeries.value[0].data = [ { value: res.data.payableMoney } { value: payableMoney } ] // 设置应收金额数据 barSeries.value[1].data = [ { value: res.data.receivableMoney } { value: receivableMoney } ] }) } @@ -2031,4 +2034,4 @@ color: #B8C8E0; font-size: 11px; } </style> </style> src/views/reportAnalysis/financialAnalysis/components/center-top.vue
@@ -112,50 +112,69 @@ profitRate: { value: 0, trend: 0 }, }) const fetchMonthlyIncome = async () => { const res = await getMonthlyIncome() const data = res?.data || {} const toNumber = (val) => { const num = Number(val) return Number.isFinite(num) ? num : 0 } income.value.amount = data.monthlyIncome ?? 0 const collectionRate = Number(data.collectionRate ?? 0) const overdueRate = Number(data.overdueRate ?? 0) income.value.repayRate = { value: collectionRate, trend: collectionRate >= 0 ? 1 : -1, } income.value.overdueCount = data.overdueNum ?? 0 income.value.overdueRate = { value: overdueRate, trend: overdueRate >= 0 ? 1 : -1, const fetchMonthlyIncome = async () => { try { const res = await getMonthlyIncome() const data = res?.data || {} income.value.amount = toNumber(data.monthlyIncome) const collectionRate = toNumber(data.collectionRate) const overdueRate = toNumber(data.overdueRate) income.value.repayRate = { value: collectionRate, trend: collectionRate >= 0 ? 1 : -1, } income.value.overdueCount = toNumber(data.overdueNum) income.value.overdueRate = { value: overdueRate, trend: overdueRate >= 0 ? 1 : -1, } } catch { income.value.amount = 0 income.value.repayRate = { value: 0, trend: 0 } income.value.overdueCount = 0 income.value.overdueRate = { value: 0, trend: 0 } } } const fetchMonthlyExpenditure = async () => { const res = await getMonthlyExpenditure() const data = res?.data || {} try { const res = await getMonthlyExpenditure() const data = res?.data || {} expense.value.amount = data.monthlyExpenditure ?? 0 const paymentRate = Number(data.paymentRate ?? 0) expense.value.netProfit = { value: paymentRate, trend: paymentRate >= 0 ? 1 : -1, } expense.value.grossProfit = data.grossProfit ?? 0 expense.value.amount = toNumber(data.monthlyExpenditure) const paymentRate = toNumber(data.paymentRate) expense.value.netProfit = { value: paymentRate, trend: paymentRate >= 0 ? 1 : -1, } expense.value.grossProfit = toNumber(data.grossProfit) const profitMarginRate = Number(data.profitMarginRate ?? 0) expense.value.profitRate = { value: profitMarginRate, trend: profitMarginRate >= 0 ? 1 : -1, const profitMarginRate = toNumber(data.profitMarginRate) expense.value.profitRate = { value: profitMarginRate, trend: profitMarginRate >= 0 ? 1 : -1, } } catch { expense.value.amount = 0 expense.value.netProfit = { value: 0, trend: 0 } expense.value.grossProfit = 0 expense.value.profitRate = { value: 0, trend: 0 } } } const isWanAmount = (val) => { const num = Number(val) || 0 const num = toNumber(val) return Math.abs(num) >= 10000 } const formatAmountWanNumber = (val) => { const num = Number(val) || 0 const num = toNumber(val) if (Math.abs(num) >= 10000) { return (num / 10000).toFixed(2) } @@ -163,7 +182,7 @@ } const formatPercent = (val) => { const num = Number(val) || 0 const num = toNumber(val) // 百分比展示始终用绝对值,小数保留两位 return `${Math.abs(num).toFixed(2)}%` } src/views/system/user/index.vue
@@ -511,6 +511,9 @@ roleOptions.value = response.roles form.value.postIds = response.postIds form.value.roleIds = response.roleIds if (response.deptIds && response.deptIds.length > 0) { form.value.deptId = response.deptIds[0] } open.value = true title.value = "修改用户" form.password = ""