| | |
| | | style="width: 200px" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="产品规格:"> |
| | | <el-select |
| | | v-model="searchForm.specificationModel" |
| | | placeholder="请选择产品规格" |
| | | clearable |
| | | filterable |
| | | style="width: 200px" |
| | | > |
| | | <el-option |
| | | v-for="m in modelOptions" |
| | | :key="m.value" |
| | | :label="m.label" |
| | | :value="m.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="handleSearch" :loading="loading"> |
| | | 查询 |
| | |
| | | <div class="summary-stats"> |
| | | <div class="stat-item"> |
| | | <span class="stat-label">采购总额:</span> |
| | | <span class="stat-value">¥{{ businessSummaryStats.totalAmount.toLocaleString() }}</span> |
| | | <span class="stat-value">¥{{ formatStatAmount(businessSummaryStats.totalAmount) }}</span> |
| | | </div> |
| | | <div class="stat-item"> |
| | | <span class="stat-label">商品种类:</span> |
| | |
| | | </div> |
| | | <div class="stat-item"> |
| | | <span class="stat-label">退款总额:</span> |
| | | <span class="stat-value">{{ businessSummaryStats.returnAmount }}</span> |
| | | <span class="stat-value">¥{{ formatStatAmount(businessSummaryStats.returnAmount) }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | // 搜索表单 |
| | | const searchForm = reactive({ |
| | | dateRange: [], |
| | | productCategory: '' |
| | | productCategory: '', |
| | | specificationModel: '' |
| | | }) |
| | | |
| | | // 产品类别树选项 |
| | | const productOptions = ref([]) |
| | | |
| | | // 产品规格选项(按报表实际出现的规格动态生成) |
| | | const modelOptions = ref([]) |
| | | |
| | | // 统计数据 |
| | | const businessSummaryStats = ref({ |
| | | totalAmount: 0, |
| | | returnAmount: 0, |
| | | productTypes: 0 |
| | | }) |
| | | |
| | | // 数值展示口径:后端返回 6 位小数(如 211.970000、¥3,420,674.200000), |
| | | // 页面按后端实际位数展示即可,不再补齐 6 位——多余的结尾 0 一律去掉。 |
| | | // maximumFractionDigits 保留 6,是为了兜住前端自己 reduce 求和时的浮点尾数(如 3420674.2000000002)。 |
| | | const NUM_FMT = { maximumFractionDigits: 6 } |
| | | |
| | | // 数量/金额统一格式化:带千分位,去掉结尾多余的 0;空值按 0 处理 |
| | | const formatNum = (val, prefix = '') => { |
| | | const num = parseFloat(val) |
| | | if (val === null || val === undefined || val === '' || Number.isNaN(num)) { |
| | | return `${prefix}0` |
| | | } |
| | | return `${prefix}${num.toLocaleString('zh-CN', NUM_FMT)}` |
| | | } |
| | | |
| | | // 汇总金额展示 |
| | | const formatStatAmount = (val) => { |
| | | return formatNum(val) |
| | | } |
| | | |
| | | // 表格列配置(根据后端字段定义) |
| | | const tableColumns = ref([ |
| | |
| | | label: '采购数量', |
| | | prop: 'purchaseNum', |
| | | width: 120, |
| | | formatData: (val) => { |
| | | return val ? parseFloat(val).toLocaleString() : '0' |
| | | } |
| | | formatData: (val) => formatNum(val) |
| | | }, |
| | | { |
| | | label: '退货数量', |
| | | prop: 'returnQuantity', |
| | | width: 120, |
| | | formatData: (val) => { |
| | | return val ? parseFloat(val).toLocaleString() : '0' |
| | | } |
| | | formatData: (val) => formatNum(val) |
| | | }, |
| | | { |
| | | label: '退货金额', |
| | | prop: 'returnAmount', |
| | | width: 120, |
| | | formatData: (val) => { |
| | | return val ? `¥${parseFloat(val).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : '¥0.00' |
| | | } |
| | | formatData: (val) => formatNum(val, '¥') |
| | | }, |
| | | { |
| | | label: '退款储量', |
| | | prop: 'purchaseAmount', |
| | | formatData: (val) => { |
| | | return val ? `¥${parseFloat(val).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : '¥0.00' |
| | | } |
| | | formatData: (val) => formatNum(val, '¥') |
| | | }, |
| | | { |
| | | label: '采购次数', |
| | |
| | | label: '平均单价', |
| | | prop: 'averagePrice', |
| | | width: 120, |
| | | formatData: (val) => { |
| | | return val ? `¥${parseFloat(val).toFixed(2)}` : '¥0.00' |
| | | } |
| | | formatData: (val) => formatNum(val, '¥') |
| | | }, |
| | | { |
| | | label: '供应商名称', |
| | |
| | | try { |
| | | loading.value = true |
| | | const params = {} |
| | | |
| | | |
| | | // 时间范围 |
| | | if (searchForm.dateRange && searchForm.dateRange.length === 2) { |
| | | params.entryDateStart = searchForm.dateRange[0] |
| | | params.entryDateEnd = searchForm.dateRange[1] |
| | | } |
| | | |
| | | // 产品类别 |
| | | |
| | | // 产品大类 |
| | | if (searchForm.productCategory) { |
| | | const categoryName = findNodeLabelById(productOptions.value, searchForm.productCategory) |
| | | if (categoryName) { |
| | | params.productCategory = categoryName |
| | | } |
| | | } |
| | | |
| | | // 分页参数 |
| | | params.current = page.current |
| | | params.size = page.size |
| | | |
| | | // 报表按「产品大类+规格+供应商」聚合,数据量小,一次取回后按规格本地过滤 |
| | | params.current = 1 |
| | | params.size = 1000 |
| | | |
| | | const res = await procurementBusinessSummaryListPage(params) |
| | | if (res && res.data) { |
| | | // 兼容后端可能直接返回数组/或返回分页对象 |
| | | businessSummaryData.value = Array.isArray(res.data) ? res.data : (res.data.records || []) |
| | | const payload = res && res.data !== undefined ? res.data : res |
| | | let rows = Array.isArray(payload) ? payload : ((payload && payload.records) || []) |
| | | |
| | | if (!Array.isArray(res.data)) { |
| | | page.total = Number(res.data.total ?? 0) |
| | | page.current = Number(res.data.current ?? page.current) |
| | | page.size = Number(res.data.size ?? page.size) |
| | | } |
| | | |
| | | // 计算统计数据 |
| | | if (businessSummaryData.value.length > 0) { |
| | | businessSummaryStats.value.totalAmount = businessSummaryData.value.reduce((sum, item) => { |
| | | return sum + (parseFloat(item.purchaseAmount) || 0) |
| | | }, 0) |
| | | businessSummaryStats.value.returnAmount = businessSummaryData.value.reduce((sum, item) => { |
| | | return sum + (parseFloat(item.returnAmount) || 0) |
| | | }, 0) |
| | | businessSummaryStats.value.productTypes = businessSummaryData.value.length |
| | | } else { |
| | | businessSummaryStats.value = { |
| | | totalAmount: 0, |
| | | productTypes: 0 |
| | | } |
| | | } |
| | | // 规格下拉选项随当前查询结果动态生成 |
| | | const specSet = [] |
| | | rows.forEach((item) => { |
| | | const sp = item.specificationModel |
| | | if (sp && specSet.indexOf(sp) === -1) specSet.push(sp) |
| | | }) |
| | | modelOptions.value = specSet.sort().map((sp) => ({ label: sp, value: sp })) |
| | | |
| | | // 产品规格过滤 |
| | | if (searchForm.specificationModel) { |
| | | rows = rows.filter((item) => item.specificationModel === searchForm.specificationModel) |
| | | } |
| | | |
| | | page.total = rows.length |
| | | if (page.current < 1) page.current = 1 |
| | | const startIdx = (page.current - 1) * page.size |
| | | businessSummaryData.value = rows.slice(startIdx, startIdx + page.size) |
| | | |
| | | // 统计按全量过滤结果口径 |
| | | businessSummaryStats.value.totalAmount = rows.reduce((sum, item) => { |
| | | return sum + (parseFloat(item.purchaseAmount) || 0) |
| | | }, 0) |
| | | businessSummaryStats.value.returnAmount = rows.reduce((sum, item) => { |
| | | return sum + (parseFloat(item.returnAmount) || 0) |
| | | }, 0) |
| | | businessSummaryStats.value.productTypes = rows.length |
| | | } catch (error) { |
| | | console.error('查询失败:', error) |
| | | } finally { |
| | |
| | | const resetSearch = () => { |
| | | Object.assign(searchForm, { |
| | | dateRange: [], |
| | | productCategory: '' |
| | | productCategory: '', |
| | | specificationModel: '' |
| | | }) |
| | | page.current = 1 |
| | | handleSearch() |
| | |
| | | |
| | | |
| | | onMounted(() => { |
| | | // 初始化产品类别树 |
| | | // 初始化产品类别树、产品规格 |
| | | getProductOptions() |
| | | |
| | | // 设置默认时间范围为最近30天 |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .app-container { |
| | | padding: 20px; |
| | | background-color: #f5f7fa; |
| | | min-height: 100vh; |
| | | } |
| | | |
| | | .page-header { |
| | | text-align: center; |
| | | margin-bottom: 20px; |