Gogs преди 2 месеца
родител
ревизия
83bf751e3b
променени са 2 файла, в които са добавени 436 реда и са изтрити 105 реда
  1. 219 105
      src/views/order/efficiency/index.vue
  2. 217 0
      tmp-efficiency-check.mjs

+ 219 - 105
src/views/order/efficiency/index.vue

@@ -1,40 +1,48 @@
 <template>
   <div class="department-efficiency-view">
-    <!-- 1. 页面头部 -->
     <header class="page-header">
       <h1 class="page-title">综合运营分析</h1>
       <p class="page-description">分析各部门的平均销售额与渠道商品多样性。</p>
     </header>
 
-    <!-- 2. 部门运营效率分析 -->
     <section class="chart-area">
       <div v-if="barChart.loading" class="status-overlay">
-        <p>hina正在努力加载部门效率数据...</p>
+        <p>正在加载部门效率数据...</p>
       </div>
-      <div v-else-if="barChart.error" class="status-overlay error">
-        <p>部门效率数据加载失败</p>
-        <p class="error-message">{{ barChart.error }}</p>
+      <div v-else-if="barChart.message" :class="['status-overlay', { error: barChart.isError }]">
+        <p>{{ barChart.title }}</p>
+        <p class="error-message">{{ barChart.message }}</p>
       </div>
-      <div ref="barChartRef" style="width: 100%; height: 500px;"></div>
+      <div ref="barChartRef" class="chart-canvas"></div>
     </section>
 
-    <!-- 3. 渠道商品多样性分析 -->
     <section class="chart-area">
       <div v-if="pieChart.loading" class="status-overlay">
-        <p>hina正在努力加载渠道多样性数据...</p>
+        <p>正在加载渠道多样性数据...</p>
       </div>
-      <div v-else-if="pieChart.error" class="status-overlay error">
-        <p>渠道多样性数据加载失败</p>
-        <p class="error-message">{{ pieChart.error }}</p>
+      <div v-else-if="pieChart.message" :class="['status-overlay', { error: pieChart.isError }]">
+        <p>{{ pieChart.title }}</p>
+        <p class="error-message">{{ pieChart.message }}</p>
       </div>
-      <div ref="pieChartRef" style="width: 100%; height: 500px;"></div>
+      <div ref="pieChartRef" class="chart-canvas"></div>
     </section>
   </div>
 </template>
 
 <script>
-import * as echarts from 'echarts';
-import { getShopChannelDiversity, getShopDepartmentEfficiency } from '@/api/order';
+import * as echarts from 'echarts'
+import { getShopChannelDiversity, getShopDepartmentEfficiency } from '@/api/order'
+
+const EMPTY_DATA_HINTS = ['未上传', '请先上传', '暂无数据', '无数据', 'not found', 'no data']
+
+function createChartState(defaultTitle) {
+  return {
+    loading: true,
+    title: defaultTitle,
+    message: '',
+    isError: false
+  }
+}
 
 export default {
   name: 'OrderEfficiency',
@@ -42,111 +50,204 @@ export default {
     return {
       barChartInstance: null,
       pieChartInstance: null,
-      barChart: { loading: true, error: null },
-      pieChart: { loading: true, error: null }
-    };
+      barChart: createChartState('部门效率数据暂不可用'),
+      pieChart: createChartState('渠道多样性数据暂不可用')
+    }
   },
   mounted() {
-    this.initBarChart();
-    this.initPieChart();
-    window.addEventListener('resize', this.handleResize);
+    this.loadCharts()
+    window.addEventListener('resize', this.handleResize)
   },
   beforeDestroy() {
-    window.removeEventListener('resize', this.handleResize);
-    if (this.barChartInstance) this.barChartInstance.dispose();
-    if (this.pieChartInstance) this.pieChartInstance.dispose();
+    window.removeEventListener('resize', this.handleResize)
+    this.disposeCharts()
   },
   methods: {
+    async loadCharts() {
+      await Promise.all([this.initBarChart(), this.initPieChart()])
+    },
+    disposeCharts() {
+      if (this.barChartInstance) {
+        this.barChartInstance.dispose()
+        this.barChartInstance = null
+      }
+      if (this.pieChartInstance) {
+        this.pieChartInstance.dispose()
+        this.pieChartInstance = null
+      }
+    },
+    resetChartState(target, title) {
+      target.loading = true
+      target.title = title
+      target.message = ''
+      target.isError = false
+    },
+    getErrorMessage(error, fallback) {
+      return (
+        error?.response?.data?.message ||
+        error?.response?.data?.msg ||
+        error?.message ||
+        fallback
+      )
+    },
+    isEmptyDataMessage(message) {
+      const normalized = String(message || '').toLowerCase()
+      return EMPTY_DATA_HINTS.some(item => normalized.includes(item.toLowerCase()))
+    },
+    setChartStatus(target, title, message, isError = false) {
+      target.title = title
+      target.message = message
+      target.isError = isError
+    },
+    normalizeBarData(response) {
+      if (!response || response.success !== true || !response.data) {
+        throw new Error((response && response.message) || '部门效率数据加载失败')
+      }
+
+      const rows = Object.entries(response.data)
+        .map(([name, value]) => ({
+          name,
+          value: Number(value)
+        }))
+        .filter(item => Number.isFinite(item.value))
+
+      if (!rows.length) {
+        throw new Error('请先上传店铺价值 CSV 文件')
+      }
+
+      return rows
+    },
+    normalizePieData(response) {
+      if (!response || response.success !== true || !response.data) {
+        throw new Error((response && response.message) || '渠道多样性数据加载失败')
+      }
+
+      const rows = Object.entries(response.data)
+        .map(([name, value]) => ({
+          name,
+          value: Number(value)
+        }))
+        .filter(item => Number.isFinite(item.value) && item.value > 0)
+
+      if (!rows.length) {
+        throw new Error('请先上传店铺价值 CSV 文件')
+      }
+
+      return rows
+    },
+    initBarChartInstance() {
+      const chartEl = this.$refs.barChartRef
+      if (!chartEl) return null
+      this.barChartInstance = echarts.getInstanceByDom(chartEl) || echarts.init(chartEl)
+      return this.barChartInstance
+    },
+    initPieChartInstance() {
+      const chartEl = this.$refs.pieChartRef
+      if (!chartEl) return null
+      this.pieChartInstance = echarts.getInstanceByDom(chartEl) || echarts.init(chartEl)
+      return this.pieChartInstance
+    },
     async initBarChart() {
-      this.barChart.loading = true;
+      this.resetChartState(this.barChart, '部门效率数据暂不可用')
       try {
-        const response = await axios.get('/api/shop/import/department-efficiency');
-        if (!response.data || !response.data.success) {
-          throw new Error(response.data.message || 'REPLACED__');
-        }
-        const rawData = response.data;
-        const chartData = Object.entries(rawData).map(([name, value]) => ({
-          name,
-          value: parseFloat(value).toFixed(2)
-        }));
-
-        await this.$nextTick();
-        const chartEl = this.$refs.barChartRef;
-        if (chartEl) {
-          this.barChartInstance = echarts.init(chartEl);
-          const option = {
-            title: { text: '部门效率分析', left: 'center' },
-            tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: '{b}<br/>平均销售额: {c} 元' },
-            grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
-            xAxis: { type: 'category', data: chartData.map(item => item.name), axisLabel: { rotate: 30, interval: 0 } },
-            yAxis: { type: 'value', name: '平均销售额(元)' },
-            series: [{
-              name: '平均销售额',
-              type: 'bar',
-              data: chartData.map(item => item.value),
-              barWidth: '40%',
-              itemStyle: {
-                borderRadius: [5, 5, 0, 0],
-                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                  { offset: 0, color: '#83bff6' },
-                  { offset: 1, color: '#188df0' }
-                ])
-              }
-            }]
-          };
-          this.barChartInstance.setOption(option);
-        }
-      } catch (err) {
-        this.barChart.error = err.message || '部门效率数据加载失败';
+        const response = await getShopDepartmentEfficiency()
+        const chartData = this.normalizeBarData(response)
+
+        await this.$nextTick()
+        const chart = this.initBarChartInstance()
+        if (!chart) return
+
+        chart.setOption({
+          title: { text: '部门效率分析', left: 'center' },
+          tooltip: {
+            trigger: 'axis',
+            axisPointer: { type: 'shadow' },
+            formatter: '{b}<br/>平均销售额: {c} 元'
+          },
+          grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
+          xAxis: {
+            type: 'category',
+            data: chartData.map(item => item.name),
+            axisLabel: { rotate: 30, interval: 0 }
+          },
+          yAxis: {
+            type: 'value',
+            name: '平均销售额(元)'
+          },
+          series: [{
+            name: '平均销售额',
+            type: 'bar',
+            data: chartData.map(item => Number(item.value.toFixed(2))),
+            barWidth: '40%',
+            itemStyle: {
+              borderRadius: [5, 5, 0, 0],
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                { offset: 0, color: '#83bff6' },
+                { offset: 1, color: '#188df0' }
+              ])
+            }
+          }]
+        }, true)
+      } catch (error) {
+        const message = this.getErrorMessage(error, '部门效率数据加载失败')
+        this.setChartStatus(
+          this.barChart,
+          this.isEmptyDataMessage(message) ? '暂无部门效率数据' : '部门效率数据加载失败',
+          this.isEmptyDataMessage(message) ? '请先在店铺价值页面上传 CSV 文件,再查看该图表。' : message,
+          !this.isEmptyDataMessage(message)
+        )
       } finally {
-        this.barChart.loading = false;
+        this.barChart.loading = false
       }
     },
     async initPieChart() {
-      this.pieChart.loading = true;
+      this.resetChartState(this.pieChart, '渠道多样性数据暂不可用')
       try {
-        const response = await axios.get('/api/shop/import/channel-diversity');
-        if (!response.data || !response.data.success) {
-          throw new Error(response.data.message || '渠道多样性数据获取失败');
-        }
-        const rawData = response.data;
-        const chartData = Object.entries(rawData).map(([name, value]) => ({ name, value }));
-
-        await this.$nextTick();
-        const chartEl = this.$refs.pieChartRef;
-        if (chartEl) {
-          this.pieChartInstance = echarts.init(chartEl);
-          const option = {
-            title: { text: '渠道商品多样性', left: 'center' },
-            tooltip: { trigger: 'item', formatter: '{a} <br/>{b} : {c} ({d}%)' },
-            legend: { orient: 'vertical', left: 'left', top: '10%' },
-            series: [{
-              name: '渠道',
-              type: 'pie',
-              radius: [20, 140],
-              center: ['50%', '60%'],
-              roseType: 'area',
-              itemStyle: { borderRadius: 5 },
-              data: chartData
-            }]
-          };
-          this.pieChartInstance.setOption(option);
-        }
-      } catch (err) {
-        this.pieChart.error = err.message || '渠道多样性数据加载失败';
+        const response = await getShopChannelDiversity()
+        const chartData = this.normalizePieData(response)
+
+        await this.$nextTick()
+        const chart = this.initPieChartInstance()
+        if (!chart) return
+
+        chart.setOption({
+          title: { text: '渠道商品多样性', left: 'center' },
+          tooltip: { trigger: 'item', formatter: '{a}<br/>{b}: {c} ({d}%)' },
+          legend: { orient: 'vertical', left: 'left', top: '10%' },
+          series: [{
+            name: '渠道',
+            type: 'pie',
+            radius: [20, 140],
+            center: ['50%', '60%'],
+            roseType: 'area',
+            itemStyle: { borderRadius: 5 },
+            data: chartData
+          }]
+        }, true)
+      } catch (error) {
+        const message = this.getErrorMessage(error, '渠道多样性数据加载失败')
+        this.setChartStatus(
+          this.pieChart,
+          this.isEmptyDataMessage(message) ? '暂无渠道多样性数据' : '渠道多样性数据加载失败',
+          this.isEmptyDataMessage(message) ? '请先在店铺价值页面上传 CSV 文件,再查看该图表。' : message,
+          !this.isEmptyDataMessage(message)
+        )
       } finally {
-        this.pieChart.loading = false;
+        this.pieChart.loading = false
       }
     },
     handleResize() {
-      if (this.barChartInstance) this.barChartInstance.resize();
-      if (this.pieChartInstance) this.pieChartInstance.resize();
+      if (this.barChartInstance) {
+        this.barChartInstance.resize()
+      }
+      if (this.pieChartInstance) {
+        this.pieChartInstance.resize()
+      }
     }
   }
-};
+}
 </script>
 
-
 <style scoped>
 .department-efficiency-view {
   display: flex;
@@ -154,51 +255,64 @@ export default {
   gap: 20px;
   padding: 20px;
 }
+
 .page-header {
   padding: 15px 20px;
-  background-color: #ffffff;
+  background-color: #fff;
   border-radius: 8px;
   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
 }
+
 .page-title {
   margin: 0;
   font-size: 18px;
   font-weight: 600;
   color: #333;
 }
+
 .page-description {
   margin: 4px 0 0;
   font-size: 14px;
   color: #666;
 }
+
 .chart-area {
   position: relative;
   padding: 20px;
-  background-color: #ffffff;
+  background-color: #fff;
   border-radius: 8px;
   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
   min-height: 540px;
 }
+
+.chart-canvas {
+  width: 100%;
+  height: 500px;
+}
+
 .status-overlay {
   position: absolute;
-  inset: 0; /* a shorthand for top, right, bottom, left */
+  inset: 0;
   display: flex;
   flex-direction: column;
   justify-content: center;
   align-items: center;
-  background-color: rgba(255, 255, 255, 0.8);
+  padding: 24px;
+  text-align: center;
+  background-color: rgba(255, 255, 255, 0.88);
   border-radius: 8px;
   z-index: 10;
   color: #555;
   font-size: 16px;
 }
+
 .status-overlay.error {
   color: #f56c6c;
 }
+
 .error-message {
+  margin-top: 8px;
   font-size: 14px;
   color: #999;
-  margin-top: 8px;
 }
 </style>
-// __MARKER__

+ 217 - 0
tmp-efficiency-check.mjs

@@ -0,0 +1,217 @@
+
+import * as echarts from 'echarts'
+import { getShopChannelDiversity, getShopDepartmentEfficiency } from '@/api/order'
+
+const EMPTY_DATA_HINTS = ['未上传', '请先上传', '暂无数据', '无数据', 'not found', 'no data']
+
+function createChartState(defaultTitle) {
+  return {
+    loading: true,
+    title: defaultTitle,
+    message: '',
+    isError: false
+  }
+}
+
+export default {
+  name: 'OrderEfficiency',
+  data() {
+    return {
+      barChartInstance: null,
+      pieChartInstance: null,
+      barChart: createChartState('部门效率数据暂不可用'),
+      pieChart: createChartState('渠道多样性数据暂不可用')
+    }
+  },
+  mounted() {
+    this.loadCharts()
+    window.addEventListener('resize', this.handleResize)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.handleResize)
+    this.disposeCharts()
+  },
+  methods: {
+    async loadCharts() {
+      await Promise.all([this.initBarChart(), this.initPieChart()])
+    },
+    disposeCharts() {
+      if (this.barChartInstance) {
+        this.barChartInstance.dispose()
+        this.barChartInstance = null
+      }
+      if (this.pieChartInstance) {
+        this.pieChartInstance.dispose()
+        this.pieChartInstance = null
+      }
+    },
+    resetChartState(target, title) {
+      target.loading = true
+      target.title = title
+      target.message = ''
+      target.isError = false
+    },
+    getErrorMessage(error, fallback) {
+      return (
+        error?.response?.data?.message ||
+        error?.response?.data?.msg ||
+        error?.message ||
+        fallback
+      )
+    },
+    isEmptyDataMessage(message) {
+      const normalized = String(message || '').toLowerCase()
+      return EMPTY_DATA_HINTS.some(item => normalized.includes(item.toLowerCase()))
+    },
+    setChartStatus(target, title, message, isError = false) {
+      target.title = title
+      target.message = message
+      target.isError = isError
+    },
+    normalizeBarData(response) {
+      if (!response || response.success !== true || !response.data) {
+        throw new Error((response && response.message) || '部门效率数据加载失败')
+      }
+
+      const rows = Object.entries(response.data)
+        .map(([name, value]) => ({
+          name,
+          value: Number(value)
+        }))
+        .filter(item => Number.isFinite(item.value))
+
+      if (!rows.length) {
+        throw new Error('请先上传店铺价值 CSV 文件')
+      }
+
+      return rows
+    },
+    normalizePieData(response) {
+      if (!response || response.success !== true || !response.data) {
+        throw new Error((response && response.message) || '渠道多样性数据加载失败')
+      }
+
+      const rows = Object.entries(response.data)
+        .map(([name, value]) => ({
+          name,
+          value: Number(value)
+        }))
+        .filter(item => Number.isFinite(item.value) && item.value > 0)
+
+      if (!rows.length) {
+        throw new Error('请先上传店铺价值 CSV 文件')
+      }
+
+      return rows
+    },
+    initBarChartInstance() {
+      const chartEl = this.$refs.barChartRef
+      if (!chartEl) return null
+      this.barChartInstance = echarts.getInstanceByDom(chartEl) || echarts.init(chartEl)
+      return this.barChartInstance
+    },
+    initPieChartInstance() {
+      const chartEl = this.$refs.pieChartRef
+      if (!chartEl) return null
+      this.pieChartInstance = echarts.getInstanceByDom(chartEl) || echarts.init(chartEl)
+      return this.pieChartInstance
+    },
+    async initBarChart() {
+      this.resetChartState(this.barChart, '部门效率数据暂不可用')
+      try {
+        const response = await getShopDepartmentEfficiency()
+        const chartData = this.normalizeBarData(response)
+
+        await this.$nextTick()
+        const chart = this.initBarChartInstance()
+        if (!chart) return
+
+        chart.setOption({
+          title: { text: '部门效率分析', left: 'center' },
+          tooltip: {
+            trigger: 'axis',
+            axisPointer: { type: 'shadow' },
+            formatter: '{b}<br/>平均销售额: {c} 元'
+          },
+          grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
+          xAxis: {
+            type: 'category',
+            data: chartData.map(item => item.name),
+            axisLabel: { rotate: 30, interval: 0 }
+          },
+          yAxis: {
+            type: 'value',
+            name: '平均销售额(元)'
+          },
+          series: [{
+            name: '平均销售额',
+            type: 'bar',
+            data: chartData.map(item => Number(item.value.toFixed(2))),
+            barWidth: '40%',
+            itemStyle: {
+              borderRadius: [5, 5, 0, 0],
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                { offset: 0, color: '#83bff6' },
+                { offset: 1, color: '#188df0' }
+              ])
+            }
+          }]
+        }, true)
+      } catch (error) {
+        const message = this.getErrorMessage(error, '部门效率数据加载失败')
+        this.setChartStatus(
+          this.barChart,
+          this.isEmptyDataMessage(message) ? '暂无部门效率数据' : '部门效率数据加载失败',
+          this.isEmptyDataMessage(message) ? '请先在店铺价值页面上传 CSV 文件,再查看该图表。' : message,
+          !this.isEmptyDataMessage(message)
+        )
+      } finally {
+        this.barChart.loading = false
+      }
+    },
+    async initPieChart() {
+      this.resetChartState(this.pieChart, '渠道多样性数据暂不可用')
+      try {
+        const response = await getShopChannelDiversity()
+        const chartData = this.normalizePieData(response)
+
+        await this.$nextTick()
+        const chart = this.initPieChartInstance()
+        if (!chart) return
+
+        chart.setOption({
+          title: { text: '渠道商品多样性', left: 'center' },
+          tooltip: { trigger: 'item', formatter: '{a}<br/>{b}: {c} ({d}%)' },
+          legend: { orient: 'vertical', left: 'left', top: '10%' },
+          series: [{
+            name: '渠道',
+            type: 'pie',
+            radius: [20, 140],
+            center: ['50%', '60%'],
+            roseType: 'area',
+            itemStyle: { borderRadius: 5 },
+            data: chartData
+          }]
+        }, true)
+      } catch (error) {
+        const message = this.getErrorMessage(error, '渠道多样性数据加载失败')
+        this.setChartStatus(
+          this.pieChart,
+          this.isEmptyDataMessage(message) ? '暂无渠道多样性数据' : '渠道多样性数据加载失败',
+          this.isEmptyDataMessage(message) ? '请先在店铺价值页面上传 CSV 文件,再查看该图表。' : message,
+          !this.isEmptyDataMessage(message)
+        )
+      } finally {
+        this.pieChart.loading = false
+      }
+    },
+    handleResize() {
+      if (this.barChartInstance) {
+        this.barChartInstance.resize()
+      }
+      if (this.pieChartInstance) {
+        this.pieChartInstance.resize()
+      }
+    }
+  }
+}