Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-management into dev_New
| | |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // 产品销售金额分析 |
| | | export const productSalesAnalysis = () => { |
| | | return request({ |
| | | url: '/home/productSalesAnalysis', |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // 原材料采购金额占比 |
| | | export const rawMaterialPurchaseAmountRatio = () => { |
| | | return request({ |
| | | url: '/home/rawMaterialPurchaseAmountRatio', |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // 销售/采购/储存产品数 |
| | | export const salesPurchaseStorageProductCount = () => { |
| | | return request({ |
| | | url: '/home/salesPurchaseStorageProductCount', |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // 产品出入库分析(可传 productType: 1 原材料 2 半成品 3 成品) |
| | | export const productInOutAnalysis = (params) => { |
| | | return request({ |
| | | url: '/home/productInOutAnalysis', |
| | | method: 'get', |
| | | params |
| | | }) |
| | | } |
| | | |
| | | // 产品周转天数 |
| | | export const productTurnoverDays = () => { |
| | | return request({ |
| | | url: '/home/productTurnoverDays', |
| | | method: 'get' |
| | | }) |
| | | } |
| | |
| | | <el-form-item label="来票数:"> |
| | | <el-input-number :step="0.1" |
| | | :min="0" |
| | | :max="maxTicketsNum" |
| | | style="width: 100%" |
| | | v-model="form.ticketsNum" |
| | | @change="inputTicketsNum" |
| | | :precision="2" /> |
| | | <div style="font-size: 12px; color: #909399; margin-top: 4px;"> |
| | | 可填写数量:{{ maxTicketsNum }} |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | |
| | | import useFormData from "@/hooks/useFormData"; |
| | | import { updateRegistration, getProductRecordById } from "@/api/procurementManagement/procurementInvoiceLedger"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { getCurrentInstance, ref, nextTick } from "vue"; |
| | | import { getCurrentInstance, ref, nextTick, computed } from "vue"; |
| | | |
| | | defineOptions({ |
| | | name: "来票台账编辑", |
| | |
| | | |
| | | const saleLedgerProjectId = ref(""); |
| | | const temFutureTickets = ref(0); |
| | | const originalTicketsNum = ref(0); // 原始已来票数 |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | // 计算最大可填写数量 = 原始已来票数 + 未来票数 |
| | | const maxTicketsNum = computed(() => { |
| | | return Number(originalTicketsNum.value) + Number(temFutureTickets.value); |
| | | }); |
| | | |
| | | const { |
| | | id, |
| | |
| | | form.taxInclusiveUnitPrice = data.taxInclusiveUnitPrice; |
| | | form.futureTickets = data.futureTickets; |
| | | temFutureTickets.value = data.futureTickets; |
| | | // 保存原始已来票数 |
| | | originalTicketsNum.value = data.ticketsNum || 0; |
| | | } |
| | | }; |
| | | |
| | |
| | | proxy.$modal.msgWarning("含税单价不能为零或未定义"); |
| | | return; |
| | | } |
| | | if (Number(form.ticketsNum) > Number(temFutureTickets.value)) { |
| | | proxy.$modal.msgWarning("开票数不得大于未开票数"); |
| | | form.ticketsNum = temFutureTickets.value; |
| | | |
| | | // 检查来票数不能大于(原始已来票数 + 未来票数) |
| | | const maxNum = maxTicketsNum.value; |
| | | if (Number(form.ticketsNum) > maxNum) { |
| | | proxy.$modal.msgWarning(`来票数不能大于${maxNum}(已来票数${originalTicketsNum.value} + 未来票数${temFutureTickets.value})`); |
| | | form.ticketsNum = maxNum; |
| | | return; |
| | | } |
| | | |
| | | // 计算本次新增的来票数(当前来票数 - 原始已来票数) |
| | | const newTicketsNum = Number(form.ticketsNum) - Number(originalTicketsNum.value); |
| | | |
| | | // 如果新增的来票数大于未来票数,则限制 |
| | | if (newTicketsNum > Number(temFutureTickets.value)) { |
| | | proxy.$modal.msgWarning("本次新增来票数不得大于未来票数"); |
| | | form.ticketsNum = Number(originalTicketsNum.value) + Number(temFutureTickets.value); |
| | | return; |
| | | } |
| | | |
| | | // 确保所有数值都转换为数字类型进行计算 |
| | | const ticketsAmount = |
| | | Number(form.ticketsNum) * Number(form.taxInclusiveUnitPrice); |
| | | const futureTickets = |
| | | Number(temFutureTickets.value) - Number(form.ticketsNum); |
| | | Number(temFutureTickets.value) - newTicketsNum; |
| | | form.futureTickets = Number(futureTickets.toFixed(2)); |
| | | form.ticketsAmount = Number(ticketsAmount.toFixed(2)); |
| | | }; |
| | |
| | | return; |
| | | } |
| | | |
| | | if (Number(val) > Number(form.futureTickets * form.taxInclusiveUnitPrice)) { |
| | | proxy.$modal.msgWarning("本次来票金额不得大于总金额"); |
| | | form.ticketsAmount = ( |
| | | form.futureTickets * form.taxInclusiveUnitPrice |
| | | ).toFixed(2); |
| | | const ticketsNum = |
| | | Number(form.ticketsAmount) / Number(form.taxInclusiveUnitPrice); |
| | | form.ticketsNum = Number(ticketsNum.toFixed(2)); |
| | | // 计算最大可填写金额 = (原始已来票数 + 未来票数)* 含税单价 |
| | | const maxAmount = maxTicketsNum.value * Number(form.taxInclusiveUnitPrice); |
| | | |
| | | if (Number(val) > maxAmount) { |
| | | proxy.$modal.msgWarning(`本次来票金额不得大于${maxAmount.toFixed(2)}元`); |
| | | form.ticketsAmount = maxAmount.toFixed(2); |
| | | form.ticketsNum = maxTicketsNum.value; |
| | | return; |
| | | } |
| | | |
| | | // 确保所有数值都转换为数字类型进行计算 |
| | | const ticketsNum = Number(val) / Number(form.taxInclusiveUnitPrice); |
| | | |
| | | // 检查来票数不能大于最大值 |
| | | if (ticketsNum > maxTicketsNum.value) { |
| | | proxy.$modal.msgWarning(`来票数不能大于${maxTicketsNum.value}`); |
| | | form.ticketsNum = maxTicketsNum.value; |
| | | form.ticketsAmount = maxAmount.toFixed(2); |
| | | return; |
| | | } |
| | | |
| | | form.ticketsNum = Number(ticketsNum.toFixed(2)); |
| | | |
| | | // 计算未来票数 |
| | | const newTicketsNum = form.ticketsNum - originalTicketsNum.value; |
| | | const futureTickets = Number(temFutureTickets.value) - newTicketsNum; |
| | | form.futureTickets = Number(futureTickets.toFixed(2)); |
| | | }; |
| | | |
| | | const open = async row => { |
| | |
| | | <el-button |
| | | type="primary" |
| | | link |
| | | @click="downLoadFile(row)" |
| | | @click="openEdit(row)" |
| | | > |
| | | 附件 |
| | | 编辑 |
| | | </el-button> |
| | | <el-button |
| | | type="primary" |
| | | link |
| | | @click="openEdit(row)" |
| | | @click="downLoadFile(row)" |
| | | > |
| | | 编辑 |
| | | 附件 |
| | | </el-button> |
| | | <el-button |
| | | type="primary" |
| | |
| | | width: 240, |
| | | }, |
| | | { |
| | | label: "产品大类", |
| | | prop: "productCategory", |
| | | width: 150, |
| | | }, |
| | | { |
| | | label: "规格型号", |
| | | prop: "specificationModel", |
| | | width: 150, |
| | |
| | | |
| | | <script setup> |
| | | import {ref} from "vue"; |
| | | import {getStaffJoinInfo, staffJoinAdd, staffJoinUpdate} from "@/api/personnelManagement/onboarding.js"; |
| | | // import {getStaffJoinInfo, staffJoinAdd, staffJoinUpdate} from "@/api/personnelManagement/onboarding.js"; |
| | | import {userListNoPageByTenantId} from "@/api/system/user.js"; |
| | | import {productionReport, productionReportUpdate} from "@/api/productionManagement/productionReporting.js"; |
| | | const { proxy } = getCurrentInstance() |
| | |
| | | @change="getModels" |
| | | :data="productOptions" |
| | | :render-after-expand="false" |
| | | :disabled="operationType === 'edit'" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="规格型号:" prop="model"> |
| | | <el-input v-model="form.model" placeholder="请输入" clearable/> |
| | | <el-form-item label="规格型号:" prop="productModelId"> |
| | | <el-select v-model="form.productModelId" placeholder="请选择" clearable :disabled="operationType === 'edit'" |
| | | filterable readonly @change="handleChangeModel"> |
| | | <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="单位:" prop="unit"> |
| | | <el-input v-model="form.unit" placeholder="请输入" clearable/> |
| | | <el-input v-model="form.unit" placeholder="请输入" disabled/> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | |
| | | <script setup> |
| | | import {ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue"; |
| | | import {getOptions} from "@/api/procurementManagement/procurementLedger.js"; |
| | | import {productTreeList} from "@/api/basicData/product.js"; |
| | | import {modelList, productTreeList} from "@/api/basicData/product.js"; |
| | | import {qualityInspectAdd, qualityInspectUpdate} from "@/api/qualityManagement/rawMaterialInspection.js"; |
| | | import {userListNoPage} from "@/api/system/user.js"; |
| | | import {qualityInspectDetailByProductId, getQualityTestStandardParamByTestStandardId} from "@/api/qualityManagement/metricMaintenance.js"; |
| | |
| | | checkName: "", |
| | | productName: "", |
| | | productId: "", |
| | | productModelId: "", |
| | | model: "", |
| | | testStandardId: "", |
| | | unit: "", |
| | |
| | | process: [{ required: true, message: "请输入", trigger: "blur" }], |
| | | checkName: [{ required: false, message: "请输入", trigger: "blur" }], |
| | | productId: [{ required: true, message: "请输入", trigger: "blur" }], |
| | | model: [{ required: false, message: "请输入", trigger: "blur" }], |
| | | productModelId: [{ required: false, message: "请选择", trigger: "change" }], |
| | | testStandardId: [{required: false, message: "请选择指标", trigger: "change"}], |
| | | unit: [{ required: false, message: "请输入", trigger: "blur" }], |
| | | quantity: [{ required: true, message: "请输入", trigger: "blur" }], |
| | |
| | | const userList = ref([]); |
| | | const currentProductId = ref(0); |
| | | const testStandardOptions = ref([]); // 指标选择下拉框数据 |
| | | const modelOptions = ref([]); |
| | | |
| | | // 打开弹框 |
| | | const openDialog = async (type, row) => { |
| | |
| | | }); |
| | | }; |
| | | const getModels = (value) => { |
| | | currentProductId.value = value |
| | | form.value.productModelId = undefined; |
| | | form.value.unit = undefined; |
| | | modelOptions.value = []; |
| | | currentProductId.value = value |
| | | form.value.productName = findNodeById(productOptions.value, value); |
| | | if (currentProductId.value) { |
| | | getList(); |
| | | } |
| | | modelList({ id: value }).then((res) => { |
| | | modelOptions.value = res; |
| | | }) |
| | | if (currentProductId.value) { |
| | | getList(); |
| | | } |
| | | }; |
| | | |
| | | const handleChangeModel = (value) => { |
| | | form.value.model = modelOptions.value.find(item => item.id == value)?.model || ''; |
| | | form.value.unit = modelOptions.value.find(item => item.id == value)?.unit || ''; |
| | | } |
| | | |
| | | const findNodeById = (nodes, productId) => { |
| | | for (let i = 0; i < nodes.length; i++) { |
| | | if (nodes[i].value === productId) { |
| | |
| | | @change="getModels" |
| | | :data="productOptions" |
| | | :render-after-expand="false" |
| | | :disabled="operationType === 'edit'" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="规格型号:" prop="model"> |
| | | <el-input v-model="form.model" placeholder="请输入" clearable/> |
| | | <el-form-item label="规格型号:" prop="productModelId"> |
| | | <el-select v-model="form.productModelId" placeholder="请选择" clearable :disabled="operationType === 'edit'" |
| | | filterable readonly @change="handleChangeModel"> |
| | | <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="单位:" prop="unit"> |
| | | <el-input v-model="form.unit" placeholder="请输入" clearable/> |
| | | <el-input v-model="form.unit" placeholder="请输入" disabled/> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | |
| | | <script setup> |
| | | import {ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue"; |
| | | import {getOptions} from "@/api/procurementManagement/procurementLedger.js"; |
| | | import {productTreeList} from "@/api/basicData/product.js"; |
| | | import {modelList, productTreeList} from "@/api/basicData/product.js"; |
| | | import {qualityInspectAdd, qualityInspectUpdate} from "@/api/qualityManagement/rawMaterialInspection.js"; |
| | | import {qualityInspectDetailByProductId, getQualityTestStandardParamByTestStandardId} from "@/api/qualityManagement/metricMaintenance.js"; |
| | | import {userListNoPage} from "@/api/system/user.js"; |
| | |
| | | checkName: "", |
| | | productName: "", |
| | | productId: "", |
| | | productModelId: "", |
| | | model: "", |
| | | testStandardId: "", |
| | | unit: "", |
| | |
| | | process: [{ required: true, message: "请输入工序", trigger: "blur" }], |
| | | checkName: [{ required: false, message: "请输入", trigger: "blur" }], |
| | | productId: [{ required: true, message: "请输入", trigger: "blur" }], |
| | | model: [{ required: false, message: "请输入", trigger: "blur" }], |
| | | productModelId: [{ required: false, message: "请选择", trigger: "change" }], |
| | | testStandardId: [{required: false, message: "请选择指标", trigger: "change"}], |
| | | unit: [{ required: false, message: "请输入", trigger: "blur" }], |
| | | quantity: [{ required: true, message: "请输入", trigger: "blur" }], |
| | |
| | | const tableLoading = ref(false); |
| | | const currentProductId = ref(0); |
| | | const testStandardOptions = ref([]); // 指标选择下拉框数据 |
| | | const modelOptions = ref([]); |
| | | |
| | | // 打开弹框 |
| | | const openDialog = async (type, row) => { |
| | |
| | | }); |
| | | }; |
| | | const getModels = (value) => { |
| | | currentProductId.value = value |
| | | form.value.productModelId = undefined; |
| | | form.value.unit = undefined; |
| | | modelOptions.value = []; |
| | | currentProductId.value = value |
| | | form.value.productName = findNodeById(productOptions.value, value); |
| | | if (currentProductId.value) { |
| | | getList(); |
| | | } |
| | | modelList({ id: value }).then((res) => { |
| | | modelOptions.value = res; |
| | | }) |
| | | if (currentProductId.value) { |
| | | getList(); |
| | | } |
| | | }; |
| | | |
| | | const handleChangeModel = (value) => { |
| | | form.value.model = modelOptions.value.find(item => item.id == value)?.model || ''; |
| | | form.value.unit = modelOptions.value.find(item => item.id == value)?.unit || ''; |
| | | } |
| | | |
| | | const findNodeById = (nodes, productId) => { |
| | | for (let i = 0; i < nodes.length; i++) { |
| | | if (nodes[i].value === productId) { |
| | |
| | | }; |
| | | const getModels = (value) => { |
| | | form.value.productModelId = undefined; |
| | | form.value.unit = undefined; |
| | | modelOptions.value = []; |
| | | currentProductId.value = value |
| | | form.value.productName = findNodeById(productOptions.value, value); |
| | |
| | | @change="handleChange" |
| | | > |
| | | <el-radio-button :label="1">原材料</el-radio-button> |
| | | <el-radio-button :label="2">半成品</el-radio-button> |
| | | <el-radio-button :label="3">成品</el-radio-button> |
| | | <el-radio-button :label="3">半成品</el-radio-button> |
| | | <el-radio-button :label="2">成品</el-radio-button> |
| | | </el-radio-group> |
| | | </template> |
| | | |
| | |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import PanelHeader from './PanelHeader.vue' |
| | | import ProductTypeSwitch from './ProductTypeSwitch.vue' |
| | | import { productInOutAnalysis } from '@/api/viewIndex.js' |
| | | |
| | | const productType = ref(1) // 1=原材料 2=半成品 3=成品 |
| | | |
| | |
| | | const xAxis1 = ref([ |
| | | { |
| | | type: 'category', |
| | | data: ['6/9', '6/10', '6/11', '6/12', '6/13', '6/14', '6/15'], |
| | | data: [], |
| | | axisTick: { show: false }, |
| | | axisLine: { show: false,lineStyle: { color: 'rgba(184, 200, 224, 0.3)' } }, |
| | | axisLine: { show: false, lineStyle: { color: 'rgba(184, 200, 224, 0.3)' } }, |
| | | axisLabel: { color: '#B8C8E0', fontSize: 12 }, |
| | | splitLine: { show: false, lineStyle: { type: 'dashed', color: 'rgba(184, 200, 224, 0.2)' } }, |
| | | }, |
| | |
| | | axisLine: { show: false }, |
| | | axisTick: { show: false }, |
| | | axisLabel: { color: '#B8C8E0', fontSize: 12 }, |
| | | splitLine: { lineStyle: { color: '#B8C8E0' } }, |
| | | splitLine: { lineStyle: { color: '#B8C8E0' } }, |
| | | }, |
| | | ] |
| | | |
| | |
| | | showSymbol: true, |
| | | symbol: 'circle', |
| | | symbolSize: 8, |
| | | lineStyle: { color: 'rgba(11, 137, 254,1', width: 2 }, |
| | | lineStyle: { color: 'rgba(11, 137, 254, 1)', width: 2 }, |
| | | itemStyle: { color: 'rgba(11, 137, 254, 1)', borderWidth: 0 }, |
| | | areaStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | |
| | | { offset: 1, color: 'rgba(11, 137, 254, 0.05)' }, |
| | | ]), |
| | | }, |
| | | data: [80, 100, 140, 160, 120, 150, 180], |
| | | data: [], |
| | | emphasis: { focus: 'series' }, |
| | | }, |
| | | { |
| | |
| | | showSymbol: true, |
| | | symbol: 'circle', |
| | | symbolSize: 8, |
| | | |
| | | lineStyle: { color: 'rgba(11, 249, 254, 1)', width: 2 }, |
| | | itemStyle: { color: 'rgba(11, 249, 254, 1)', borderWidth: 0 }, |
| | | areaStyle: { |
| | |
| | | { offset: 1, color: 'rgba(11, 249, 254, 0.05)' }, |
| | | ]), |
| | | }, |
| | | data: [160, 200, 200, 200, 170, 200, 200], |
| | | data: [], |
| | | emphasis: { focus: 'series' }, |
| | | }, |
| | | ]) |
| | |
| | | }, |
| | | } |
| | | |
| | | const handleFilterChange = () => { |
| | | // 可按 productType 切换后请求出入库接口,此处仅预留 |
| | | const fetchData = () => { |
| | | productInOutAnalysis({ type: productType.value }) |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | const list = res.data |
| | | xAxis1.value[0].data = list.map((d) => d.date) |
| | | lineSeries.value[0].data = list.map((d) => Number(d.outCount) || 0) |
| | | lineSeries.value[1].data = list.map((d) => Number(d.inCount) || 0) |
| | | } |
| | | }) |
| | | .catch((err) => { |
| | | console.error('获取产品出入库分析失败:', err) |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => {}) |
| | | const handleFilterChange = () => { |
| | | fetchData() |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchData() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | |
| | | <script setup> |
| | | import { ref, onMounted } from 'vue' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import { customerRevenueAnalysis } from '@/api/viewIndex.js' |
| | | import { listCustomer } from '@/api/basicData/customerFile.js' |
| | | |
| | | const dateType = ref(1) |
| | | const customerValue = ref(null) |
| | | const customerOptions = ref([]) |
| | | import { productTurnoverDays } from '@/api/viewIndex.js' |
| | | |
| | | const chartStyle = { width: '100%', height: '100%' } |
| | | const grid = { left: '3%', right: '4%', bottom: '3%', top: '4%', containLabel: true } |
| | | const barLegend = { show: false, textStyle: { color: '#B8C8E0' }, data: ['营收'] } |
| | | const barLegend = { show: false, textStyle: { color: '#B8C8E0' }, data: ['周转天数'] } |
| | | const barSeries1 = ref([ |
| | | { |
| | | name: '营收', |
| | | name: '周转天数', |
| | | type: 'bar', |
| | | barGap: 0, |
| | | barWidth: 30, |
| | |
| | | formatter(params) { |
| | | let result = params[0].axisValueLabel + '<br/>' |
| | | params.forEach((item) => { |
| | | result += `<div>${item.marker} ${item.seriesName}: ${item.value}</div>` |
| | | result += `<div>${item.marker} ${item.seriesName}: ${item.value} 天</div>` |
| | | }) |
| | | return result |
| | | }, |
| | |
| | | const xAxis1 = ref([{ type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] }]) |
| | | const yAxis1 = [{ type: 'value', axisLabel: { color: '#B8C8E0' } }] |
| | | |
| | | const getCustomerRevenueAnalysis = () => { |
| | | if (customerOptions.value.length > 0 && !customerValue.value) customerValue.value = customerOptions.value[0].value |
| | | if (!customerValue.value) return |
| | | customerRevenueAnalysis({ customerId: customerValue.value, type: dateType.value }) |
| | | const fetchData = () => { |
| | | productTurnoverDays() |
| | | .then((res) => { |
| | | xAxis1.value[0].data = [] |
| | | barSeries1.value[0].data = [] |
| | | const items = res.data?.items || [] |
| | | items.forEach((item) => { |
| | | xAxis1.value[0].data.push(item.name) |
| | | barSeries1.value[0].data.push(item.value) |
| | | }) |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | const list = res.data |
| | | xAxis1.value[0].data = list.map((d) => d.name) |
| | | barSeries1.value[0].data = list.map((d) => Number(d.value) || 0) |
| | | } |
| | | }) |
| | | .catch((e) => console.error('获取客户营收分析失败:', e)) |
| | | } |
| | | |
| | | const fetchCustomerOptions = async () => { |
| | | try { |
| | | const res = await listCustomer({ pageNum: 1, pageSize: 200 }) |
| | | const records = res?.records || res?.data?.records || res?.rows || [] |
| | | customerOptions.value = records.map((r) => ({ |
| | | label: r.customerName || r.name || r.customer || '-', |
| | | value: r.id ?? r.customerId ?? r.customerCode ?? r.customerName, |
| | | })) |
| | | if (customerOptions.value.length > 0 && !customerValue.value) { |
| | | customerValue.value = customerOptions.value[0].value |
| | | getCustomerRevenueAnalysis() |
| | | } |
| | | } catch (e) { |
| | | customerOptions.value = [ |
| | | { label: '华东精密', value: '华东精密' }, |
| | | { label: '星辰电子', value: '星辰电子' }, |
| | | { label: '启航科技', value: '启航科技' }, |
| | | { label: '铭诚制造', value: '铭诚制造' }, |
| | | { label: '远景材料', value: '远景材料' }, |
| | | ] |
| | | } |
| | | .catch((err) => { |
| | | console.error('获取产品周转天数失败:', err) |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchCustomerOptions() |
| | | fetchData() |
| | | }) |
| | | </script> |
| | | |
| | |
| | | <div> |
| | | <!-- 顶部统计卡片 --> |
| | | <div class="stats-cards"> |
| | | <div class="stat-card"> |
| | | <div |
| | | v-for="item in statItems" |
| | | :key="item.name" |
| | | class="stat-card" |
| | | > |
| | | <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" /> |
| | | <div class="card-content"> |
| | | <span class="card-label">销售产品数</span> |
| | | <span class="card-value">{{ totalStaff }}</span> |
| | | <div class="card-compare" :class="compareClass(staffYoY)"> |
| | | <span class="card-label">{{ item.name }}</span> |
| | | <span class="card-value">{{ item.value }}</span> |
| | | <div class="card-compare" :class="compareClass(Number(item.rate))"> |
| | | <span>同比</span> |
| | | <span class="compare-value">{{ formatPercent(staffYoY) }}</span> |
| | | <span class="compare-icon">{{ staffYoY >= 0 ? '↑' : '↓' }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="stat-card"> |
| | | <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" /> |
| | | <div class="card-content"> |
| | | <span class="card-label">采购产品数</span> |
| | | <span class="card-value">{{ totalCustomers }}</span> |
| | | <div class="card-compare" :class="compareClass(customersYoY)"> |
| | | <span>同比</span> |
| | | <span class="compare-value">{{ formatPercent(customersYoY) }}</span> |
| | | <span class="compare-icon">{{ customersYoY >= 0 ? '↑' : '↓' }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="stat-card"> |
| | | <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" /> |
| | | <div class="card-content"> |
| | | <span class="card-label">库存数</span> |
| | | <span class="card-value">{{ totalSuppliers }}</span> |
| | | <div class="card-compare" :class="compareClass(suppliersYoY)"> |
| | | <span>同比</span> |
| | | <span class="compare-value">{{ formatPercent(suppliersYoY) }}</span> |
| | | <span class="compare-icon">{{ suppliersYoY >= 0 ? '↑' : '↓' }}</span> |
| | | <span class="compare-value">{{ formatPercent(item.rate) }}</span> |
| | | <span class="compare-icon">{{ Number(item.rate) >= 0 ? '↑' : '↓' }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | |
| | | <script setup> |
| | | import { ref, onMounted } from 'vue' |
| | | import { summaryStatistics } from '@/api/viewIndex.js' |
| | | import { salesPurchaseStorageProductCount } from '@/api/viewIndex.js' |
| | | |
| | | // 统计数据 |
| | | const totalStaff = ref(0) |
| | | const totalCustomers = ref(0) |
| | | const totalSuppliers = ref(0) |
| | | // 同比 |
| | | const staffYoY = ref(0) |
| | | const customersYoY = ref(0) |
| | | const suppliersYoY = ref(0) |
| | | const statItems = ref([]) |
| | | |
| | | const formatPercent = (val) => { |
| | | const num = Number(val) || 0 |
| | | return `${Math.abs(num).toFixed(2)}%` |
| | | return `${num.toFixed(2)}%` |
| | | } |
| | | |
| | | const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down') |
| | | |
| | | // 获取员工、客户、供应商数量 |
| | | const getNum = () => { |
| | | summaryStatistics().then((res) => { |
| | | totalStaff.value = res.data.totalStaff |
| | | staffYoY.value = res.data.staffGrowthRate |
| | | totalCustomers.value = res.data.totalCustomer |
| | | customersYoY.value = res.data.customerGrowthRate |
| | | totalSuppliers.value = res.data.totalSupplier |
| | | suppliersYoY.value = res.data.supplierGrowthRate |
| | | }).catch(err => { |
| | | console.error('获取基础统计数据失败:', err) |
| | | }) |
| | | const fetchData = () => { |
| | | salesPurchaseStorageProductCount() |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | statItems.value = res.data.map((item) => ({ |
| | | name: item.name, |
| | | value: item.value, |
| | | rate: item.rate, |
| | | })) |
| | | } |
| | | }) |
| | | .catch((err) => { |
| | | console.error('获取销售/采购/储存产品数失败:', err) |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getNum() |
| | | fetchData() |
| | | }) |
| | | </script> |
| | | |
| | |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import PanelHeader from './PanelHeader.vue' |
| | | import CarouselCards from './CarouselCards.vue' |
| | | import { productCategoryDistribution } from '@/api/viewIndex.js' |
| | | import { rawMaterialPurchaseAmountRatio } from '@/api/viewIndex.js' |
| | | |
| | | /** |
| | | * @introduction 把数组中key值相同的那一项提取出来,组成一个对象 |
| | |
| | | // 卡片数据 |
| | | const cardItems = ref([]) |
| | | |
| | | // 假数据 |
| | | const mockCardData = [ |
| | | { name: '电子产品', value: 156, rate: '28.5' }, |
| | | { name: '机械设备', value: 132, rate: '24.1' }, |
| | | { name: '原材料', value: 98, rate: '17.9' }, |
| | | { name: '化工产品', value: 87, rate: '15.9' }, |
| | | { name: '纺织品', value: 45, rate: '8.2' }, |
| | | { name: '其他', value: 31, rate: '5.7' }, |
| | | ] |
| | | |
| | | // 颜色列表 |
| | | const landColors = ['#26FFCB', '#24CBFF', '#35FBF4', '#2651FF', '#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF'] |
| | | |
| | |
| | | return { |
| | | orient: 'vertical', |
| | | top: 'center', |
| | | left: '60%', |
| | | left: '52%', |
| | | itemGap: 30, |
| | | data: data, |
| | | formatter: function (name) { |
| | | const item = landObjData.value[name] |
| | | if (!item) return name |
| | | return `{title|${name}}{value|${item.value}}{unit|件}{percent|${item.rate}}{unit|%}` |
| | | return `{title|${name}}{value|${item.value}}{unit|元}{percent|${item.rate}}{unit|%}` |
| | | }, |
| | | textStyle: { |
| | | rich: { |
| | |
| | | color: '#43e8fc', |
| | | fontSize: 14, |
| | | fontWeight: 600, |
| | | padding: [0, 0, 0, 30], |
| | | padding: [0, 0, 0, 10], |
| | | }, |
| | | unit: { |
| | | color: '#82baff', |
| | |
| | | // 提示框 |
| | | const landTooltip = { |
| | | trigger: 'item', |
| | | formatter: '{a} <br/>{b} : {c} ({d}%)', |
| | | formatter: '{a} <br/>{b} : {c}元 ({d}%)', |
| | | } |
| | | |
| | | // 双层环形饼图 |
| | |
| | | textStyle: { color: '#B8C8E0' }, |
| | | } |
| | | |
| | | const setMockData = () => { |
| | | // 卡片数据 |
| | | cardItems.value = mockCardData.map(item => ({ |
| | | label: item.name, |
| | | value: item.value, |
| | | unit: '件', |
| | | rate: item.rate |
| | | })) |
| | | // 图表数据 |
| | | dataList.value = mockCardData.map((it) => ({ |
| | | name: it.name, |
| | | value: Number(it.value || 0), |
| | | rate: it.rate, |
| | | children: [], |
| | | })) |
| | | landSeries.value[0].data = dataList.value |
| | | } |
| | | |
| | | const loadData = async () => { |
| | | setMockData() |
| | | // try { |
| | | // const res = await productCategoryDistribution() |
| | | // const items = res?.data?.items || [] |
| | | // dataList.value = items.map((it) => ({ |
| | | // name: it.name, |
| | | // value: Number(it.value || 0), |
| | | // rate: it.rate, |
| | | // children: Array.isArray(it.children) ? it.children : [], |
| | | // })) |
| | | // // 卡片数据 |
| | | // cardItems.value = items.map(item => ({ |
| | | // label: item.name, |
| | | // value: parseInt(item.value), |
| | | // unit: '件', |
| | | // rate: item.rate |
| | | // })) |
| | | // landLegend.data = dataList.value.map((d) => d.name) |
| | | // landSeries.value[0].data = dataList.value |
| | | // } catch (e) { |
| | | // console.error('获取产品大类分布失败:', e) |
| | | // setMockData() |
| | | // } |
| | | const fetchData = () => { |
| | | rawMaterialPurchaseAmountRatio() |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | const items = res.data |
| | | cardItems.value = items.map((item) => ({ |
| | | label: item.name, |
| | | value: item.value, |
| | | unit: '元', |
| | | rate: item.rate, |
| | | })) |
| | | dataList.value = items.map((it) => ({ |
| | | name: it.name, |
| | | value: parseFloat(it.value) || 0, |
| | | rate: it.rate, |
| | | children: [], |
| | | })) |
| | | landSeries.value[0].data = dataList.value |
| | | } |
| | | }) |
| | | .catch((err) => { |
| | | console.error('获取原材料采购金额占比失败:', err) |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadData() |
| | | fetchData() |
| | | }) |
| | | </script> |
| | | |
| | |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, computed } from 'vue' |
| | | import { deptStaffDistribution } from '@/api/viewIndex.js' |
| | | import { productSalesAnalysis } from '@/api/viewIndex.js' |
| | | import PanelHeader from './PanelHeader.vue' |
| | | import CarouselCards from './CarouselCards.vue' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | |
| | | return { |
| | | orient: 'vertical', |
| | | top: 'center', |
| | | left: '60%', |
| | | left: '52%', |
| | | itemGap: 30, |
| | | data: data, |
| | | formatter: function (name) { |
| | | const item = pieObjData.value[name] |
| | | if (!item) return name |
| | | return `{title|${name}}{value|${item.value}}{unit|人}{percent|${item.rate}}{unit|%}` |
| | | return `{title|${name}}{value|${item.value}}{unit|元}{percent|${item.rate}}{unit|%}` |
| | | }, |
| | | textStyle: { |
| | | rich: { |
| | |
| | | color: '#43e8fc', |
| | | fontSize: 14, |
| | | fontWeight: 600, |
| | | padding: [0, 0, 0, 30], |
| | | padding: [0, 0, 0, 10], |
| | | }, |
| | | unit: { |
| | | color: '#82baff', |
| | |
| | | |
| | | const pieTooltip = { |
| | | trigger: 'item', |
| | | formatter: '{a} <br/>{b} : {c} ({d}%)', |
| | | formatter: '{a} <br/>{b} : {c}元 ({d}%)', |
| | | } |
| | | |
| | | const pieSeries = computed(() => [ |
| | |
| | | |
| | | const cardItems = ref([]) |
| | | |
| | | // 假数据 |
| | | const mockData = [ |
| | | { name: '生产部', value: 125, rate: '35.2' }, |
| | | { name: '技术部', value: 85, rate: '23.9' }, |
| | | { name: '销售部', value: 65, rate: '18.3' }, |
| | | { name: '财务部', value: 32, rate: '9.0' }, |
| | | { name: '人事部', value: 28, rate: '7.9' }, |
| | | { name: '行政部', value: 20, rate: '5.6' }, |
| | | ] |
| | | |
| | | const getDeptStaffDistribution = () => { |
| | | setMockData() |
| | | // deptStaffDistribution().then(res => { |
| | | // if (res.code === 200) { |
| | | // const items = res.data.items || [] |
| | | // // 卡片数据 |
| | | // cardItems.value = items.map(item => ({ |
| | | // label: item.name, |
| | | // value: parseInt(item.value), |
| | | // unit: '人' |
| | | // })) |
| | | // // 图表数据 |
| | | // pieDatas.value = items.map(item => ({ |
| | | // name: item.name, |
| | | // value: parseInt(item.value), |
| | | // rate: item.rate |
| | | // })) |
| | | // } else { |
| | | // // 使用假数据 |
| | | // setMockData() |
| | | // } |
| | | // }).catch(err => { |
| | | // console.error('获取部门人员分布数据失败:', err) |
| | | // // 使用假数据 |
| | | // setMockData() |
| | | // }) |
| | | } |
| | | |
| | | const setMockData = () => { |
| | | // 卡片数据 |
| | | cardItems.value = mockData.map(item => ({ |
| | | label: item.name, |
| | | value: item.value, |
| | | unit: '人' |
| | | })) |
| | | // 图表数据 |
| | | pieDatas.value = mockData.map(item => ({ |
| | | name: item.name, |
| | | value: item.value, |
| | | rate: item.rate |
| | | })) |
| | | const fetchData = () => { |
| | | productSalesAnalysis() |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | const items = res.data |
| | | cardItems.value = items.map((item) => ({ |
| | | label: item.name, |
| | | value: item.value, |
| | | unit: '元', |
| | | rate: item.rate, |
| | | })) |
| | | pieDatas.value = items.map((item) => ({ |
| | | name: item.name, |
| | | value: parseFloat(item.value) || 0, |
| | | rate: item.rate, |
| | | })) |
| | | } |
| | | }) |
| | | .catch((err) => { |
| | | console.error('获取产品销售金额分析失败:', err) |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getDeptStaffDistribution() |
| | | fetchData() |
| | | }) |
| | | </script> |
| | | |
| | |
| | | border: 1px solid #1a58b0; |
| | | padding: 18px; |
| | | height: 240px; |
| | | padding-top: 0px; |
| | | } |
| | | |
| | | .equipment-header { |
| | |
| | | formatter: function (params) { |
| | | let result = params[0].axisValueLabel + '<br/>' |
| | | params.forEach((item) => { |
| | | result += `<div>${item.marker} ${item.seriesName}: ${item.value}</div>` |
| | | result += `<div>${item.marker} ${item.seriesName}: ${item.value}元</div>` |
| | | }) |
| | | return result |
| | | }, |
| | |
| | | textStyle: { fontSize: '100%' }, |
| | | formatter: function (params) { |
| | | let result = params[0].axisValueLabel + '<br/>' |
| | | result += `<div>${params[0].marker}${params[0].value}</div>` |
| | | result += `<div>${params[0].marker}${params[0].value}元</div>` |
| | | return result |
| | | }, |
| | | } |
| | |
| | | specificationModel: row.specificationModel || "", |
| | | pendingInvoiceTotal: Number(row.pendingInvoiceTotal || 0), |
| | | taxRate: row.taxRate ?? "", |
| | | receiptPaymentAmount: "", |
| | | // 默认本次回款金额 = 待回款金额 |
| | | receiptPaymentAmount: Number(row.pendingInvoiceTotal || 0), |
| | | receiptPaymentType: "", |
| | | registrant: userStore.nickName, |
| | | receiptPaymentDate: "", |
| | |
| | | |
| | | // 保存回款记录 |
| | | const saveReceiptPayment = (row) => { |
| | | // 子表回款金额合计校验:所有回款记录金额之和不能大于父数据合同金额 |
| | | // 这里父数据“合同金额”按:已回款金额( invoiceTotal ) + 待回款金额( pendingInvoiceTotal ) 计算 |
| | | const findParentRowByChildId = (childId) => { |
| | | return tableData.value.find((p) => |
| | | Array.isArray(p.children) && p.children.some((c) => c.id === childId) |
| | | ); |
| | | }; |
| | | const parentRow = findParentRowByChildId(row.id); |
| | | if (parentRow) { |
| | | const contractAmount = |
| | | Number(parentRow.invoiceTotal || 0) + Number(parentRow.pendingInvoiceTotal || 0); |
| | | const sumReceipt = (parentRow.children || []).reduce((sum, item) => { |
| | | const val = Number(item?.receiptPaymentAmount ?? 0); |
| | | return sum + (Number.isFinite(val) ? val : 0); |
| | | }, 0); |
| | | if (sumReceipt > contractAmount) { |
| | | proxy.$modal.msgError( |
| | | `回款金额合计(${sumReceipt.toFixed(2)})不能大于合同金额(${contractAmount.toFixed(2)})` |
| | | ); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | let updateData = { |
| | | id: row.id, |
| | | receiptPaymentType: row.receiptPaymentType, |