From db42d47f5692ef64e5436c5a6d29dcb537b44596 Mon Sep 17 00:00:00 2001
From: zouyu <2723363702@qq.com>
Date: 星期一, 26 一月 2026 16:36:13 +0800
Subject: [PATCH] 浪潮对接单点登录:mis调整
---
src/views/salesManagement/indicatorStats/index.vue | 399 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 399 insertions(+), 0 deletions(-)
diff --git a/src/views/salesManagement/indicatorStats/index.vue b/src/views/salesManagement/indicatorStats/index.vue
new file mode 100644
index 0000000..65dae96
--- /dev/null
+++ b/src/views/salesManagement/indicatorStats/index.vue
@@ -0,0 +1,399 @@
+<template>
+ <div class="app-container indicator-stats">
+ <el-card class="box-card">
+ <!-- KPI 姹囨�� -->
+ <el-row :gutter="20" class="stats-row">
+ <el-col :span="8">
+ <div class="stat-card">
+ <div class="stat-icon" style="background: #ecf5ff;">
+ <el-icon :size="30" color="#409eff"><Document /></el-icon>
+ </div>
+ <div class="stat-content">
+ <div class="stat-value">{{ indicatorKpis.orderCount.toLocaleString() }}</div>
+ <div class="stat-label">璁㈠崟鏁伴噺</div>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="8">
+ <div class="stat-card">
+ <div class="stat-icon" style="background: #f0f9ff;">
+ <el-icon :size="30" color="#67c23a"><Tickets /></el-icon>
+ </div>
+ <div class="stat-content">
+ <div class="stat-value">楼{{ indicatorKpis.salesAmount.toLocaleString() }}</div>
+ <div class="stat-label">閿�鍞</div>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="8">
+ <div class="stat-card">
+ <div class="stat-icon" style="background: #fef0f0;">
+ <el-icon :size="30" color="#e6a23c"><Van /></el-icon>
+ </div>
+ <div class="stat-content">
+ <div class="stat-value">{{ indicatorKpis.shipRate }}%</div>
+ <div class="stat-label">鍙戣揣鐜�</div>
+ </div>
+ </div>
+ </el-col>
+ </el-row>
+
+ <!-- 缁村害绛涢�� -->
+ <el-row :gutter="20" class="search-row">
+ <el-col :span="6">
+ <el-tree-select v-model="indicatorFilter.productCategory" placeholder="浜у搧绫诲埆" clearable check-strictly
+ :data="productOptions" :render-after-expand="false" style="width: 100%" />
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="indicatorFilter.customerName" placeholder="瀹㈡埛" clearable filterable>
+ <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.customerName" />
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-date-picker v-model="indicatorFilter.dateRange" type="daterange" range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�" end-placeholder="缁撴潫鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%" />
+ </el-col>
+ <el-col :span="6" style="text-align: right;">
+ <el-button type="primary" @click="applyIndicatorFilter">鏌ヨ</el-button>
+ <el-button @click="resetIndicatorFilter">閲嶇疆</el-button>
+ </el-col>
+ </el-row>
+
+ <!-- 鍥捐〃鍖� -->
+ <div class="chart-container">
+ <div ref="indicatorChartRef" class="chart-wrapper"></div>
+ </div>
+
+ <!-- 涓氱哗缁熻锛堝洟闃熺淮搴︼紝鏃犱釜浜哄鍚嶏級 -->
+ <el-table v-if="showTeamPerformance" :data="teamPerformanceList" border stripe style="margin-top: 20px;">
+ <el-table-column prop="team" label="閿�鍞洟闃�"/>
+ <el-table-column prop="orderCount" label="璁㈠崟鏁�"/>
+ <el-table-column prop="salesAmount" label="閿�鍞">
+ <template #default="scope">楼{{ scope.row.salesAmount.toLocaleString() }}</template>
+ </el-table-column>
+ <el-table-column prop="shipRate" label="鍙戣揣鐜�">
+ <template #default="scope">{{ scope.row.shipRate }}</template>
+ </el-table-column>
+ <el-table-column prop="attainment" label="鐩爣杈炬垚鐜�">
+ <template #default="scope">
+ <el-tag :type="scope.row.attainment >= 100 ? 'success' : scope.row.attainment >= 80 ? 'warning' : 'danger'">
+ {{ scope.row.attainment }}%
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
+import { Document, Van, Tickets } from '@element-plus/icons-vue'
+import * as echarts from 'echarts'
+import { getTotalStatistics, getStatisticsTable } from '@/api/salesManagement/indicatorStats'
+import { productTreeList } from '@/api/basicData/product.js'
+import { customerList } from '@/api/salesManagement/salesLedger.js'
+import { ElMessage } from 'element-plus'
+
+const indicatorKpis = reactive({
+ orderCount: 0,
+ salesAmount: 0,
+ shipRate: 0
+})
+
+// 鏄惁灞曠ず閿�鍞洟闃熸槑缁嗚〃锛屾寜闇�寮�鍚�
+const showTeamPerformance = ref(false)
+const loading = ref(false)
+
+const indicatorFilter = reactive({
+ productCategory: '',
+ customerName: '',
+ dateRange: []
+})
+
+const indicatorChartRef = ref(null)
+let indicatorChart = null
+
+const productOptions = ref([])
+const customerOption = ref([])
+
+const teamPerformanceList = ref([
+ { team: '鍗庝笢澶у尯', orderCount: 320, salesAmount: 2850000, shipRate: 90, attainment: 105 },
+ { team: '鍗庡寳澶у尯', orderCount: 280, salesAmount: 2150000, shipRate: 86, attainment: 92 },
+ { team: '鍗庡崡澶у尯', orderCount: 210, salesAmount: 1850000, shipRate: 88, attainment: 78 },
+ { team: '瑗垮崡澶у尯', orderCount: 180, salesAmount: 1500000, shipRate: 83, attainment: 74 }
+])
+
+// 杞崲浜у搧鏍戞暟鎹紝灏� id 鏀逛负 value
+function convertIdToValue(data) {
+ return data.map((item) => {
+ const { id, children, ...rest } = item
+ const newItem = {
+ ...rest,
+ value: id, // 灏� id 鏀逛负 value
+ }
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children)
+ }
+ return newItem
+ })
+}
+
+// 鑾峰彇浜у搧鏍戞暟鎹�
+const getProductOptions = () => {
+ return productTreeList().then((res) => {
+ productOptions.value = convertIdToValue(res)
+ }).catch((error) => {
+ console.error('鑾峰彇浜у搧鏍戝け璐�:', error)
+ ElMessage.error('鑾峰彇浜у搧绫诲埆澶辫触')
+ })
+}
+
+// 鑾峰彇瀹㈡埛鍒楄〃
+const getCustomerList = () => {
+ return customerList().then((res) => {
+ customerOption.value = res || []
+ }).catch((error) => {
+ console.error('鑾峰彇瀹㈡埛鍒楄〃澶辫触:', error)
+ ElMessage.error('鑾峰彇瀹㈡埛鍒楄〃澶辫触')
+ })
+}
+
+// 鏍规嵁 id 鏌ユ壘浜у搧绫诲埆鍚嶇О
+const findNodeLabelById = (nodes, id) => {
+ if (!id) return null
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].value === id) {
+ return nodes[i].label
+ }
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const found = findNodeLabelById(nodes[i].children, id)
+ if (found) return found
+ }
+ }
+ return null
+}
+
+// 鑾峰彇澶撮儴缁熻鏁版嵁
+const fetchTotalStatistics = async () => {
+ try {
+ loading.value = true
+ const params = {}
+ if (indicatorFilter.customerName) {
+ params.customerName = indicatorFilter.customerName
+ }
+ if (indicatorFilter.productCategory) {
+ // 鏍规嵁 id 鏌ユ壘浜у搧绫诲埆鍚嶇О
+ const categoryName = findNodeLabelById(productOptions.value, indicatorFilter.productCategory)
+ if (categoryName) {
+ params.productCategory = categoryName
+ }
+ }
+ if (indicatorFilter.dateRange && indicatorFilter.dateRange.length === 2) {
+ params.entryDateStart = indicatorFilter.dateRange[0]
+ params.entryDateEnd = indicatorFilter.dateRange[1]
+ }
+ const res = await getTotalStatistics(params)
+ if (res && res.data) {
+ indicatorKpis.orderCount = res.data.total || 0
+ indicatorKpis.salesAmount = res.data.contractAmountTotal || 0
+ // 鍙戣揣鐜囧鏋滄帴鍙f病鏈夎繑鍥烇紝淇濇寔鍘熷�兼垨璁句负0
+ // indicatorKpis.shipRate = res.data.shipRate || 0
+ }
+ } catch (error) {
+ console.error('鑾峰彇澶撮儴缁熻澶辫触:', error)
+ ElMessage.error('鑾峰彇缁熻鏁版嵁澶辫触')
+ } finally {
+ loading.value = false
+ }
+}
+
+// 鑾峰彇鏌辩姸鍥炬暟鎹�
+const fetchStatisticsTable = async () => {
+ try {
+ loading.value = true
+ const params = {}
+ if (indicatorFilter.customerName) {
+ params.customerName = indicatorFilter.customerName
+ }
+ if (indicatorFilter.productCategory) {
+ // 鏍规嵁 id 鏌ユ壘浜у搧绫诲埆鍚嶇О
+ const categoryName = findNodeLabelById(productOptions.value, indicatorFilter.productCategory)
+ if (categoryName) {
+ params.productCategory = categoryName
+ }
+ }
+ if (indicatorFilter.dateRange && indicatorFilter.dateRange.length === 2) {
+ params.entryDateStart = indicatorFilter.dateRange[0]
+ params.entryDateEnd = indicatorFilter.dateRange[1]
+ }
+ const res = await getStatisticsTable(params)
+ if (res && res.data) {
+ updateChart(res.data)
+ }
+ } catch (error) {
+ console.error('鑾峰彇鍥捐〃鏁版嵁澶辫触:', error)
+ ElMessage.error('鑾峰彇鍥捐〃鏁版嵁澶辫触')
+ } finally {
+ loading.value = false
+ }
+}
+
+// 鏇存柊鍥捐〃
+const updateChart = (chartData) => {
+ if (!indicatorChartRef.value) return
+ if (indicatorChart) indicatorChart.dispose()
+ indicatorChart = echarts.init(indicatorChartRef.value)
+
+ // 鏍规嵁鎺ュ彛杩斿洖鐨勬暟鎹粨鏋勬洿鏂板浘琛�
+ // 鎺ュ彛杩斿洖: dateList, orderCountList, salesAmountList
+ const option = {
+ title: { text: '澶氱淮搴﹂攢鍞寚鏍囪秼鍔�', left: 'center' },
+ tooltip: { trigger: 'axis' },
+ legend: { data: ['璁㈠崟鏁�', '閿�鍞'], top: 30 },
+ grid: { left: '3%', right: '8%', bottom: '3%', containLabel: true },
+ xAxis: {
+ type: 'category',
+ data: chartData.dateList || []
+ },
+ yAxis: [
+ { type: 'value', name: '閲戦', position: 'left', axisLabel: { formatter: '{value}' } },
+ {
+ type: 'value',
+ name: '鏁伴噺',
+ position: 'right',
+ minInterval: 1,
+ axisLabel: {
+ formatter: (value) => {
+ const intValue = Math.round(value)
+ return intValue.toString()
+ }
+ }
+ }
+ ],
+ series: [
+ {
+ name: '璁㈠崟鏁�',
+ type: 'line',
+ yAxisIndex: 1,
+ data: chartData.orderCountList || [],
+ itemStyle: { color: '#409eff' }
+ },
+ {
+ name: '閿�鍞',
+ type: 'bar',
+ yAxisIndex: 0,
+ data: chartData.salesAmountList || [],
+ itemStyle: { color: '#67c23a' }
+ }
+ ]
+ }
+ indicatorChart.setOption(option)
+}
+
+const initIndicatorChart = () => {
+ if (!indicatorChartRef.value) return
+ if (indicatorChart) indicatorChart.dispose()
+ indicatorChart = echarts.init(indicatorChartRef.value)
+ const option = {
+ title: { text: '澶氱淮搴﹂攢鍞寚鏍囪秼鍔�', left: 'center' },
+ tooltip: { trigger: 'axis' },
+ legend: { data: ['璁㈠崟鏁�', '閿�鍞'], top: 30 },
+ grid: { left: '3%', right: '8%', bottom: '3%', containLabel: true },
+ xAxis: { type: 'category', data: [] },
+ yAxis: [
+ { type: 'value', name: '閲戦', position: 'left', axisLabel: { formatter: '{value}' } },
+ {
+ type: 'value',
+ name: '鏁伴噺',
+ position: 'right',
+ minInterval: 1,
+ axisLabel: {
+ formatter: (value) => {
+ const intValue = Math.round(value)
+ return intValue.toString()
+ }
+ }
+ }
+ ],
+ series: [
+ { name: '璁㈠崟鏁�', type: 'line', yAxisIndex: 1, data: [], itemStyle: { color: '#409eff' } },
+ { name: '閿�鍞', type: 'bar', yAxisIndex: 0, data: [], itemStyle: { color: '#67c23a' } }
+ ]
+ }
+ indicatorChart.setOption(option)
+}
+
+const applyIndicatorFilter = async () => {
+ await Promise.all([
+ fetchTotalStatistics(),
+ fetchStatisticsTable()
+ ])
+}
+
+const resetIndicatorFilter = () => {
+ indicatorFilter.productCategory = ''
+ indicatorFilter.customerName = ''
+ indicatorFilter.dateRange = []
+ applyIndicatorFilter()
+}
+
+// 绐楀彛澶у皬鍙樺寲鏃惰皟鏁村浘琛ㄥぇ灏�
+const handleResize = () => {
+ if (indicatorChart) {
+ indicatorChart.resize()
+ }
+}
+
+onMounted(() => {
+ nextTick(() => {
+ initIndicatorChart()
+ getProductOptions()
+ getCustomerList()
+ fetchTotalStatistics()
+ fetchStatisticsTable()
+ })
+ // 鐩戝惉绐楀彛澶у皬鍙樺寲
+ window.addEventListener('resize', handleResize)
+})
+
+onUnmounted(() => {
+ // 绉婚櫎绐楀彛澶у皬鍙樺寲鐩戝惉鍣�
+ window.removeEventListener('resize', handleResize)
+ // 閿�姣佸浘琛ㄥ疄渚�
+ if (indicatorChart) {
+ indicatorChart.dispose()
+ indicatorChart = null
+ }
+})
+</script>
+
+<style scoped>
+.indicator-stats {
+ padding: 0;
+}
+.box-card { border: none; box-shadow: none; }
+.search-row { margin-bottom: 20px; }
+.stats-row { margin-bottom: 24px; }
+.stat-card { display: flex; align-items: center; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); }
+.stat-icon { width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; border-radius: 8px; margin-right: 16px; }
+.stat-content { flex: 1; }
+.stat-value { font-size: 28px; font-weight: bold; color: #303133; margin-bottom: 4px; }
+.stat-label { font-size: 14px; color: #909399; }
+.chart-container {
+ margin: 20px 0;
+ padding: 20px;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+ width: 100%;
+ overflow: hidden;
+}
+.chart-wrapper {
+ width: 100%;
+ height: 360px;
+ min-width: 0;
+}
+</style>
+
+
--
Gitblit v1.9.3