From e9f35f9782f3d99d742c294fe503fc1294f203c4 Mon Sep 17 00:00:00 2001
From: WXL (wul) <wl_5969728@163.com>
Date: 星期一, 14 九月 2026 11:10:31 +0800
Subject: [PATCH] 测试完成

---
 src/views/Satisfaction/sfstatistics/components/DataOverview.vue |  467 ++++++++++++++++++++++++++++++++++++++++++++++++---------
 1 files changed, 390 insertions(+), 77 deletions(-)

diff --git a/src/views/Satisfaction/sfstatistics/components/DataOverview.vue b/src/views/Satisfaction/sfstatistics/components/DataOverview.vue
index 18b5142..1987125 100644
--- a/src/views/Satisfaction/sfstatistics/components/DataOverview.vue
+++ b/src/views/Satisfaction/sfstatistics/components/DataOverview.vue
@@ -1,6 +1,54 @@
 <template>
   <div class="data-overview">
-    <!-- 琛ㄦ牸涓�锛氬悇鏈嶅姟椤圭洰婊℃剰搴︾粺璁� -->
+    <!-- 椤堕儴绛涢�夛細鏃堕棿鑼冨洿 + 绉戝/鐥呭尯锛屾帶鍒舵湰椤垫墍鏈夋暟鎹� -->
+    <el-card shadow="never" class="overview-card filter-card">
+      <el-form inline class="filter-form">
+        <el-form-item label="鏃堕棿鑼冨洿">
+          <el-date-picker
+            v-model="queryParams.dateRange"
+            type="daterange"
+            range-separator="鑷�"
+            start-placeholder="寮�濮嬫棩鏈�"
+            end-placeholder="缁撴潫鏃ユ湡"
+            value-format="yyyy-MM-dd"
+            size="small"
+          />
+        </el-form-item>
+        <el-form-item label="绉戝/鐥呭尯">
+          <el-select
+            v-model="queryParams.deptWard"
+            clearable
+            filterable
+            placeholder="鍏ㄩ儴"
+            size="small"
+            class="filter-select"
+          >
+            <el-option-group label="绉戝">
+              <el-option
+                v-for="d in deptOptions"
+                :key="'dept:' + d"
+                :label="d"
+                :value="'dept:' + d"
+              />
+            </el-option-group>
+            <el-option-group label="鐥呭尯">
+              <el-option
+                v-for="w in wardOptions"
+                :key="'ward:' + w"
+                :label="w"
+                :value="'ward:' + w"
+              />
+            </el-option-group>
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" size="small" icon="el-icon-search" @click="fetchData">鏌ヨ</el-button>
+          <el-button size="small" icon="el-icon-refresh" @click="handleReset">閲嶇疆</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 琛ㄦ牸涓�锛氬悇鏈嶅姟椤圭洰婊℃剰搴︾粺璁★紙鍥哄畾浜旈」閫夐」鐨勯鐩級 -->
     <el-card shadow="never" class="overview-card">
       <div slot="header" class="card-header">
         <span class="card-title">鍚勬湇鍔¢」鐩弧鎰忓害缁熻</span>
@@ -32,31 +80,48 @@
       </el-table>
     </el-card>
 
-    <!-- 琛ㄦ牸浜岋細鍗曢�夐/澶氶�夐缁熻 -->
+    <!-- 琛ㄦ牸浜岋細鍗曢�夐/澶氶�夐缁熻锛堝叾浣欓鐩級 -->
     <el-card shadow="never" class="overview-card">
       <div slot="header" class="card-header">
         <span class="card-title">鍗曢�夐/澶氶�夐缁熻</span>
         <el-button type="warning" size="small" icon="el-icon-download" @click="exportTable('table2')">瀵煎嚭Excel</el-button>
       </div>
-      <div class="question-block" v-for="(q, qi) in questionStats" :key="qi">
-        <h4 class="q-title">{{ q.title }}</h4>
-        <el-table :data="q.options" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
-          <el-table-column prop="option" label="閫夐」" min-width="180" />
-          <el-table-column prop="count" label="鏁伴噺" width="100" align="center" />
-          <el-table-column prop="rate" label="姣斾緥" width="100" align="center" />
-        </el-table>
-        <div v-if="q.total" class="q-footer">鏈鏈夋晥濉啓浜烘锛�<b>{{ q.total }}</b></div>
+      <div id="table2">
+        <div class="question-block" v-for="(q, qi) in questionStats" :key="qi">
+          <h4 class="q-title">{{ q.title }}</h4>
+          <el-table :data="q.options" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
+            <el-table-column prop="option" label="閫夐」" min-width="180" />
+            <el-table-column prop="count" label="鏁伴噺" width="100" align="center" />
+            <el-table-column prop="rate" label="姣斾緥" width="100" align="center" />
+          </el-table>
+          <div v-if="q.total" class="q-footer">鏈鏈夋晥濉啓浜烘锛�<b>{{ q.total }}</b></div>
+        </div>
       </div>
     </el-card>
 
-    <!-- 琛ㄦ牸涓夛細鍚勭瀹ゆ弧鎰忓害璇勫垎瀵规瘮 -->
+    <!-- 琛ㄦ牸涓夛細鍚勭瀹�/鐥呭尯婊℃剰搴﹁瘎鍒嗗姣� -->
     <el-card shadow="never" class="overview-card">
       <div slot="header" class="card-header">
-        <span class="card-title">鍚勭瀹ゆ弧鎰忓害璇勫垎瀵规瘮</span>
-        <el-button type="warning" size="small" icon="el-icon-download" @click="exportTable('table3')">瀵煎嚭Excel</el-button>
+        <span class="card-title">鍚勭瀹�/鐥呭尯婊℃剰搴﹁瘎鍒嗗姣�</span>
+        <div class="header-right">
+          <el-radio-group v-model="statDimension" size="small" @change="onStatDimensionChange">
+            <el-radio-button label="dept">鎸夌瀹�</el-radio-button>
+            <el-radio-button label="ward">鎸夌梾鍖�</el-radio-button>
+          </el-radio-group>
+          <el-select v-model="statDeptWard" size="small" class="stat-select" placeholder="娌跨敤椤堕儴鏉′欢">
+            <el-option label="娌跨敤椤堕儴鏉′欢" value="inherit" />
+            <template v-if="statDimension === 'dept'">
+              <el-option v-for="d in deptOptions" :key="'dept:' + d" :label="d" :value="'dept:' + d" />
+            </template>
+            <template v-else>
+              <el-option v-for="w in wardOptions" :key="'ward:' + w" :label="w" :value="'ward:' + w" />
+            </template>
+          </el-select>
+          <el-button type="warning" size="small" icon="el-icon-download" @click="exportTable('table3')">瀵煎嚭Excel</el-button>
+        </div>
       </div>
-      <el-table id="table3" :data="deptComparison" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
-        <el-table-column prop="dept" label="绉戝"fixed="left" align="center" />
+      <el-table id="table3" :data="table3Rows" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
+        <el-table-column prop="name" :label="statDimension === 'dept' ? '绉戝' : '鐥呭尯'" fixed="left" align="center" />
         <el-table-column label="绗�1棰�" align="center">
           <el-table-column prop="q1VeryGood" label="闈炲父婊℃剰" width="100" />
           <el-table-column prop="q1Good" label="婊℃剰" width="90" />
@@ -71,7 +136,7 @@
           <el-table-column prop="q2Bad" label="涓嶆弧鎰�" width="90" />
           <el-table-column prop="q2Score" label="寰楀垎" width="100" />
         </el-table-column>
-        <el-table-column prop="totalScore" label="缁煎悎寰楀垎"  align="center" />
+        <el-table-column prop="totalScore" label="缁煎悎寰楀垎" align="center" />
       </el-table>
     </el-card>
   </div>
@@ -80,68 +145,312 @@
 <script>
 import XLSX from 'xlsx';
 
+// 婊℃剰搴﹂鐨勫浐瀹氶�夐」锛堥『搴忓嵆琛ㄦ牸鍒楅『搴忥級
+const FIXED_OPTIONS = ['闈炲父婊℃剰', '婊℃剰', '涓�鑸�', '涓嶆弧鎰�', '涓嶉�傜敤'];
+const DEPTS = ['鍐呭垎娉岀', '鐨偆绉�', '鑲跨槫鍐呯', '娑堝寲鍐呯', '鑲惧唴绉�'];
+const WARDS = ['涓�鐥呭尯', '浜岀梾鍖�', '涓夌梾鍖�', '鍥涚梾鍖�', '浜旂梾鍖�'];
+
+// 婊℃剰搴﹂锛堝浐瀹氫簲椤归�夐」锛宑ounts 椤哄簭瀵瑰簲 FIXED_OPTIONS锛�
+const SATISFACTION_BASE = [
+  ['璇婄枟杩囩▼涓紝鍖诲姟浜哄憳鏄惁娉ㄦ剰淇濇姢鎮ㄧ殑闅愮锛�', [3219, 1351, 142, 33, 0]],
+  ['鎮ㄥ鍖婚櫌鏈嶅姟璁炬柦鐨勬�讳綋鍗拌薄濡備綍锛�', [3110, 1369, 217, 49, 0]],
+  ['鍖婚櫌鐨勫帟鎵�鏄惁娓呮磥鏃犲紓鍛筹紵', [2973, 1504, 235, 33, 0]],
+  ['鍖婚櫌鍐呯殑璺爣鍜屾寚绀烘槸鍚︽槑纭紵', [3234, 1289, 196, 26, 0]],
+  ['鍖婚櫌鐨勭┖闂村竷灞�鏄惁渚垮埄锛�', [3037, 1460, 209, 39, 0]],
+  ['鐪嬬梾鏈熼棿锛屾偍鐨勬姳鎬ㄥ拰涓嶆弧鑳藉惁鍙婃椂寰楀埌鍥炲簲锛�', [3019, 1317, 294, 115, 0]],
+  ['鍖荤敓鏄惁鐢ㄦ偍鍚緱鎳傜殑鏂瑰紡瑙i噴闂锛�', [3402, 967, 254, 122, 0]],
+  ['鍖荤敓瀵规偍鏄惁灏婇噸锛�', [3495, 897, 249, 104, 0]],
+  ['鍖荤敓鏄惁浠旂粏鍊惧惉鎮ㄨ璇濓紵', [3422, 937, 251, 135, 0]],
+  ['瀵规寕鍙锋湇鍔′汉鍛樻湇鍔℃�佸害鏄惁婊℃剰', [3458, 1205, 52, 30, 0]],
+  ['鎮ㄥ棰勭害鎸傚彿鏄惁婊℃剰锛�', [3527, 1088, 72, 58, 0]],
+  ['鎶ゅ+瀵规偍鏄惁灏婇噸锛�', [3420, 1184, 113, 28, 0]],
+  ['鎶ゅ+鏄惁鐢ㄦ偍鍚緱鎳傜殑鏂瑰紡瑙i噴闂锛�', [3346, 1250, 122, 27, 0]],
+  ['鎶ゅ+鏄惁浠旂粏鍊惧惉鎮ㄨ璇濓紵', [3378, 1205, 134, 28, 0]],
+];
+
+// 鍏朵綑棰樼洰锛堝崟閫�/澶氶�夛紝闈炲浐瀹氭弧鎰忓害閫夐」锛�
+const OTHER_BASE = [
+  {
+    title: '鎮ㄥ鎴戦櫌宸ヤ綔浜哄憳鐨勬湇鍔¤川閲忔弧鎰忎笉婊℃剰锛熴�愬垎鍊煎崟閫夐銆�',
+    type: 'single',
+    options: [['闈炲父婊℃剰', 1084], ['婊℃剰', 1959], ['涓�鑸�', 167], ['涓嶆弧鎰�', 153]],
+  },
+  {
+    title: '鎮ㄥ鍝儴鍒嗘湇鍔′笉婊℃剰锛熴�愬閫夐銆�',
+    type: 'multiple',
+    total: 408,
+    options: [['鍖荤敓璇婃柇鍜岃瘖鐤�', 131], ['鎶ょ悊鏈嶅姟', 47], ['鍏朵粬鍖诲姟浜哄憳鎶�鏈按骞�', 30], ['宸ヤ汉', 72]],
+  },
+  {
+    title: '鍖荤敓璇婃柇鍜屾不鐤椾腑鍝簺鏈嶅姟璐ㄩ噺涓嶆弧鎰忥紵銆愬閫夐銆�',
+    type: 'multiple',
+    total: 131,
+    options: [['鍖荤敓璇婃柇閿欒', 23], ['瀵规不鐤楁晥鏋滀笉婊℃剰', 72], ['鍏朵粬', 54]],
+  },
+];
+
+// 缁熶竴鎺ュ彛杩斿洖鐨勯鐩紙妯℃嫙锛夛細鍥哄畾浜旈」閫夐」 => 琛ㄤ竴锛屽叾浣� => 琛ㄤ簩
+const RAW_QUESTIONS = [
+  ...SATISFACTION_BASE.map(([title, counts]) => ({
+    title,
+    type: 'single',
+    options: FIXED_OPTIONS.map((opt, i) => ({ option: opt, count: counts[i] })),
+  })),
+  ...OTHER_BASE.map((q) => ({
+    title: q.title,
+    type: q.type,
+    total: q.total,
+    options: q.options.map(([option, count]) => ({ option, count })),
+  })),
+];
+
+// 纭畾鎬т吉闅忔満锛堜究浜庣瓫閫夋潯浠跺浐瀹氭椂缁撴灉绋冲畾锛�
+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: "DataOverview",
+  name: 'DataOverview',
   data() {
     return {
-      serviceSatisfaction: [
-        { category: "璇婄枟杩囩▼涓紝鍖诲姟浜哄憳鏄惁娉ㄦ剰淇濇姢鎮ㄧ殑闅愮锛�", veryGoodCount: 3219, veryGoodRate: "67.84%", goodCount: 1351, goodRate: "28.47%", normalCount: 142, normalRate: "2.99%", badCount: 33, badRate: "0.70%", naCount: 0, naRate: "0.00%", score: "87.82" },
-        { category: "鎮ㄥ鍖婚櫌鏈嶅姟璁炬柦鐨勬�讳綋鍗拌薄濡備綍锛�", veryGoodCount: 3110, veryGoodRate: "65.54%", goodCount: 1369, goodRate: "28.85%", normalCount: 217, normalRate: "4.57%", badCount: 49, badRate: "1.03%", naCount: 0, naRate: "0.00%", score: "86.30" },
-        { category: "鍖婚櫌鐨勫帟鎵�鏄惁娓呮磥鏃犲紓鍛筹紵", veryGoodCount: 2973, veryGoodRate: "62.66%", goodCount: 1504, goodRate: "31.70%", normalCount: 235, normalRate: "4.95%", badCount: 33, badRate: "0.70%", naCount: 0, naRate: "0.00%", score: "85.44" },
-        { category: "鍖婚櫌鍐呯殑璺爣鍜屾寚绀烘槸鍚︽槑纭紵", veryGoodCount: 3234, veryGoodRate: "68.16%", goodCount: 1289, goodRate: "27.17%", normalCount: 196, normalRate: "4.13%", badCount: 26, badRate: "0.55%", naCount: 0, naRate: "0.00%", score: "87.64" },
-        { category: "鍖婚櫌鐨勭┖闂村竷灞�鏄惁渚垮埄锛�", veryGoodCount: 3037, veryGoodRate: "64.00%", goodCount: 1460, goodRate: "30.77%", normalCount: 209, normalRate: "4.40%", badCount: 39, badRate: "0.82%", naCount: 0, naRate: "0.00%", score: "85.99" },
-        { category: "鐪嬬梾鏈熼棿锛屾偍鐨勬姳鎬ㄥ拰涓嶆弧鑳藉惁鍙婃椂寰楀埌鍥炲簲锛�", veryGoodCount: 3019, veryGoodRate: "63.62%", goodCount: 1317, goodRate: "27.76%", normalCount: 294, normalRate: "6.20%", badCount: 115, badRate: "2.42%", naCount: 0, naRate: "0.00%", score: "84.19" },
-        { category: "鍖荤敓鏄惁鐢ㄦ偍鍚緱鎳傜殑鏂瑰紡瑙i噴闂锛�", veryGoodCount: 3402, veryGoodRate: "71.70%", goodCount: 967, goodRate: "20.38%", normalCount: 254, normalRate: "5.35%", badCount: 122, badRate: "2.57%", naCount: 0, naRate: "0.00%", score: "87.07" },
-        { category: "鍖荤敓瀵规偍鏄惁灏婇噸锛�", veryGoodCount: 3495, veryGoodRate: "73.66%", goodCount: 897, goodRate: "18.90%", normalCount: 249, normalRate: "5.25%", badCount: 104, badRate: "2.19%", naCount: 0, naRate: "0.00%", score: "88.01" },
-        { category: "鍖荤敓鏄惁浠旂粏鍊惧惉鎮ㄨ璇濓紵", veryGoodCount: 3422, veryGoodRate: "72.12%", goodCount: 937, goodRate: "19.75%", normalCount: 251, normalRate: "5.29%", badCount: 135, badRate: "2.85%", naCount: 0, naRate: "0.00%", score: "87.05" },
-        { category: "瀵规寕鍙锋湇鍔′汉鍛樻湇鍔℃�佸害鏄惁婊℃剰", veryGoodCount: 3458, veryGoodRate: "72.88%", goodCount: 1205, goodRate: "25.40%", normalCount: 52, normalRate: "1.10%", badCount: 30, badRate: "0.63%", naCount: 0, naRate: "0.00%", score: "90.17" },
-        { category: "鎮ㄥ棰勭害鎸傚彿鏄惁婊℃剰锛�", veryGoodCount: 3527, veryGoodRate: "74.33%", goodCount: 1088, goodRate: "22.93%", normalCount: 72, normalRate: "1.52%", badCount: 58, badRate: "1.22%", naCount: 0, naRate: "0.00%", score: "90.12" },
-        { category: "鎶ゅ+瀵规偍鏄惁灏婇噸锛�", veryGoodCount: 3420, veryGoodRate: "72.08%", goodCount: 1184, goodRate: "24.95%", normalCount: 113, normalRate: "2.38%", badCount: 28, badRate: "0.59%", naCount: 0, naRate: "0.00%", score: "89.50" },
-        { category: "鎶ゅ+鏄惁鐢ㄦ偍鍚緱鎳傜殑鏂瑰紡瑙i噴闂锛�", veryGoodCount: 3346, veryGoodRate: "70.52%", goodCount: 1250, goodRate: "26.34%", normalCount: 122, normalRate: "2.57%", badCount: 27, badRate: "0.57%", naCount: 0, naRate: "0.00%", score: "88.94" },
-        { category: "鎶ゅ+鏄惁浠旂粏鍊惧惉鎮ㄨ璇濓紵", veryGoodCount: 3378, veryGoodRate: "71.19%", goodCount: 1205, goodRate: "25.40%", normalCount: 134, normalRate: "2.82%", badCount: 28, badRate: "0.59%", naCount: 0, naRate: "0.00%", score: "89.06" },
-        { category: "鎬昏", veryGoodCount: 54863, veryGoodRate: "69.02%", goodCount: 20800, goodRate: "26.17%", normalCount: 2920, normalRate: "3.67%", badCount: 908, badRate: "1.14%", naCount: 0, naRate: "0.00%", score: "87.66" },
-      ],
-      questionStats: [
-        {
-          title: "绗�1棰橈細鎮ㄥ鎴戦櫌宸ヤ綔浜哄憳鐨勬湇鍔¤川閲忔弧鎰忎笉婊℃剰锛熴�愬垎鍊煎崟閫夐銆�- 缁煎悎寰楀垎",
-          options: [
-            { option: "闈炲父婊℃剰", count: 1084, rate: "75.66%" },
-            { option: "婊℃剰", count: 1959, rate: "20.92%" },
-            { option: "涓�鑸�", count: 167, rate: "1.78%" },
-            { option: "涓嶆弧鎰�", count: 153, rate: "1.63%" },
-          ],
-          total: 3363
-        },
-        {
-          title: "绗�2棰橈細鎮ㄥ鍝儴鍒嗘湇鍔′笉婊℃剰锛熴�愬閫夐銆�",
-          options: [
-            { option: "鍖荤敓璇婃柇鍜岃瘖鐤�", count: 131, rate: "32.11%" },
-            { option: "鎶ょ悊鏈嶅姟", count: 47, rate: "11.52%" },
-            { option: "鍏朵粬鍖诲姟浜哄憳鎶�鏈按骞�", count: 30, rate: "7.33%" },
-            { option: "宸ヤ汉", count: 72, rate: "17.65%" },
-          ],
-          total: 408
-        },
-        {
-          title: "绗�3棰橈細鍖荤敓璇婃柇鍜屾不鐤椾腑鍝簺鏈嶅姟璐ㄩ噺涓嶆弧鎰忥紵銆愬閫夐銆�",
-          options: [
-            { option: "鍖荤敓璇婃柇閿欒", count: 23, rate: "15.44%" },
-            { option: "瀵规不鐤楁晥鏋滀笉婊℃剰", count: 72, rate: "48.32%" },
-            { option: "鍏朵粬", count: 54, rate: "36.24%" },
-          ],
-          total: 131
-        },
-      ],
-      deptComparison: [
-        { dept: "鍐呭垎娉岀", q1VeryGood: 103, q1Good: 34, q1Normal: 0, q1Bad: 0, q1Score: "377.00", q2VeryGood: 101, q2Good: 35, q2Normal: 1, q2Bad: 0, q2Score: "374.00", totalScore: "75.25" },
-        { dept: "鐨偆绉�", q1VeryGood: 112, q1Good: 25, q1Normal: 2, q1Bad: 3, q1Score: "388.00", q2VeryGood: 111, q2Good: 27, q2Normal: 3, q2Bad: 1, q2Score: "390.00", totalScore: "76.12" },
-        { dept: "鑲跨槫鍐呯", q1VeryGood: 24, q1Good: 12, q1Normal: 2, q1Bad: 0, q1Score: "98.00", q2VeryGood: 25, q2Good: 12, q2Normal: 1, q2Bad: 0, q2Score: "100.00", totalScore: "70.22" },
-        { dept: "娑堝寲鍐呯", q1VeryGood: 180, q1Good: 52, q1Normal: 1, q1Bad: 0, q1Score: "645.00", q2VeryGood: 168, q2Good: 62, q2Normal: 3, q2Bad: 0, q2Score: "631.00", totalScore: "73.56" },
-        { dept: "鑲惧唴绉�", q1VeryGood: 21, q1Good: 4, q1Normal: 0, q1Bad: 0, q1Score: "71.00", q2VeryGood: 19, q2Good: 6, q2Normal: 0, q2Bad: 0, q2Score: "65.00", totalScore: "68.50" },
-      ],
+      // 椤堕儴鍏ㄥ眬绛涢��
+      queryParams: {
+        dateRange: [],
+        deptWard: '',
+      },
+      deptOptions: DEPTS,
+      wardOptions: WARDS,
+
+      // 琛ㄦ牸涓夛細缁熻缁村害 + 鐙珛绉戝/鐥呭尯鏉′欢锛�'inherit' = 娌跨敤椤堕儴锛�
+      statDimension: 'dept',
+      statDeptWard: 'inherit',
+
+      // 缁熶竴鎺ュ彛杩斿洖鐨勬暟鎹紙妯℃嫙锛�
+      questionList: [],
+      deptComparison: [],
+      wardComparison: [],
     };
   },
+  computed: {
+    // 鍥哄畾浜旈」閫夐」鐨勯鐩� => 琛ㄤ竴
+    satisfactionQuestions() {
+      return this.questionList.filter((q) => this.isSatisfactionQuestion(q));
+    },
+    // 鍏朵綑棰樼洰 => 琛ㄤ簩
+    otherQuestions() {
+      return this.questionList.filter((q) => !this.isSatisfactionQuestion(q));
+    },
+    // 琛ㄤ竴锛氭弧鎰忓害缁熻锛堝惈鎬昏琛岋級
+    serviceSatisfaction() {
+      const rows = this.satisfactionQuestions.map((q) => {
+        const m = {};
+        q.options.forEach((o) => (m[o.option] = o.count));
+        const veryGood = m['闈炲父婊℃剰'] || 0;
+        const good = m['婊℃剰'] || 0;
+        const normal = m['涓�鑸�'] || 0;
+        const bad = m['涓嶆弧鎰�'] || 0;
+        const na = m['涓嶉�傜敤'] || 0;
+        const total = veryGood + good + normal + bad + na;
+        return {
+          category: q.title,
+          veryGoodCount: veryGood,
+          veryGoodRate: this.rateOf(veryGood, total),
+          goodCount: good,
+          goodRate: this.rateOf(good, total),
+          normalCount: normal,
+          normalRate: this.rateOf(normal, total),
+          badCount: bad,
+          badRate: this.rateOf(bad, total),
+          naCount: na,
+          naRate: this.rateOf(na, total),
+          score: this.scoreOf([veryGood, good, normal, bad, na]),
+        };
+      });
+
+      // 鎬昏琛�
+      const sum = { veryGood: 0, good: 0, normal: 0, bad: 0, na: 0 };
+      rows.forEach((r) => {
+        sum.veryGood += r.veryGoodCount;
+        sum.good += r.goodCount;
+        sum.normal += r.normalCount;
+        sum.bad += r.badCount;
+        sum.na += r.naCount;
+      });
+      const total = sum.veryGood + sum.good + sum.normal + sum.bad + sum.na;
+      rows.push({
+        category: '鎬昏',
+        veryGoodCount: sum.veryGood,
+        veryGoodRate: this.rateOf(sum.veryGood, total),
+        goodCount: sum.good,
+        goodRate: this.rateOf(sum.good, total),
+        normalCount: sum.normal,
+        normalRate: this.rateOf(sum.normal, total),
+        badCount: sum.bad,
+        badRate: this.rateOf(sum.bad, total),
+        naCount: sum.na,
+        naRate: this.rateOf(sum.na, total),
+        score: this.scoreOf([sum.veryGood, sum.good, sum.normal, sum.bad, sum.na]),
+      });
+      return rows;
+    },
+    // 琛ㄤ簩锛氬崟閫�/澶氶�夌粺璁�
+    questionStats() {
+      return this.otherQuestions.map((q) => {
+        const total = this.questionTotal(q);
+        return {
+          title: q.title,
+          total,
+          options: q.options.map((o) => ({
+            option: o.option,
+            count: o.count,
+            rate: this.rateOf(o.count, total),
+          })),
+        };
+      });
+    },
+    // 琛ㄤ笁锛氭寜缁村害 + 鐙珛鏉′欢杩囨护鍚庣殑琛�
+    table3Rows() {
+      const list = this.statDimension === 'dept' ? this.deptComparison : this.wardComparison;
+      const f = this.statFilter();
+      if (!f) return list;
+      const [type, name] = f.split(':');
+      if (type !== this.statDimension) return list;
+      return list.filter((r) => r.name === name);
+    },
+  },
+  mounted() {
+    if (!this.queryParams.dateRange || !this.queryParams.dateRange.length) {
+      this.queryParams.dateRange = this.defaultRange();
+    }
+    this.fetchData();
+  },
   methods: {
+    // 琛ㄤ笁鐙珛鏉′欢锛�'inherit' 鏃舵部鐢ㄩ《閮ㄦ潯浠�
+    statFilter() {
+      const v = this.statDeptWard === 'inherit' ? this.queryParams.deptWard : this.statDeptWard;
+      return v || '';
+    },
+    onStatDimensionChange() {
+      // 鍒囨崲缁村害鍚庯紝鑻ュ綋鍓嶅凡閫夋潯浠朵笉灞炰簬鏂扮淮搴︼紝鍒欐仮澶嶁�滄部鐢ㄩ《閮ㄢ��
+      if (this.statDeptWard !== 'inherit' && !this.statDeptWard.startsWith(this.statDimension + ':')) {
+        this.statDeptWard = 'inherit';
+      }
+    },
+    handleReset() {
+      this.queryParams.dateRange = this.defaultRange();
+      this.queryParams.deptWard = '';
+      this.fetchData();
+    },
+    // 鍒ゆ柇棰樼洰閫夐」鏄惁涓哄浐瀹氭弧鎰忓害浜旈」
+    isSatisfactionQuestion(q) {
+      if (q.options.length !== FIXED_OPTIONS.length) return false;
+      const set = q.options.map((o) => o.option);
+      return FIXED_OPTIONS.every((o) => set.includes(o));
+    },
+    questionTotal(q) {
+      if (q.total != null) return q.total;
+      return q.options.reduce((s, o) => s + o.count, 0);
+    },
+    rateOf(count, total) {
+      if (!total) return '0.00%';
+      return ((count / total) * 100).toFixed(2) + '%';
+    },
+    // 缁煎悎寰楀垎锛氶潪甯告弧鎰�5鍒嗐�佹弧鎰�4鍒嗐�佷竴鑸�3鍒嗐�佷笉婊℃剰2鍒嗭紝鎹㈢畻鐧惧垎鍒讹紙涓嶉�傜敤涓嶈鍏ワ級鈥斺�斿崰浣嶅叕寮忥紝鎺ュ彛灏辩华鍚庡彲鎸夊悗绔彛寰勬浛鎹�
+    scoreOf(counts) {
+      const [a, b, c, d] = counts;
+      const total = a + b + c + d;
+      if (!total) return '0.00';
+      return (((a * 5 + b * 4 + c * 3 + d * 2) / total) * 20).toFixed(2);
+    },
+    defaultRange() {
+      const end = new Date();
+      const start = new Date();
+      start.setDate(start.getDate() - 29);
+      const f = (d) => {
+        const y = d.getFullYear();
+        const m = String(d.getMonth() + 1).padStart(2, '0');
+        const day = String(d.getDate()).padStart(2, '0');
+        return `${y}-${m}-${day}`;
+      };
+      return [f(start), f(end)];
+    },
+    dateRangeDays(range) {
+      if (Array.isArray(range) && range.length === 2 && range[0] && range[1]) {
+        const ms = new Date(range[1]).getTime() - new Date(range[0]).getTime();
+        return Math.max(1, Math.round(ms / 86400000));
+      }
+      return 30;
+    },
+    // 鎷夊彇鏁版嵁锛堝綋鍓嶄负妯℃嫙鏁版嵁锛屾帴鍙e氨缁悗鏇挎崲涓虹湡瀹炶姹傦級
+    fetchData() {
+      const data = this.mockFetch(this.queryParams);
+      this.questionList = data.questionList;
+      this.deptComparison = data.deptComparison;
+      this.wardComparison = data.wardComparison;
+    },
+    mockFetch(params) {
+      const seed = hashString(JSON.stringify(params));
+      const rnd = mulberry32(seed);
+      const days = this.dateRangeDays(params.dateRange);
+      const timeFactor = Math.max(0.05, days / 30);
+      const scopeFactor = params.deptWard ? 0.18 : 1;
+      const jitter = () => 0.9 + rnd() * 0.2;
+
+      const questionList = RAW_QUESTIONS.map((q) => {
+        const options = q.options.map((o) => ({
+          option: o.option,
+          count: Math.round(o.count * timeFactor * scopeFactor * jitter()),
+        }));
+        const item = { title: q.title, type: q.type, options };
+        if (q.type === 'multiple' && q.total != null) {
+          item.total = Math.round(q.total * timeFactor * scopeFactor);
+        }
+        return item;
+      });
+
+      const deptComparison = DEPTS.map((d) => this.genCompareRow(d, rnd));
+      const wardComparison = WARDS.map((w) => this.genCompareRow(w, rnd));
+
+      return { questionList, deptComparison, wardComparison };
+    },
+    genCompareRow(name, rnd) {
+      const r = (lo, hi) => Math.round(lo + rnd() * (hi - lo));
+      const q1VeryGood = r(20, 180);
+      const q1Good = r(5, 55);
+      const q1Normal = r(0, 4);
+      const q1Bad = r(0, 3);
+      const q2VeryGood = r(18, 175);
+      const q2Good = r(5, 55);
+      const q2Normal = r(0, 4);
+      const q2Bad = r(0, 3);
+      return {
+        name,
+        q1VeryGood, q1Good, q1Normal, q1Bad,
+        q1Score: this.scoreOf([q1VeryGood, q1Good, q1Normal, q1Bad]),
+        q2VeryGood, q2Good, q2Normal, q2Bad,
+        q2Score: this.scoreOf([q2VeryGood, q2Good, q2Normal, q2Bad]),
+        totalScore: this.scoreOf([
+          q1VeryGood + q2VeryGood,
+          q1Good + q2Good,
+          q1Normal + q2Normal,
+          q1Bad + q2Bad,
+        ]),
+      };
+    },
     exportTable(tableId) {
       const tableNames = { table1: '鍚勬湇鍔¢」鐩弧鎰忓害缁熻', table2: '鍗曢�夐澶氶�夐缁熻', table3: '鍚勭瀹ゆ弧鎰忓害璇勫垎瀵规瘮' };
       const name = tableNames[tableId] || '缁熻鏁版嵁';
@@ -152,9 +461,9 @@
         // 浠庤〃鏍兼彁鍙栨暟鎹瀯寤哄甫鏍峰紡鐨� Excel
         const wb = XLSX.utils.book_new();
         const data = [];
-        table.querySelectorAll('tr').forEach(tr => {
+        table.querySelectorAll('tr').forEach((tr) => {
           const row = [];
-          tr.querySelectorAll('th,td').forEach(td => {
+          tr.querySelectorAll('th,td').forEach((td) => {
             row.push((td.innerText || '').trim());
           });
           if (row.length) data.push(row);
@@ -170,14 +479,16 @@
       } catch (e) {
         // fallback: CSV
         let csv = '锘�';
-        table.querySelectorAll('tr').forEach(tr => {
+        table.querySelectorAll('tr').forEach((tr) => {
           const row = [];
-          tr.querySelectorAll('th,td').forEach(td => row.push('"' + (td.innerText || '').replace(/"/g, '""') + '"'));
+          tr.querySelectorAll('th,td').forEach((td) => row.push('"' + (td.innerText || '').replace(/"/g, '""') + '"'));
           csv += row.join(',') + '\n';
         });
         const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
         const link = document.createElement('a');
-        link.href = URL.createObjectURL(blob); link.download = name + '.csv'; link.click();
+        link.href = URL.createObjectURL(blob);
+        link.download = name + '.csv';
+        link.click();
       }
     },
   },
@@ -186,8 +497,10 @@
 
 <style lang="scss" scoped>
 .data-overview { .overview-card { margin-bottom: 20px; } }
+.filter-card { .filter-form { margin-bottom: -10px; } .filter-select { width: 200px; } }
 .card-header { display: flex; justify-content: space-between; align-items: center; }
 .card-title { font-size: 16px; font-weight: 600; color: #303133; }
+.header-right { display: flex; align-items: center; .stat-select { width: 180px; margin: 0 12px; } }
 .question-block { margin-bottom: 24px;
   .q-title { margin: 0 0 12px 0; font-size: 14px; color: #606266; }
   .q-footer { margin-top: 8px; font-size: 13px; color: #909399; text-align: right; }

--
Gitblit v1.9.3