From a9d2b856e0f6be6475319c2dc36bf405c2f908fc Mon Sep 17 00:00:00 2001
From: WXL (wul) <wl_5969728@163.com>
Date: 星期二, 22 九月 2026 14:04:52 +0800
Subject: [PATCH] 测试完成

---
 src/views/Satisfaction/sfstatistics/components/ChartsPanel.vue |  620 +++++++++++++++++++++++++++++++++++++++++++------------
 1 files changed, 481 insertions(+), 139 deletions(-)

diff --git a/src/views/Satisfaction/sfstatistics/components/ChartsPanel.vue b/src/views/Satisfaction/sfstatistics/components/ChartsPanel.vue
index e0ce533..cc1be67 100644
--- a/src/views/Satisfaction/sfstatistics/components/ChartsPanel.vue
+++ b/src/views/Satisfaction/sfstatistics/components/ChartsPanel.vue
@@ -17,8 +17,14 @@
         <span class="filter-label">鍖洪棿绫诲瀷锛�</span>
         <el-radio-group v-model="queryParams.interval" size="small" @change="refreshAll">
           <el-radio-button label="day">鎸夊ぉ</el-radio-button>
+          <el-radio-button label="week">鎸夊懆</el-radio-button>
           <el-radio-button label="month">鎸夋湀</el-radio-button>
           <el-radio-button label="year">鎸夊勾</el-radio-button>
+        </el-radio-group>
+        <span class="filter-label">缁熻缁村害锛�</span>
+        <el-radio-group v-model="queryParams.dimension" size="small" @change="onDimensionChange">
+          <el-radio-button label="ward">鎸夌梾鍖虹粺璁�</el-radio-button>
+          <el-radio-button label="dept">鎸夌瀹ょ粺璁�</el-radio-button>
         </el-radio-group>
         <el-button type="primary" size="small" icon="el-icon-search" @click="refreshAll">鏌ヨ</el-button>
         <el-button size="small" icon="el-icon-refresh" @click="handleReset">閲嶇疆</el-button>
@@ -31,10 +37,13 @@
       <el-col :span="12">
         <el-card shadow="never" class="chart-card">
           <div slot="header" class="card-header">
-            <span>鏃堕棿缁村害 路 闂ㄨ瘖鎮h�呮弧鎰忓害瓒嬪娍</span>
+            <span>鎮h�呮弧鎰忓害缁煎悎寰楀垎瓒嬪娍</span>
             <el-tag type="success" size="small">鐜瘮 {{ trendMoM }}</el-tag>
           </div>
-          <div ref="chart1" class="chart-box"></div>
+          <div class="chart-wrap" v-loading="trendLoading" element-loading-text="瓒嬪娍鍔犺浇涓�...">
+            <div ref="chart1" class="chart-box"></div>
+            <div v-if="!trendLoading && !trend.buckets.length" class="chart-empty">鏆傛棤瓒嬪娍鏁版嵁</div>
+          </div>
         </el-card>
       </el-col>
 
@@ -44,7 +53,10 @@
           <div slot="header" class="card-header">
             <span>鍒嗙被缁村害 路 鍚勬寚鏍噞{ intervalLabel }}瀵规瘮</span>
           </div>
-          <div ref="chart2" class="chart-box"></div>
+          <div class="chart-wrap" v-loading="categoryLoading" element-loading-text="鍒嗙被缁村害鍔犺浇涓�...">
+            <div ref="chart2" class="chart-box"></div>
+            <div v-if="!categoryLoading && !categoryCompare.categories.length" class="chart-empty">鏆傛棤鍒嗙被缁村害鏁版嵁</div>
+          </div>
         </el-card>
       </el-col>
     </el-row>
@@ -56,25 +68,26 @@
           <div slot="header" class="card-header">
             <span>鍗曢 路 閫夐」缁熻</span>
             <div class="header-right">
-              <el-select v-model="singleQuestionId" size="small" class="sel sel-lg" @change="refreshAll">
-                <el-option v-for="q in questionOptions" :key="q.id" :label="q.title" :value="q.id" />
+              <el-select v-model="singleQuestionId" size="small" class="sel sel-lg" filterable remote clearable
+                :remote-method="searchQuestions" :loading="questionLoading" placeholder="璇疯緭鍏ラ棶棰樺唴瀹规悳绱�" @change="onSingleQuestionChange">
+                <el-option v-for="q in questionOptions" :key="q.id" :label="q.scriptContent || q.title" :value="q.id" />
               </el-select>
-              <el-select v-model="singleDeptWards" multiple collapse-tags size="small" class="sel" clearable filterable placeholder="鍏ㄩ儴绉戝/鐥呭尯" @change="refreshAll">
-                <el-option-group label="绉戝">
+              <el-select v-model="singleDeptWards" multiple collapse-tags size="small" class="sel" clearable filterable :placeholder="queryParams.dimension === 'dept' ? '鍏ㄩ儴绉戝' : '鍏ㄩ儴鐥呭尯'" @change="refreshAll">
+                <template v-if="queryParams.dimension === 'dept'">
                   <el-option v-for="d in deptOptions" :key="'dept:' + d" :label="d" :value="'dept:' + d" />
-                </el-option-group>
-                <el-option-group label="鐥呭尯">
+                </template>
+                <template v-else>
                   <el-option v-for="w in wardOptions" :key="'ward:' + w" :label="w" :value="'ward:' + w" />
-                </el-option-group>
+                </template>
               </el-select>
             </div>
           </div>
-          <el-table :data="singleQuestionData" border size="small">
+          <el-table :data="singleQuestionData" border size="small" v-loading="singleLoading" element-loading-text="閫夐」缁熻鍔犺浇涓�...">
             <el-table-column prop="option" label="閫夐」" />
             <el-table-column prop="count" label="鍚堣鏁伴噺" width="120" align="center" />
             <el-table-column prop="rate" label="鍗犳瘮姣斾緥" width="120" align="center" />
           </el-table>
-          <div class="table-footer">鏈鏈夋晥濉啓浜烘锛�<b>{{ singleQuestionMeta.total }}</b>銆�缁煎悎婊℃剰搴︼細<b>{{ singleQuestionMeta.score }}</b></div>
+          <div class="table-footer">鏈鏈夋晥濉啓浜烘锛�<b>{{ singleQuestionMeta.total }}</b>銆�缁煎悎寰楀垎锛�<b>{{ singleQuestionMeta.score }}</b></div>
         </el-card>
       </el-col>
 
@@ -84,7 +97,10 @@
           <div slot="header" class="card-header">
             <span>闄㈠尯缁村害 路 鍚勯櫌鍖簕{ intervalLabel }}瀵规瘮</span>
           </div>
-          <div ref="chart4" class="chart-box"></div>
+          <div class="chart-wrap" v-loading="hospitalLoading" element-loading-text="闄㈠尯缁村害鍔犺浇涓�...">
+            <div ref="chart4" class="chart-box"></div>
+            <div v-if="!hospitalLoading && !hospitalCompare.hospitals.length" class="chart-empty">鏆傛棤闄㈠尯缁村害鏁版嵁</div>
+          </div>
         </el-card>
       </el-col>
     </el-row>
@@ -105,10 +121,11 @@
               </el-select>
             </div>
           </div>
-          <el-table :data="rankRows" border size="small" max-height="400">
-            <el-table-column type="index" label="鎺掑悕" width="60" align="center" />
+          <el-table :data="rankRows" border size="small" max-height="400" v-loading="rankLoading" element-loading-text="鎺掑悕鍔犺浇涓�...">
+            <el-table-column prop="rank" label="鎺掑悕" width="60" align="center" />
             <el-table-column prop="name" :label="rankDimLabel" />
             <el-table-column prop="score" label="缁煎悎寰楀垎" width="120" align="center" />
+            <el-table-column prop="itemCount" label="椤规暟" width="100" align="center" />
           </el-table>
         </el-card>
       </el-col>
@@ -128,10 +145,11 @@
               </el-select>
             </div>
           </div>
-          <el-table :data="crossRows" border size="small" max-height="400">
-            <el-table-column type="index" label="鎺掑悕" width="60" align="center" />
+          <el-table :data="crossRows" border size="small" max-height="400" v-loading="crossLoading" element-loading-text="鎺掑悕鍔犺浇涓�...">
+            <el-table-column prop="rank" label="鎺掑悕" width="60" align="center" />
             <el-table-column prop="name" :label="crossDimLabel" />
             <el-table-column prop="score" label="缁煎悎寰楀垎" width="120" align="center" />
+            <el-table-column prop="itemCount" label="椤规暟" width="100" align="center" />
           </el-table>
         </el-card>
       </el-col>
@@ -141,54 +159,29 @@
 
 <script>
 import * as echarts from 'echarts';
+import { mydTrend, sltdMydTotalByDimension, sltdMydTotalByScore ,mzMydScoreRank} from "@/api/AiCentre/satisfaction";
+import { getissuelist } from "@/api/AiCentre/questionnaire";
 
-// 鍗曢閫夐」
-const QUESTION_OPTIONS = ['闈炲父婊℃剰', '婊℃剰', '涓�鑸�', '涓嶆弧鎰�'];
-// 鍙�夐鐩�
-const QUESTIONS = [
-  { id: 22, title: '绗�22棰橈細鎶辨�ㄥ拰涓嶆弧鑳藉惁鍙婃椂寰楀埌鍥炲簲锛�' },
-  { id: 1, title: '绗�1棰橈細鎮ㄥ鎴戦櫌宸ヤ綔浜哄憳鐨勬湇鍔¤川閲忔弧鎰忎笉婊℃剰锛�' },
-  { id: 5, title: '绗�5棰橈細鎮ㄥ鍖婚櫌鏈嶅姟璁炬柦鐨勬�讳綋鍗拌薄濡備綍锛�' },
-  { id: 8, title: '绗�8棰橈細鍖荤敓瀵规偍鏄惁灏婇噸锛�' },
-  { id: 12, title: '绗�12棰橈細鎶ゅ+瀵规偍鏄惁灏婇噸锛�' },
-  { id: 15, title: '绗�15棰橈細鎮ㄥ棰勭害鎸傚彿鏄惁婊℃剰锛�' },
-];
 const DEPTS = ['淇濆仴绉�', '浠嬪叆娌荤枟绉�', '涓撶鎶ゅ+闂ㄨ瘖', '搴峰鍖诲绉�', '鐨偆绉�', '鍐呭垎娉岀', '娑堝寲鍐呯', '鑲惧唴绉�', '鍛煎惛鍐呯', '蹇冭绠″唴绉�', '绁炵粡鍐呯', '琛�娑茬', '椋庢箍鍏嶇柅绉�', '鑲跨槫鍐呯', '鎰熸煋绉�', '鑰佸勾绉�', '鏅绉�', '楠ㄧ', '娉屽翱澶栫', '濡囦骇绉�'];
 const WARDS = ['涓�鐥呭尯', '浜岀梾鍖�', '涓夌梾鍖�', '鍥涚梾鍖�', '浜旂梾鍖�', '鍏梾鍖�', '涓冪梾鍖�', '鍏梾鍖�'];
 
-// 纭畾鎬т吉闅忔満
-function hashString(str) {
-  let h = 2166136261;
-  for (let i = 0; i < str.length; i++) {
-    h ^= str.charCodeAt(i);
-    h = Math.imul(h, 16777619);
-  }
-  return h >>> 0;
-}
-function mulberry32(a) {
-  return function () {
-    a |= 0;
-    a = (a + 0x6d2b79f5) | 0;
-    let t = Math.imul(a ^ (a >>> 15), 1 | a);
-    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
-    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
-  };
-}
-
 export default {
   name: 'ChartsPanel',
+  dicts: ['dimensionality_type'],
   data() {
     return {
       queryParams: {
         dateRange: [],
         interval: 'month',
+        dimension: 'ward', // 缁熻缁村害锛歸ard=鎸夌梾鍖虹粺璁� dept=鎸夌瀹ょ粺璁�
       },
       deptOptions: DEPTS,
       wardOptions: WARDS,
-      questionOptions: QUESTIONS,
+      questionOptions: [],
 
       // 鍗曢绛涢��
-      singleQuestionId: 22,
+      singleQuestionId: null,
+      singleQuestionContent: '',
       singleDeptWards: [],
 
       // 鎺掑悕缁村害绛涢��
@@ -198,22 +191,44 @@
       crossDeptWards: [],
 
       // 鍥捐〃鏁版嵁
+      trendLoading: false,
+      categoryLoading: false,
+      hospitalLoading: false,
       trend: { buckets: [], data: [], seriesName: '闂ㄨ瘖鎮h�呮弧鎰忓害' },
       categoryCompare: { categories: [], seriesNames: [], series1: [], series2: [] },
       hospitalCompare: { hospitals: [], seriesNames: [], series1: [], series2: [] },
 
+      // 鍗曢 / 鎺掑悕 / 浜ゅ弶琛ㄥ姞杞界姸鎬�
+      singleLoading: false,
+      questionLoading: false,
+      rankLoading: false,
+      crossLoading: false,
+
       // 琛ㄦ牸鏁版嵁
       singleQuestionData: [],
-      singleQuestionMeta: { total: '0', score: '0.00%' },
-      deptRankData: [],
-      wardRankData: [],
-      crossDeptData: [],
-      crossWardData: [],
+      singleQuestionMeta: { total: '0', score: '0.00' },
+      rankData: [],
+      crossData: [],
     };
   },
   computed: {
+    // 绉戝/鐥呭尯缂栫爜锛氫紭鍏堝彇褰撳墠鐢ㄦ埛鎵�灞烇紝鏃犲垯鍏滃簳鍚嶇О鏈韩浣滀负缂栫爜
+    deptCodeList() {
+      const list = this.$store.getters.belongDepts || [];
+      return list.length ? list : DEPTS.map((d) => ({ deptName: d, deptCode: d }));
+    },
+    wardCodeList() {
+      const list = this.$store.getters.belongWards || [];
+      return list.length ? list : WARDS.map((w) => ({ districtName: w, districtCode: w }));
+    },
+    defaultDeptCodes() {
+      return Array.from(new Set(this.deptCodeList.map((d) => d.deptCode)));
+    },
+    defaultWardCodes() {
+      return Array.from(new Set(this.wardCodeList.map((w) => w.districtCode)));
+    },
     intervalLabel() {
-      return { day: '鎸夊ぉ', month: '鎸夋湀', year: '鎸夊勾' }[this.queryParams.interval] || '鎸夋湀';
+      return { day: '鎸夊ぉ', week: '鎸夊懆', month: '鎸夋湀', year: '鎸夊勾' }[this.queryParams.interval] || '鎸夋湀';
     },
     trendMoM() {
       const d = this.trend.data;
@@ -237,14 +252,10 @@
       return this.crossDimension === 'dept' ? this.deptOptions : this.wardOptions;
     },
     rankRows() {
-      const list = this.rankDimension === 'dept' ? this.deptRankData : this.wardRankData;
-      if (!this.rankDeptWards.length) return list;
-      return list.filter((r) => this.rankDeptWards.includes(r.name));
+      return this.rankData;
     },
     crossRows() {
-      const list = this.crossDimension === 'dept' ? this.crossDeptData : this.crossWardData;
-      if (!this.crossDeptWards.length) return list;
-      return list.filter((r) => this.crossDeptWards.includes(r.name));
+      return this.crossData;
     },
   },
   mounted() {
@@ -253,7 +264,26 @@
         this.queryParams.dateRange = this.defaultRange();
       }
       this.initCharts();
+      // 鍒濆鍖栧嵆鏄剧ず鍚勬暟鎹潡鐨勫姞杞界姸鎬侊紝閬垮厤鍏堥棯鐜扳�滄殏鏃犳暟鎹��
+      this.trendLoading = true;
+      this.categoryLoading = true;
+      this.hospitalLoading = true;
+      this.singleLoading = true;
+      this.rankLoading = true;
+      this.crossLoading = true;
+      // 鏁版嵁鍧椾笌棰樼洰鍒楄〃骞惰鍔犺浇锛氭暟鎹潡涓嶄緷璧栭鐩垪琛紝鏃犻渶涓茶绛夊緟
       this.refreshAll();
+      this.loadQuestions('').then(() => {
+        if (this.singleQuestionContent) {
+          // 鍗曢閫夐」缁熻 / 鍗曢浜ゅ弶鎺掑悕渚濊禆闂鏂囨湰锛岄鐩姞杞藉畬鎴愬悗琛ユ媺涓�娆�
+          this.fetchSingleQuestion();
+          this.fetchCross();
+        } else {
+          // 棰樼洰鍒楄〃涓虹┖鎴栧姞杞藉け璐ワ紝缁撴潫鍗曢鐩稿叧鍔犺浇鐘舵��
+          this.singleLoading = false;
+          this.crossLoading = false;
+        }
+      });
     });
   },
   methods: {
@@ -269,9 +299,163 @@
         if (c) c.resize();
       });
     },
+    // 缁村害缂栫爜 鈫� 瀛楀吀鍚嶇О锛坉imensionality_type锛�
+    getDimensionLabel(code) {
+      const dict = this.dict && this.dict.type && this.dict.type.dimensionality_type;
+      if (dict) {
+        const hit = dict.find((item) => String(item.value) === String(code));
+        if (hit) return hit.label;
+      }
+      return code == null ? '' : String(code);
+    },
+    // 鍙�夐鐩細杩滅▼妯$硦鎼滅储锛堟寜闂鍐呭 scriptContent锛�
+    searchQuestions(keyword) {
+      this.loadQuestions(keyword || '');
+    },
+    // 鍔犺浇鍙�夐鐩紙鍙傝�冮棶棰樺垪琛ㄩ〉 getissuelist锛�
+    async loadQuestions(keyword) {
+      this.questionLoading = true;
+      try {
+        const res = await getissuelist({
+          pageNum: 1,
+          pageSize: 50,
+          isavailable: '',
+          scriptContent: keyword || '',
+        });
+        const rows = (res && res.rows) || [];
+        this.questionOptions = rows.map((q) => ({
+          id: q.id,
+          scriptContent: q.scriptContent || q.title || '',
+        }));
+        // 鏃犲叧閿瓧鏃堕粯璁ら�変腑绗竴棰�
+        if (!keyword && this.questionOptions.length && !this.singleQuestionContent) {
+          const first = this.questionOptions[0];
+          this.singleQuestionId = first.id;
+          this.singleQuestionContent = first.scriptContent;
+        }
+      } catch (e) {
+        this.questionOptions = [];
+      } finally {
+        this.questionLoading = false;
+      }
+    },
+    onSingleQuestionChange(val) {
+      const q = this.questionOptions.find((o) => String(o.id) === String(val));
+      this.singleQuestionContent = q ? q.scriptContent : '';
+      this.refreshAll();
+    },
+    // 绉戝/鐥呭尯鍚嶇О 鈫� 缂栫爜
+    mapDeptNames(names) {
+      return names.map((n) => {
+        const hit = this.deptCodeList.find((d) => d.deptName === n);
+        return hit ? hit.deptCode : n;
+      });
+    },
+    mapWardNames(names) {
+      return names.map((n) => {
+        const hit = this.wardCodeList.find((w) => w.districtName === n);
+        return hit ? hit.districtCode : n;
+      });
+    },
+    // 缁煎悎寰楀垎锛堢湡瀹炲彛寰勶級锛毼�(閫夐」鍒嗗�� 脳 濉姤閲�) / 危(濉姤閲�)
+    scoreOfOptions(options) {
+      let num = 0;
+      let den = 0;
+      (options || []).forEach((o) => {
+        const c = Number(o && o.count) || 0;
+        const s = Number(o && o.score);
+        if (isNaN(s)) return;
+        num += s * c;
+        den += c;
+      });
+      if (!den) return '0.00';
+      return (num / den).toFixed(2);
+    },
+    // 瑙f瀽姣斾緥瀛楃涓诧細"14.29%"銆�"0"銆�"0.5" 鈫� 鏁板�硷紙鐧惧垎姣旓級锛涙棤鏁堣繑鍥� null
+    parsePercent(value) {
+      if (value == null || value === '') return null;
+      const n = Number(String(value).replace(/%/g, '').trim());
+      return isNaN(n) ? null : n;
+    },
+    // 鍗曢杩斿洖 List<QuestionResultDTO> 鈫� { options, total }
+    normalizeSingleQuestion(list) {
+      const arr = Array.isArray(list) ? list : (list && (list.list || list.rows)) || [];
+      const options = [];
+      let total = 0;
+      arr.forEach((item) => {
+        const details = Array.isArray(item && item.subtaskDetailRatioExportList)
+          ? item.subtaskDetailRatioExportList
+          : [];
+        details.forEach((d) => {
+          const option = (d && (d.optionresult || d.targetvalue || d.optioncontent || d.questiontext)) || '';
+          const count = Number(d && d.count) || 0;
+          const rawScore = Number(d && d.score);
+          const ratio = this.parsePercent(d && d.ratio);
+          options.push({ option, count, score: isNaN(rawScore) ? null : rawScore, ratio });
+          total += count;
+        });
+      });
+      return { options, total };
+    },
+    // 鍗曢閫夐」缁熻锛堢湡瀹炴帴鍙� /smartor/serviceSubtaskDetail/sltdMydTotalByScore锛�
+    async fetchSingleQuestion() {
+      const range = this.queryParams.dateRange || [];
+      if (range.length !== 2 || !range[0] || !range[1] || !this.singleQuestionContent) {
+        this.singleQuestionData = [];
+        this.singleQuestionMeta = { total: '0', score: '0.00' };
+        return;
+      }
+      this.singleLoading = true;
+      try {
+        const payload = {
+          deptOrDistrict: '1',
+          serviceTypeList: ['6', '14'],
+          startFinishTime: range[0],
+          endFinishTime: range[1],
+          questionContent: this.singleQuestionContent,
+        };
+        const sel = this.singleDeptWards || [];
+        if (this.queryParams.dimension === 'dept') {
+          const names = sel.map((s) => String(s).replace(/^dept:/, ''));
+          payload.leaveldeptcodes = names.length ? this.mapDeptNames(names) : this.defaultDeptCodes;
+        } else {
+          const names = sel.map((s) => String(s).replace(/^ward:/, ''));
+          payload.leavehospitaldistrictcodes = names.length ? this.mapWardNames(names) : this.defaultWardCodes;
+        }
+        const res = await sltdMydTotalByScore(payload);
+        console.log('[sltdMydTotalByScore 鍗曢] code=', res && res.code, 'data=', JSON.stringify(res && res.data).slice(0, 1500));
+        const list = res && res.code === 200 ? res.data : [];
+        const { options, total } = this.normalizeSingleQuestion(list);
+        this.singleQuestionData = options.map((o) => ({
+          option: o.option,
+          count: o.count,
+          rate: o.ratio != null
+            ? o.ratio.toFixed(2) + '%'
+            : (total ? ((o.count / total) * 100).toFixed(2) + '%' : '0.00%'),
+        }));
+        this.singleQuestionMeta = {
+          total: total.toLocaleString(),
+          score: this.scoreOfOptions(options),
+        };
+        if (!options.length) this.notify('褰撳墠鏉′欢涓嬫殏鏃犺棰樼殑閫夐」缁熻鏁版嵁', 'warning');
+      } catch (e) {
+        this.singleQuestionData = [];
+        this.singleQuestionMeta = { total: '0', score: '0.00' };
+        this.notify('鍗曢閫夐」缁熻鏁版嵁璇锋眰澶辫触锛岃绋嶅悗閲嶈瘯', 'error');
+      } finally {
+        this.singleLoading = false;
+      }
+    },
     handleReset() {
       this.queryParams.dateRange = this.defaultRange();
       this.queryParams.interval = 'month';
+      this.queryParams.dimension = 'ward';
+      this.singleDeptWards = [];
+      this.refreshAll();
+    },
+    // 鍒囨崲鍏ㄥ眬缁熻缁村害鏃舵竻绌哄崟棰樼瓫閫夛紝淇濊瘉绉戝/鐥呭尯浜掓枼
+    onDimensionChange() {
+      this.singleDeptWards = [];
       this.refreshAll();
     },
     onRankDimensionChange() {
@@ -285,7 +469,7 @@
     defaultRange() {
       const end = new Date();
       const start = new Date();
-      start.setDate(start.getDate() - 29);
+      start.setDate(start.getDate() - 91);
       const f = (d) => {
         const y = d.getFullYear();
         const m = String(d.getMonth() + 1).padStart(2, '0');
@@ -298,6 +482,7 @@
     intervalConfig() {
       const buckets = this.genBuckets(this.queryParams.interval);
       if (this.queryParams.interval === 'day') return { buckets, seriesNames: ['鏄ㄦ棩', '浠婃棩'] };
+      if (this.queryParams.interval === 'week') return { buckets, seriesNames: ['涓婂懆', '鏈懆'] };
       if (this.queryParams.interval === 'year') return { buckets, seriesNames: ['鍘诲勾', '浠婂勾'] };
       return { buckets, seriesNames: ['涓婃湀', '鏈湀'] };
     },
@@ -312,6 +497,12 @@
           d.setDate(end.getDate() - i);
           buckets.push(`${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`);
         }
+      } else if (type === 'week') {
+        for (let i = 5; i >= 0; i--) {
+          const d = new Date(end);
+          d.setDate(end.getDate() - i * 7);
+          buckets.push(`${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} 鍛╜);
+        }
       } else if (type === 'year') {
         for (let i = 3; i >= 0; i--) buckets.push(String(end.getFullYear() - i));
       } else {
@@ -322,83 +513,208 @@
       }
       return buckets;
     },
-    // 鍒锋柊鍏ㄩ儴鏁版嵁锛堟ā鎷燂紝鎺ュ彛灏辩华鍚庢浛鎹负鐪熷疄璇锋眰锛�
-    refreshAll() {
-      const baseChart = JSON.stringify(this.queryParams);
-      const baseTime = JSON.stringify({ dateRange: this.queryParams.dateRange });
-
-      // 鍥捐〃锛氬尯闂寸被鍨� + 鏃堕棿鑼冨洿
-      this.genChartData(mulberry32(hashString(baseChart + 'charts')));
-      // 鍗曢琛細鏃堕棿鑼冨洿 + 棰樼洰 + 绉戝/鐥呭尯
-      this.genSingleData(mulberry32(hashString(baseTime + JSON.stringify({ sq: this.singleQuestionId, sdw: this.singleDeptWards }))));
-      // 绉戝/鐥呭尯鎺掑悕锛氭椂闂磋寖鍥� + 缁村害 + 閫夋嫨
-      this.genRankData(mulberry32(hashString(baseTime + JSON.stringify({ rd: this.rankDimension, rdw: this.rankDeptWards }))));
-      // 鍗曢脳绉戝/鐥呭尯鎺掑悕锛氭椂闂磋寖鍥� + 缁村害 + 閫夋嫨 + 棰樼洰
-      this.genCrossData(mulberry32(hashString(baseTime + JSON.stringify({ cd: this.crossDimension, cdw: this.crossDeptWards, sq: this.singleQuestionId }))));
+    // 鍒锋柊鍏ㄩ儴鏁版嵁锛堣秼鍔裤�佸垎绫荤淮搴︺�侀櫌鍖虹淮搴︺�佸崟棰樸�佹帓鍚嶃�佷氦鍙夊潎涓虹湡瀹炴帴鍙o級
+    async refreshAll() {
+      // 鍏釜鏁版嵁鍧楀苟琛屾媺鍙栵紝鍚勫尯鍧楀悓鏃舵樉绀哄悇鑷殑鍔犺浇鐘舵��
+      await Promise.all([
+        this.fetchTrend(),
+        this.fetchCategoryCompare(),
+        this.fetchHospitalCompare(),
+        this.fetchSingleQuestion(),
+        this.fetchRank(),
+        this.fetchCross(),
+      ]);
 
       this.renderCharts();
     },
-    genChartData(rnd) {
-      const jitter = (lo, hi) => lo + rnd() * (hi - lo);
-      const cfg = this.intervalConfig();
-      const buckets = cfg.buckets;
+    // 婊℃剰搴﹁秼鍔匡紙鐪熷疄鎺ュ彛锛夛細data = [{ period, totalScore }]
+    async fetchTrend() {
+      const range = this.queryParams.dateRange || [];
+      if (range.length !== 2 || !range[0] || !range[1]) {
+        this.trend = { buckets: [], data: [], seriesName: '闂ㄨ瘖鎮h�呮弧鎰忓害' };
+        return;
+      }
 
-      this.trend = {
-        buckets,
-        seriesName: '闂ㄨ瘖鎮h�呮弧鎰忓害',
-        data: buckets.map(() => Math.round(jitter(86, 90) * 100) / 100),
-      };
-
-      const categories = ['闅愮淇濇姢', '鐜涓庢爣璇�', '鍖诲姟浜哄憳鍥炲簲', '鍖荤敓娌熼��', '鎸傚彿浣撻獙', '鎶ゅ+娌熼��'];
-      this.categoryCompare = {
-        categories,
-        seriesNames: cfg.seriesNames,
-        series1: categories.map(() => Math.round(jitter(82, 88) * 100) / 100),
-        series2: categories.map(() => Math.round(jitter(83, 89) * 100) / 100),
-      };
-
-      const hospitals = ['闄㈠尯涓�', '闄㈠尯浜�', '闄㈠尯涓�'];
-      this.hospitalCompare = {
-        hospitals,
-        seriesNames: cfg.seriesNames,
-        series1: hospitals.map(() => Math.round(jitter(85, 89) * 100) / 100),
-        series2: hospitals.map(() => Math.round(jitter(86, 90) * 100) / 100),
+      this.trendLoading = true;
+      try {
+        const res = await mydTrend({
+          startFinishTime: range[0],
+          endFinishTime: range[1],
+          type: this.queryParams.interval, // day / week / month / year
+        });
+        const list = res && res.code === 200 && Array.isArray(res.data) ? res.data : [];
+        this.trend = {
+          seriesName: '闂ㄨ瘖鎮h�呮弧鎰忓害',
+          buckets: list.map((i) => String((i && i.period) || '')),
+          data: list.map((i) => {
+            const n = Number(i && i.totalScore);
+            return isNaN(n) ? 0 : Math.round(n * 100) / 100;
+          }),
+        };
+        if (!list.length) this.notify('褰撳墠鏉′欢涓嬫殏鏃犺秼鍔挎暟鎹�', 'warning');
+      } catch (e) {
+        this.trend = { buckets: [], data: [], seriesName: '闂ㄨ瘖鎮h�呮弧鎰忓害' };
+        this.notify('瓒嬪娍鏁版嵁璇锋眰澶辫触锛岃绋嶅悗閲嶈瘯', 'error');
+      } finally {
+        this.trendLoading = false;
+      }
+    },
+    notify(msg, type) {
+      const fn = { warning: 'warning', error: 'error', success: 'success' }[type] || 'info';
+      if (this.$message && this.$message[fn]) this.$message[fn](msg);
+    },
+    // 鍒嗙被缁村害瀵规瘮锛堢湡瀹炴帴鍙o級锛歞imensionType=1锛宒ata = [{ period, dimension, totalScore }]
+    async fetchCategoryCompare() {
+      const range = this.queryParams.dateRange || [];
+      if (range.length !== 2 || !range[0] || !range[1]) {
+        this.categoryCompare = { categories: [], seriesNames: [], series1: [], series2: [] };
+        return;
+      }
+      this.categoryLoading = true;
+      try {
+        const res = await sltdMydTotalByDimension({
+          startFinishTime: range[0],
+          endFinishTime: range[1],
+          type: this.queryParams.interval, // day / week / month / year
+          dimensionType: "1", // 1=鎸夋弧鎰忓害缁村害缁熻
+        });
+        // 鎺掓煡鐢細鎵撳嵃鎺ュ彛鍘熷杩斿洖锛屼究浜庢牳瀵瑰瓧娈靛悕锛堢‘璁ゆ棤璇悗鍙垹闄わ級
+        console.log("[sltdMydTotalByDimension dimensionType=1] code=", res && res.code, "data=", JSON.stringify(res && res.data).slice(0, 2000));
+        const list = res && res.code === 200 && Array.isArray(res.data) ? res.data : [];
+        // 缁村害缂栫爜 鈫� 瀛楀吀鍚嶇О锛屽苟杩囨护涓ゆ湡鍧囨棤鏁版嵁鐨勭淮搴�
+        const d = this.buildCompareData(list, (code) => this.getDimensionLabel(code));
+        this.categoryCompare = { categories: d.labels, seriesNames: d.seriesNames, series1: d.series1, series2: d.series2 };
+        if (!list.length) this.notify('褰撳墠鏉′欢涓嬫殏鏃犲垎绫荤淮搴︽暟鎹�', 'warning');
+      } catch (e) {
+        this.categoryCompare = { categories: [], seriesNames: [], series1: [], series2: [] };
+        this.notify('鍒嗙被缁村害鏁版嵁璇锋眰澶辫触锛岃绋嶅悗閲嶈瘯', 'error');
+      } finally {
+        this.categoryLoading = false;
+      }
+    },
+    // 闄㈠尯缁村害瀵规瘮锛堢湡瀹炴帴鍙o級锛歞imensionType=2锛宒ata = [{ period, dimension(闄㈠尯鍚�), totalScore }]
+    async fetchHospitalCompare() {
+      const range = this.queryParams.dateRange || [];
+      if (range.length !== 2 || !range[0] || !range[1]) {
+        this.hospitalCompare = { hospitals: [], seriesNames: [], series1: [], series2: [] };
+        return;
+      }
+      this.hospitalLoading = true;
+      try {
+        const res = await sltdMydTotalByDimension({
+          startFinishTime: range[0],
+          endFinishTime: range[1],
+          type: this.queryParams.interval, // day / week / month / year
+          dimensionType: "2", // 2=鎸夐櫌鍖虹粺璁�
+        });
+        // 鎺掓煡鐢細鎵撳嵃鎺ュ彛鍘熷杩斿洖锛屼究浜庢牳瀵瑰瓧娈靛悕锛堢‘璁ゆ棤璇悗鍙垹闄わ級
+        console.log("[sltdMydTotalByDimension dimensionType=2] code=", res && res.code, "data=", JSON.stringify(res && res.data).slice(0, 2000));
+        const list = res && res.code === 200 && Array.isArray(res.data) ? res.data : [];
+        const d = this.buildCompareData(list);
+        this.hospitalCompare = { hospitals: d.labels, seriesNames: d.seriesNames, series1: d.series1, series2: d.series2 };
+        if (!list.length) this.notify('褰撳墠鏉′欢涓嬫殏鏃犻櫌鍖虹淮搴︽暟鎹�', 'warning');
+      } catch (e) {
+        this.hospitalCompare = { hospitals: [], seriesNames: [], series1: [], series2: [] };
+        this.notify('闄㈠尯缁村害鏁版嵁璇锋眰澶辫触锛岃绋嶅悗閲嶈瘯', 'error');
+      } finally {
+        this.hospitalLoading = false;
+      }
+    },
+    // 灏� [{ period, dimension, totalScore }] 缁勮涓哄垎缁勬煴鐘跺浘鏁版嵁锛堜笂鏈�/鏈湡涓ょ粍瀵规瘮锛�
+    // labelFn锛氬彲閫夛紝鐢ㄤ簬鎶婄淮搴︾紪鐮佹槧灏勪负瀛楀吀鍚嶇О锛堝垎绫荤淮搴﹀浘浼犲叆锛�
+    buildCompareData(list, labelFn) {
+      const labels = [];
+      const byCat = {};
+      list.forEach((i) => {
+        // 缁村害缂栫爜锛坉imensionType=1锛夋垨闄㈠尯鍚嶇О锛坉imensionType=2锛夛紱鑻ュ悗绔瓧娈靛懡鍚嶄笉鍚岋紝鍏滃簳鍙� period
+        const key = (i && (i.dimension || i.campusName || i.campusid)) || (i && i.period) || '';
+        if (!key) return;
+        if (!byCat[key]) { byCat[key] = {}; labels.push(key); }
+        const n = Number(i.totalScore);
+        byCat[key][i.period] = isNaN(n) ? 0 : Math.round(n * 100) / 100;
+      });
+      // 鍚庣鎸夋椂闂村崌搴忚繑鍥烇紝鍙栨渶鍚庝袱涓懆鏈熶綔涓衡�滀笂鏈� / 鏈湡鈥�
+      const periods = [...new Set(list.map((i) => i && i.period).filter(Boolean))];
+      const cur = periods[periods.length - 1];
+      const prev = periods[periods.length - 2];
+      const val = (cat, p) => (p != null && byCat[cat][p] != null ? byCat[cat][p] : 0);
+      // 杩囨护涓ゆ湡鍧囨棤鏁版嵁鐨勭淮搴�
+      const cats = labels.filter((c) => val(c, prev) !== 0 || val(c, cur) !== 0);
+      const display = labelFn ? cats.map((c) => labelFn(c)) : cats;
+      return {
+        labels: display,
+        seriesNames: this.intervalConfig().seriesNames,
+        series1: cats.map((c) => val(c, prev)),
+        series2: cats.map((c) => val(c, cur)),
       };
     },
-    genSingleData(rnd) {
-      const jitter = (lo, hi) => lo + rnd() * (hi - lo);
-      const scopeFactor = this.singleDeptWards.length ? 0.18 : 1;
-      const total = Math.round(jitter(20000, 30000) * scopeFactor);
-      const weights = [0.62, 0.27, 0.07, 0.04];
-      const counts = weights.map((w) => Math.max(0, Math.round(total * w * jitter(0.92, 1.08))));
-      const good = counts[0] + counts[1];
-      this.singleQuestionData = QUESTION_OPTIONS.map((opt, i) => ({
-        option: opt,
-        count: counts[i],
-        rate: ((counts[i] / total) * 100).toFixed(2) + '%',
+    // 绉戝/鐥呭尯缁村害 路 闂ㄨ瘖鎮h�呮弧鎰忓害寰楀垎鎺掑悕锛堢湡瀹炴帴鍙� /mzMydScoreRank锛屼笉甯﹂棶棰樻枃鏈級
+    async fetchRank() {
+      const range = this.queryParams.dateRange || [];
+      if (range.length !== 2 || !range[0] || !range[1]) {
+        this.rankData = [];
+        return;
+      }
+      this.rankLoading = true;
+      try {
+        const res = await mzMydScoreRank(this.buildRankPayload(this.rankDimension, this.rankDeptWards, ''));
+        this.rankData = this.normalizeRank(res);
+        if (!this.rankData.length) this.notify('褰撳墠鏉′欢涓嬫殏鏃犳帓鍚嶆暟鎹�', 'warning');
+      } catch (e) {
+        this.rankData = [];
+        this.notify('鎺掑悕鏁版嵁璇锋眰澶辫触锛岃绋嶅悗閲嶈瘯', 'error');
+      } finally {
+        this.rankLoading = false;
+      }
+    },
+    // 鍗曢脳绉戝/鐥呭尯 路 寰楀垎鎺掑悕锛堢湡瀹炴帴鍙� /mzMydScoreRank锛屽甫闂鏂囨湰锛�
+    async fetchCross() {
+      const range = this.queryParams.dateRange || [];
+      if (range.length !== 2 || !range[0] || !range[1] || !this.singleQuestionContent) {
+        this.crossData = [];
+        return;
+      }
+      this.crossLoading = true;
+      try {
+        const res = await mzMydScoreRank(this.buildRankPayload(this.crossDimension, this.crossDeptWards, this.singleQuestionContent));
+        this.crossData = this.normalizeRank(res);
+        if (!this.crossData.length) this.notify('褰撳墠鏉′欢涓嬫殏鏃犳帓鍚嶆暟鎹�', 'warning');
+      } catch (e) {
+        this.crossData = [];
+        this.notify('鎺掑悕鏁版嵁璇锋眰澶辫触锛岃绋嶅悗閲嶈瘯', 'error');
+      } finally {
+        this.crossLoading = false;
+      }
+    },
+    // 缁勮鎺掑悕鎺ュ彛璇锋眰浣擄紙dimensionType/serviceType/deptCodes/districtCodes/鏃堕棿/questionText锛�
+    buildRankPayload(dimension, selectedNames, questionText) {
+      const range = this.queryParams.dateRange || [];
+      const payload = {
+        dimensionType: dimension === 'dept' ? '1' : '2',
+        serviceTypes: ['6', '14'],
+        startFinishTime: range[0],
+        endFinishTime: range[1],
+      };
+      if (questionText) payload.questionText = questionText;
+      if (selectedNames && selectedNames.length) {
+        if (dimension === 'dept') {
+          payload.deptCodes = this.mapDeptNames(selectedNames);
+        } else {
+          payload.districtCodes = this.mapWardNames(selectedNames);
+        }
+      }
+      return payload;
+    },
+    // 褰掍竴鍖栨帓鍚嶈繑鍥� List<MzMydScoreRankDTO> 鈫� [{ rank, code, name, score, itemCount }]
+    normalizeRank(res) {
+      const list = res && res.code === 200 && Array.isArray(res.data) ? res.data : [];
+      return list.map((item) => ({
+        rank: item.rank,
+        code: item.code,
+        name: item.name,
+        score: item.score,
+        itemCount: item.itemCount,
       }));
-      this.singleQuestionMeta = {
-        total: total.toLocaleString(),
-        score: ((good / total) * 100).toFixed(2) + '%',
-      };
-    },
-    genRankData(rnd) {
-      const jitter = (lo, hi) => lo + rnd() * (hi - lo);
-      this.deptRankData = DEPTS
-        .map((d) => ({ name: d, score: jitter(70, 85).toFixed(2) }))
-        .sort((a, b) => Number(b.score) - Number(a.score));
-      this.wardRankData = WARDS
-        .map((w) => ({ name: w, score: jitter(70, 85).toFixed(2) }))
-        .sort((a, b) => Number(b.score) - Number(a.score));
-    },
-    genCrossData(rnd) {
-      const jitter = (lo, hi) => lo + rnd() * (hi - lo);
-      this.crossDeptData = DEPTS
-        .map((d) => ({ name: d, score: jitter(70, 85).toFixed(2) }))
-        .sort((a, b) => Number(b.score) - Number(a.score));
-      this.crossWardData = WARDS
-        .map((w) => ({ name: w, score: jitter(70, 85).toFixed(2) }))
-        .sort((a, b) => Number(b.score) - Number(a.score));
     },
     renderCharts() {
       const c1 = echarts.getInstanceByDom(this.$refs.chart1);
@@ -433,22 +749,46 @@
       });
 
       // 2. 鍒嗙粍鏌辩姸鍥� - 鍒嗙被缁村害
-      c2.setOption({
+      // 鍒嗙被杈冨锛堝鍗佸嚑涓弧鎰忓害缁村害锛夋椂搴曢儴鍚嶇О浼氶噸鍙狅紝杩欓噷鎸夋暟閲忓姩鎬侀�傞厤锛�
+      //   鈮�6 涓細姝e父姘村钩鏍囩锛�
+      //   7~10 涓細x 杞存爣绛惧�炬枩 35掳 + 搴曢儴鐣欑櫧鍔犲ぇ + 鏌卞瓙鍙樼粏锛�
+      //   >10 涓細鍐嶅惎鐢ㄦí鍚戠缉鏀撅紙鍒濆鏄剧ず绾� 8 涓垎绫伙紝鍙嫋鍔�/婊氬姩鏌ョ湅鍏朵綑锛夈��
+      const catCategories = this.categoryCompare.categories || [];
+      const catN = catCategories.length;
+      const catRotate = catN > 6 ? 35 : 0;
+      const catEnableZoom = catN > 10;
+      const catBarWidth = catN > 10 ? 18 : catN > 6 ? 20 : 22;
+      const catLegend = catEnableZoom
+        ? { data: this.categoryCompare.seriesNames, top: 4, textStyle: { color: '#666', fontSize: 12 } }
+        : { data: this.categoryCompare.seriesNames, bottom: 8, textStyle: { color: '#666', fontSize: 12 } };
+      const catGrid = catEnableZoom
+        ? { top: 36, right: 30, bottom: 80, left: 85 }
+        : { top: 48, right: 30, bottom: catRotate ? 100 : 60, left: 85 };
+      const catOption = {
         color: ['#6DC8EC', '#5B8FF9'],
-        grid: barGrid,
+        grid: catGrid,
         tooltip: { ...baseTooltip, trigger: 'axis', axisPointer: { type: 'shadow', shadowStyle: { color: 'rgba(0,0,0,0.03)' } } },
-        legend: { data: this.categoryCompare.seriesNames, bottom: 8, textStyle: { color: '#666', fontSize: 12 } },
-        xAxis: [{ ...baseXAxis, data: this.categoryCompare.categories, axisLabel: { ...baseXAxis.axisLabel, interval: 0 } }],
+        legend: catLegend,
+        xAxis: [{ ...baseXAxis, data: catCategories, axisLabel: { ...baseXAxis.axisLabel, interval: 0, rotate: catRotate } }],
         yAxis: [{ ...baseYAxis, name: '寰楀垎', nameTextStyle: { color: '#999', fontSize: 11 } }],
         series: [
-          { name: this.categoryCompare.seriesNames[0], type: 'bar', barWidth: 22, barGap: '30%',
+          { name: this.categoryCompare.seriesNames[0], type: 'bar', barWidth: catBarWidth, barGap: '30%',
             itemStyle: { borderRadius: [6, 6, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#6DC8EC' }, { offset: 1, color: '#48B0D5' }]) },
             data: this.categoryCompare.series1 },
-          { name: this.categoryCompare.seriesNames[1], type: 'bar', barWidth: 22,
+          { name: this.categoryCompare.seriesNames[1], type: 'bar', barWidth: catBarWidth,
             itemStyle: { borderRadius: [6, 6, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#5B8FF9' }, { offset: 1, color: '#3D6FDB' }]) },
             data: this.categoryCompare.series2 },
         ],
-      });
+      };
+      if (catEnableZoom) {
+        // 鍒濆鍙睍绀哄墠 8 涓垎绫诲搴︼紝閬垮厤鏌卞瓙鎸ゆ垚涓�鍥紝鐢ㄦ埛鍙í鍚戞粴鍔�/鎷栧姩鏌ョ湅鍏朵綑
+        const zoomEnd = catN > 0 ? Math.min(100, Math.round((8 / catN) * 100)) : 100;
+        catOption.dataZoom = [
+          { type: 'inside', start: 0, end: zoomEnd },
+          { type: 'slider', height: 16, bottom: 4, start: 0, end: zoomEnd },
+        ];
+      }
+      c2.setOption(catOption);
 
       // 4. 闄㈠尯缁村害 - 鍒嗙粍鏌辩姸鍥�
       c4.setOption({
@@ -527,6 +867,8 @@
 .filter-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; .filter-label { font-size: 13px; color: #606266; } .filter-date { width: 260px; } }
 .chart-card { margin-bottom: 0; }
 .chart-box { width: 100%; height: 360px; }
+.chart-wrap { position: relative; width: 100%; }
+.chart-empty { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; background: #fff; color: #909399; font-size: 14px; }
 .card-header { display: flex; justify-content: space-between; align-items: center; span { font-size: 15px; font-weight: 600; color: #303133; } }
 .header-right { display: flex; align-items: center; gap: 10px; .sel { width: 140px; } .sel-lg { width: 240px; } }
 .table-footer { text-align: right; padding: 10px 0 0; font-size: 13px; color: #606266; }

--
Gitblit v1.9.3