| | |
| | | "logo": "logo/JHHGLogo.png", |
| | | "favicon": "favicon/JHHGfavicon.ico" |
| | | }, |
| | | "LFJZ": { |
| | | "env": { |
| | | "VITE_APP_TITLE": "力发建筑装饰有限公司", |
| | | "VITE_BASE_API": "http://36.212.201.241:9001", |
| | | "VITE_JAVA_API": "http://36.212.201.241:9000" |
| | | }, |
| | | "logo": "logo/LFJZLogo.png", |
| | | "favicon": "favicon/LFJZfavicon.ico" |
| | | }, |
| | | "logo": "/src/assets/logo/logo.png", |
| | | "favicon": "/public/favicon.ico" |
| | | } |
| | |
| | | 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, |
| | | }); |
| | | } |
| | |
| | | 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, |
| | | }); |
| | | }; |
| | | } |
| | |
| | | method: "post", |
| | | data, |
| | | }); |
| | | }; |
| | | |
| | | // 批量反审入库记录(仅驳回状态可反审) |
| | | export const batchUnapproveStockInRecords = (data) => { |
| | | return request({ |
| | | url: "/stockInRecord/reAudit", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | }; |
| | |
| | | allowFileUpload: false, |
| | | emptySessionText: '暂无财务会话', |
| | | quickPrompts: [ |
| | | '生成本周经营周报(利润与现金流)', |
| | | '分析本月利润下降原因', |
| | | '近30天哪个客户利润贡献最高', |
| | | '查看本月经营驾驶舱', |
| | | '查询近30天亏损订单', |
| | | '分析近30天库存资金占用', |
| | | '预测未来3个月现金流', |
| | | '生成本周经营周报', |
| | | '为什么利润下降', |
| | | '哪个客户最赚钱', |
| | | '哪个工序成本最高' |
| | | ] |
| | | } |
| | |
| | | :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"> |
| | |
| | | sales_customer_profile_list: '客户档案', |
| | | sales_quotation_list: '销售报价', |
| | | sales_ledger_list: '销售台账', |
| | | sales_return_list: '销售退货', |
| | | sales_return_list: '销售退款/回款记录', |
| | | sales_customer_interaction_list: '客户往来', |
| | | sales_shipping_list: '发货台账', |
| | | sales_dashboard: '销售指标统计', |
| | |
| | | } |
| | | const purchaseTypeLabelMap = { |
| | | purchase_material_rank: '采购物料金额排行', |
| | | purchase_stats: '采购统计', |
| | | purchase_pending_payment_list: '待付款采购单' |
| | | } |
| | | const financialStructuredTypeSet = new Set([ |
| | |
| | | stockQty: '库存量' |
| | | } |
| | | Object.assign(structuredFieldLabelMap, { |
| | | refundId: '退款/回款单号', |
| | | collectionNumber: '收款单号', |
| | | collectionAmount: '收款金额', |
| | | actualAmount: '收款金额(兼容)', |
| | | customerId: '客户ID', |
| | | salesLedgerId: '销售台账ID', |
| | | projectName: '项目名称', |
| | | receiptPaymentDate: '收付款日期', |
| | | receiptPaymentAmount: '收付款金额', |
| | | receiptPaymentType: '收付款类型', |
| | | registrant: '登记人', |
| | | returnAmount: '回款金额汇总', |
| | | totalReceiptAmount: '收款总额', |
| | | customerCount: '客户数', |
| | | paidAmount: '已付款金额', |
| | | contractAmount: '合同金额', |
| | | paymentCount: '付款笔数', |
| | | invoiceCount: '发票笔数', |
| | | paymentAmount: '付款金额', |
| | | invoiceAmount: '发票金额', |
| | | customerName: '客户名称', |
| | | riskLevel: '风险等级', |
| | | riskScore: '风险评分', |
| | |
| | | return items.filter(item => isPlainObject(item)) |
| | | } |
| | | |
| | | const normalizeSalesReturnListItems = (items = []) => { |
| | | return normalizeSalesListItems(items).map((item) => { |
| | | const collectionAmount = item.collectionAmount ?? item.actualAmount |
| | | const collectionNumber = item.collectionNumber || item.refundId || '' |
| | | return { |
| | | ...item, |
| | | refundId: item.refundId || collectionNumber, |
| | | collectionNumber, |
| | | collectionAmount: collectionAmount ?? 0, |
| | | actualAmount: item.actualAmount ?? collectionAmount ?? 0 |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const normalizeSalesTypeListItems = (type = '', items = []) => { |
| | | if (type === 'sales_return_list') { |
| | | return normalizeSalesReturnListItems(items) |
| | | } |
| | | return normalizeSalesListItems(items) |
| | | } |
| | | |
| | | const inferSalesColumnsByType = (type = '', listItems = []) => { |
| | | const columns = inferSalesColumns(listItems) |
| | | if (type === 'sales_return_list') { |
| | | return columns.filter(column => column !== 'actualAmount') |
| | | } |
| | | return columns |
| | | } |
| | | |
| | | const buildSalesStructuredData = (parsedData) => { |
| | | const type = String(parsedData?.type || '') |
| | | if (!salesStructuredTypeSet.has(type)) return null |
| | | |
| | | const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {} |
| | | const listItems = normalizeSalesListItems(rawData.items) |
| | | const listItems = normalizeSalesTypeListItems(type, rawData.items) |
| | | const topCustomers = normalizeSalesListItems(rawData.topCustomers) |
| | | const contractTrend = normalizeSalesListItems(rawData.contractTrend) |
| | | |
| | |
| | | type, |
| | | summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary), |
| | | listItems, |
| | | columns: inferSalesColumns(listItems), |
| | | columns: inferSalesColumnsByType(type, listItems), |
| | | topCustomers, |
| | | topCustomerColumns: inferSalesColumns(topCustomers), |
| | | contractTrend, |
| | |
| | | purchaseData: null, |
| | | purchaseIntentData: null, |
| | | financeData: null, |
| | | chartMarkdownParseFailed: false, |
| | | localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : [] |
| | | } |
| | | |
| | |
| | | messageObj.purchaseData = null |
| | | messageObj.purchaseIntentData = null |
| | | messageObj.financeData = null |
| | | messageObj.chartMarkdownParseFailed = false |
| | | |
| | | if (isPurchaseIntentNotRecognized) { |
| | | messageObj.purchaseIntentData = normalizePurchaseIntentNotRecognizedData(parsedData) |
| | |
| | | if (parsedData.type === 'sales_customer_churn_risk') return '已为您生成客户流失风险分析。' |
| | | if (parsedData.type === 'sales_collection_quote_strategy') return '已为您生成回款与报价策略建议。' |
| | | if (parsedData.type === 'sales_dashboard') return '已为您生成销售指标统计。' |
| | | if (parsedData.type === 'sales_return_list') return '已返回销售退款/回款记录。' |
| | | if (parsedData.type === 'sales_customer_interaction_list') return '已返回客户往来数据。' |
| | | return '已返回销售查询结果。' |
| | | } |
| | | if (manufacturingStructuredTypeSet.has(parsedData.type)) { |
| | |
| | | if (String(parsedData.type || '').startsWith('purchase_')) return '已返回采购查询结果。' |
| | | if (parsedData.charts && Object.keys(parsedData.charts).length > 0) return '已为您生成分析图表。' |
| | | return '正在为您展示分析结果...' |
| | | } |
| | | |
| | | const resolveStructuredDescription = (parsedData) => { |
| | | if (!parsedData) return '' |
| | | if (parsedData.action === 'confirm_required') { |
| | | return getPurchaseConfirmDescription(parsedData) |
| | | } |
| | | |
| | | const description = String(parsedData.description || '').trim() |
| | | if (!description) return '' |
| | | |
| | | if (parsedData.type === 'sales_customer_interaction_list') { |
| | | if (description === 'no_customer_interactions') return '该时间范围暂无客户往来记录。' |
| | | if (description === 'ok') return '已返回客户往来数据。' |
| | | } |
| | | |
| | | if (description === 'ok' || description === 'success') { |
| | | return getStructuredFallbackText(parsedData) |
| | | } |
| | | |
| | | return description |
| | | } |
| | | |
| | | const buildPurchaseMaterialRankCharts = (parsedData) => { |
| | |
| | | salesData: null, |
| | | purchaseData: null, |
| | | purchaseIntentData: null, |
| | | financeData: null |
| | | financeData: null, |
| | | chartMarkdownParseFailed: false |
| | | }) |
| | | |
| | | outputState.value[botMsgIndex] = { |
| | |
| | | if (extracted) { |
| | | applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart) |
| | | } |
| | | currentMsg.htmlContent = convertStreamOutput(currentMsg.content || '', botMsgIndex) |
| | | |
| | | // 最终解析确保图表渲染 |
| | | if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) { |
| | |
| | | salesData: null, |
| | | purchaseData: null, |
| | | purchaseIntentData: null, |
| | | financeData: null |
| | | financeData: null, |
| | | chartMarkdownParseFailed: false |
| | | } |
| | | messages.value.push(botMsg) |
| | | |
| | |
| | | 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') |
| | |
| | | .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) => { |
| | | if (!output) return '' |
| | | const state = outputState.value[msgIndex] |
| | |
| | | display = '' |
| | | } |
| | | |
| | | if (parsed.description) { |
| | | display = parsed.action === 'confirm_required' |
| | | ? getPurchaseConfirmDescription(parsed) |
| | | : parsed.description |
| | | const resolvedDescription = resolveStructuredDescription(parsed) |
| | | if (resolvedDescription) { |
| | | display = resolvedDescription |
| | | } |
| | | |
| | | if (!display) { |
| | |
| | | display = '' |
| | | } |
| | | |
| | | if (parsed.description) { |
| | | display = parsed.action === 'confirm_required' |
| | | ? getPurchaseConfirmDescription(parsed) |
| | | : parsed.description |
| | | const resolvedDescription = resolveStructuredDescription(parsed) |
| | | if (resolvedDescription) { |
| | | display = resolvedDescription |
| | | } |
| | | |
| | | if (!display) { |
| | |
| | | } |
| | | } |
| | | |
| | | display = applyLocalChartMarkdownFallback(display, msgIndex) |
| | | let html = convertTextToHtml(display) |
| | | |
| | | // 还原代码块 |
| | |
| | | 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; |
| | |
| | | top: 0; |
| | | z-index: var(--layout-header-z); |
| | | width: 100%; |
| | | padding: 8px var(--content-gap) 0; |
| | | padding: 8px var(--content-gap) 8px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 6px; |
| | | background: transparent; |
| | | backdrop-filter: none; |
| | | background: var(--app-bg, #f3f7fc); |
| | | } |
| | | |
| | | .fixed-header.with-tags { |
| | |
| | | <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> |
| | |
| | | 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"; |
| | |
| | | }, |
| | | }, |
| | | { 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: "巡检结果", |
| | |
| | | /> |
| | | </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> |
| | |
| | | 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"); |
| | |
| | | // 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) => { |
| | |
| | | } else { |
| | | form.planRuntimeTime = undefined; |
| | | } |
| | | form.storageBlobVOs = data.storageBlobVOs; |
| | | form.storageBlobDTOs = data.storageBlobVOs; |
| | | } |
| | | }; |
| | | |
| | |
| | | </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"; |
| | |
| | | const qrDialogVisible = ref(false); |
| | | const qrCodeUrl = ref(""); |
| | | const qrRowData = ref(null); |
| | | |
| | | const detailDialogVisible = ref(false); |
| | | const detailData = ref({}); |
| | | |
| | | // 导入相关 |
| | | const uploadRef = ref(null) |
| | |
| | | label: "操作", |
| | | align: "center", |
| | | fixed: 'right', |
| | | width: 150, |
| | | width: 180, |
| | | operation: [ |
| | | { |
| | | name: "详情", |
| | | clickFun: (row) => { |
| | | handleDetail(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "编辑", |
| | | clickFun: (row) => { |
| | |
| | | }, |
| | | }, |
| | | { |
| | | name: "生成二维码", |
| | | name: "二维码", |
| | | clickFun: (row) => { |
| | | showQRCode(row) |
| | | }, |
| | |
| | | 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; |
| | |
| | | :action="uploadUrl" |
| | | :on-success="handleUploadSuccess" |
| | | :on-error="handleUploadError" |
| | | name="file" |
| | | name="files" |
| | | :show-file-list="false" |
| | | :headers="headers" |
| | | style="display: inline;margin-right: 10px" |
| | |
| | | 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']) |
| | | |
| | |
| | | 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) => { |
| | |
| | | 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; |
| | | }) |
| | | } |
| | | // 表格选择数据 |
| | |
| | | // 上传成功处理 |
| | | 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() { |
| | |
| | | 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; |
| | |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | }).then(() => { |
| | | fileDel(ids).then((res) => { |
| | | delStorageAttachment(ids).then((res) => { |
| | | proxy.$modal.msgSuccess("删除成功"); |
| | | getList(); |
| | | }); |
| | |
| | | |
| | | // 打开附件弹框 |
| | | const openFilesFormDia = (row) => { |
| | | filesDia.value?.openDialog(row,'计量器具台账') |
| | | filesDia.value?.openDialog(row,'measuring_instrument_ledger') |
| | | }; |
| | | |
| | | const dbRowClick = (row)=>{ |
| | |
| | | }, |
| | | { 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: "操作", |
| | |
| | | <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> |
| | |
| | | </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> |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">生产订单进度</div> |
| | | <el-radio-group v-model="orderFilter" size="small"> |
| | | <el-radio-button label="all">全部({{ orderProgressMeta.total }})</el-radio-button> |
| | | <el-radio-button label="waiting">待开始({{ orderProgressMeta.waitingCount }})</el-radio-button> |
| | | <el-radio-button label="inProgress">进行中({{ orderProgressMeta.inProgressCount }})</el-radio-button> |
| | | <el-radio-button label="completed">已完成({{ orderProgressMeta.completedCount }})</el-radio-button> |
| | | <el-radio-button label="paused">已暂停({{ orderProgressMeta.pausedCount }})</el-radio-button> |
| | | <el-radio-button :value="'all'">全部({{ orderProgressMeta.total }})</el-radio-button> |
| | | <el-radio-button :value="'waiting'">待开始({{ orderProgressMeta.waitingCount }})</el-radio-button> |
| | | <el-radio-button :value="'inProgress'">进行中({{ orderProgressMeta.inProgressCount }})</el-radio-button> |
| | | <el-radio-button :value="'completed'">已完成({{ orderProgressMeta.completedCount }})</el-radio-button> |
| | | <el-radio-button :value="'paused'">已暂停({{ orderProgressMeta.pausedCount }})</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <el-table :data="filteredOrders" stripe> |
| | |
| | | <div class="process-selection-wrapper"> |
| | | <el-checkbox-group v-model="tempProcessIds"> |
| | | <div class="process-grid"> |
| | | <el-checkbox v-for="item in processOptions" :key="item.id" :label="item.id" border> |
| | | <el-checkbox v-for="item in processOptions" :key="item.id" :value="item.id" border> |
| | | {{ item.name }} |
| | | </el-checkbox> |
| | | </div> |
| | |
| | | |
| | | 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] } }, |
| | | ]; |
| | | }; |
| | | |
| | |
| | | barSeries1.value[0].data = []; |
| | | barSeries1.value[1].data = []; |
| | | barSeries1.value[2].data = []; |
| | | (res.data.item || []).forEach((item) => { |
| | | const data = res.data || {}; |
| | | (data.item || []).forEach((item) => { |
| | | xAxis1.value[0].data.push(item.date); |
| | | barSeries1.value[0].data.push(item.supplierNum); |
| | | barSeries1.value[1].data.push(item.processNum); |
| | | barSeries1.value[2].data.push(item.factoryNum); |
| | | }); |
| | | qualityStatisticsObject.value.supplierNum = res.data.supplierNum; |
| | | qualityStatisticsObject.value.processNum = res.data.processNum; |
| | | qualityStatisticsObject.value.factoryNum = res.data.factoryNum; |
| | | qualityStatisticsObject.value.supplierNum = data.supplierNum || 0; |
| | | qualityStatisticsObject.value.processNum = data.processNum || 0; |
| | | qualityStatisticsObject.value.factoryNum = data.factoryNum || 0; |
| | | }; |
| | | |
| | | const getAmountHalfYearNum = async () => { |
| | |
| | | </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> |
| | |
| | | height="calc(100vh - 18.5em)"> |
| | | <el-table-column align="center" |
| | | type="selection" |
| | | :selectable="isRowSelectableForApprove" |
| | | :selectable="isRowSelectable" |
| | | width="55"/> |
| | | <el-table-column align="center" |
| | | label="序号" |
| | |
| | | width="60"/> |
| | | <el-table-column label="入库批次" |
| | | prop="inboundBatches" |
| | | width="280" |
| | | width="200" |
| | | show-overflow-tooltip/> |
| | | <el-table-column label="入库时间" |
| | | prop="createTime" |
| | |
| | | {{ 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> |
| | |
| | | ref, |
| | | reactive, |
| | | toRefs, |
| | | computed, |
| | | onMounted, |
| | | getCurrentInstance, |
| | | } from "vue"; |
| | |
| | | getStockInRecordListPage, |
| | | batchDeletePendingStockInRecords, |
| | | batchApproveStockInRecords, |
| | | batchUnapproveStockInRecords, |
| | | } from "@/api/inventoryManagement/stockInRecord.js"; |
| | | import { |
| | | findAllQualifiedStockInRecordTypeOptions, |
| | |
| | | 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 => { |
| | |
| | | |
| | | // 表格选择数据 |
| | | 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); |
| | |
| | | |
| | | // 删除 |
| | | 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); |
| | |
| | | justify-content: flex-end; |
| | | margin-bottom: 10px; |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | 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"> 搜索 |
| | |
| | | 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="可用数量" |
| | |
| | | </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" |
| | |
| | | <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" |
| | |
| | | </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" |
| | |
| | | <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> |
| | |
| | | 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] || ""; |
| | | }; |
| | |
| | | }; |
| | | // 打开弹框 |
| | | 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; |
| | | } |
| | | } |
| | |
| | | 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("加载数据失败"); |
| | |
| | | }; |
| | | // 打开产品弹框 |
| | | const openProductForm = async (type, row, index) => { |
| | | // 编辑时检查产品入库审核状态,完全入库时不能编辑 |
| | | if (type === "edit" && row && row.stockInApprovalStatus === '完全入库') { |
| | | proxy.$modal.msgWarning("完全入库状态的产品不能编辑"); |
| | | return; |
| | | } |
| | | |
| | | productOperationType.value = type; |
| | | productOperationIndex.value = index; |
| | | productForm.value = {}; |
| | |
| | | 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 || []; |
| | | }); |
| | | }); |
| | | }; |
| | |
| | | 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") { |
| | |
| | | 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 || []; |
| | | }); |
| | | }); |
| | | }) |
| | |
| | | <script setup> |
| | | import { ref } from "vue"; |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { registrationList } from "@/api/procurementManagement/paymentLedger.js"; |
| | | import dayjs from "dayjs"; |
| | | // import { registrationList } from "@/api/procurementManagement/paymentLedger.js"; |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "付款日期", |
| | |
| | | { |
| | | label: "登记日期", |
| | | prop: "registrationtDate", |
| | | formatData: cell => |
| | | cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-", |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | |
| | | }; |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | registrationList({ ...searchForm.value, ...page }).then((res) => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.rows; |
| | | total.value = res.total; |
| | | }); |
| | | // registrationList({ ...searchForm.value, ...page }).then((res) => { |
| | | // tableLoading.value = false; |
| | | // tableData.value = res.rows; |
| | | // total.value = res.total; |
| | | // }); |
| | | }; |
| | | getList(); |
| | | </script> |
| | |
| | | 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> |
| | |
| | | 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" |
| | |
| | | </div> |
| | | </div> |
| | | </el-checkbox-group> |
| | | <div v-if="employeeTableData.length === 0" |
| | | <div v-if="filteredEmployeeList.length === 0" |
| | | class="empty-text"> |
| | | 暂无匹配人员 |
| | | {{ employeeSearchKeyword ? '无匹配人员' : '暂无人员数据' }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | </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, |
| | |
| | | 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: { |
| | |
| | | } |
| | | |
| | | .assign-reporter-content { |
| | | .search-box { |
| | | margin-bottom: 16px; |
| | | display: flex; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .selected-tags-box { |
| | | margin-bottom: 16px; |
| | | padding: 12px; |
| | |
| | | }) |
| | | } |
| | | // 应付应收统计 |
| | | 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 } |
| | | ] |
| | | }) |
| | | } |
| | |
| | | color: #B8C8E0; |
| | | font-size: 11px; |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | 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) |
| | | } |
| | |
| | | } |
| | | |
| | | const formatPercent = (val) => { |
| | | const num = Number(val) || 0 |
| | | const num = toNumber(val) |
| | | // 百分比展示始终用绝对值,小数保留两位 |
| | | return `${Math.abs(num).toFixed(2)}%` |
| | | } |
| | |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="批号:" |
| | | <el-form-item label="库存:" |
| | | prop="batchNo"> |
| | | <el-table :data="deliveryForm.batchNoList" |
| | | border |
| | |
| | | <el-table-column label="批号" |
| | | prop="batchNo" |
| | | min-width="180" /> |
| | | <el-table-column label="产品大类" |
| | | prop="productName" |
| | | min-width="100" /> |
| | | <el-table-column label="规格型号" |
| | | prop="model" |
| | | min-width="100" /> |
| | | <el-table-column label="单位" |
| | | prop="unit" |
| | | min-width="100" /> |
| | | <el-table-column label="库存数量" |
| | | min-width="120" |
| | | align="center"> |
| | |
| | | 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 = ""
|