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/ChartsPanel.vue |  443 ++++++++++++++++++++++++++++++++++++++++++++-----------
 1 files changed, 354 insertions(+), 89 deletions(-)

diff --git a/src/views/Satisfaction/sfstatistics/components/ChartsPanel.vue b/src/views/Satisfaction/sfstatistics/components/ChartsPanel.vue
index c8a13c6..e0ce533 100644
--- a/src/views/Satisfaction/sfstatistics/components/ChartsPanel.vue
+++ b/src/views/Satisfaction/sfstatistics/components/ChartsPanel.vue
@@ -1,7 +1,28 @@
 <template>
   <div class="charts-panel" ref="pdfTarget">
-    <!-- PDF瀵煎嚭鎸夐挳 -->
+    <!-- 椤堕儴绛涢�夛細鏃堕棿鍖洪棿 + 鍖洪棿绫诲瀷 + PDF瀵煎嚭 -->
     <div class="export-bar">
+      <div class="filter-bar">
+        <span class="filter-label">鏃堕棿鑼冨洿锛�</span>
+        <el-date-picker
+          v-model="queryParams.dateRange"
+          type="daterange"
+          range-separator="鑷�"
+          start-placeholder="寮�濮嬫棩鏈�"
+          end-placeholder="缁撴潫鏃ユ湡"
+          value-format="yyyy-MM-dd"
+          size="small"
+          class="filter-date"
+        />
+        <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="month">鎸夋湀</el-radio-button>
+          <el-radio-button label="year">鎸夊勾</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>
+      </div>
       <el-button type="danger" size="small" icon="el-icon-download" @click="exportPDF">瀵煎嚭PDF</el-button>
     </div>
 
@@ -11,7 +32,7 @@
         <el-card shadow="never" class="chart-card">
           <div slot="header" class="card-header">
             <span>鏃堕棿缁村害 路 闂ㄨ瘖鎮h�呮弧鎰忓害瓒嬪娍</span>
-            <el-tag type="success" size="small">鐜瘮 +0.97</el-tag>
+            <el-tag type="success" size="small">鐜瘮 {{ trendMoM }}</el-tag>
           </div>
           <div ref="chart1" class="chart-box"></div>
         </el-card>
@@ -21,7 +42,7 @@
       <el-col :span="12">
         <el-card shadow="never" class="chart-card">
           <div slot="header" class="card-header">
-            <span>鍒嗙被缁村害 路 鍚勬寚鏍囧勾搴﹀姣�</span>
+            <span>鍒嗙被缁村害 路 鍚勬寚鏍噞{ intervalLabel }}瀵规瘮</span>
           </div>
           <div ref="chart2" class="chart-box"></div>
         </el-card>
@@ -29,18 +50,31 @@
     </el-row>
 
     <el-row :gutter="20" style="margin-top: 20px">
-      <!-- 3. 鍗曢閫夐」 - 鏁版嵁琛ㄦ牸 -->
+      <!-- 3. 鍗曢閫夐」 - 鏁版嵁琛ㄦ牸锛堝彲閫夋嫨棰樼洰銆佺瀹�/鐥呭尯锛� -->
       <el-col :span="12">
         <el-card shadow="never" class="chart-card">
           <div slot="header" class="card-header">
-            <span>鍗曢 路 绗�22棰橈細鎶辨�ㄥ拰涓嶆弧鑳藉惁鍙婃椂寰楀埌鍥炲簲锛�</span>
+            <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>
+              <el-select v-model="singleDeptWards" multiple collapse-tags size="small" class="sel" clearable filterable placeholder="鍏ㄩ儴绉戝/鐥呭尯" @change="refreshAll">
+                <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>
+            </div>
           </div>
           <el-table :data="singleQuestionData" border size="small">
             <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>26,233</b>銆�缁煎悎婊℃剰搴︼細<b>83.20%</b></div>
+          <div class="table-footer">鏈鏈夋晥濉啓浜烘锛�<b>{{ singleQuestionMeta.total }}</b>銆�缁煎悎婊℃剰搴︼細<b>{{ singleQuestionMeta.score }}</b></div>
         </el-card>
       </el-col>
 
@@ -48,7 +82,7 @@
       <el-col :span="12">
         <el-card shadow="never" class="chart-card">
           <div slot="header" class="card-header">
-            <span>闄㈠尯缁村害 路 鍚勯櫌鍖哄勾搴﹀姣�</span>
+            <span>闄㈠尯缁村害 路 鍚勯櫌鍖簕{ intervalLabel }}瀵规瘮</span>
           </div>
           <div ref="chart4" class="chart-box"></div>
         </el-card>
@@ -56,29 +90,47 @@
     </el-row>
 
     <el-row :gutter="20" style="margin-top: 20px">
-      <!-- 5. 绉戝缁村害 - 鎺掑悕琛ㄦ牸 -->
+      <!-- 5. 绉戝/鐥呭尯缁村害 - 鎺掑悕琛ㄦ牸 -->
       <el-col :span="12">
         <el-card shadow="never" class="chart-card">
           <div slot="header" class="card-header">
-            <span>绉戝缁村害 路 闂ㄨ瘖鎮h�呮弧鎰忓害寰楀垎鎺掑悕</span>
+            <span>{{ rankDimLabel }}缁村害 路 闂ㄨ瘖鎮h�呮弧鎰忓害寰楀垎鎺掑悕</span>
+            <div class="header-right">
+              <el-radio-group v-model="rankDimension" size="mini" @change="onRankDimensionChange">
+                <el-radio-button label="dept">鎸夌瀹�</el-radio-button>
+                <el-radio-button label="ward">鎸夌梾鍖�</el-radio-button>
+              </el-radio-group>
+              <el-select v-model="rankDeptWards" multiple collapse-tags size="small" class="sel" clearable filterable :placeholder="'鍏ㄩ儴' + rankDimLabel" @change="refreshAll">
+                <el-option v-for="name in rankOptions" :key="name" :label="name" :value="name" />
+              </el-select>
+            </div>
           </div>
-          <el-table :data="deptRankData" border size="small">
+          <el-table :data="rankRows" border size="small" max-height="400">
             <el-table-column type="index" label="鎺掑悕" width="60" align="center" />
-            <el-table-column prop="dept" label="绉戝" />
+            <el-table-column prop="name" :label="rankDimLabel" />
             <el-table-column prop="score" label="缁煎悎寰楀垎" width="120" align="center" />
           </el-table>
         </el-card>
       </el-col>
 
-      <!-- 6. 鍗曢绉戝 - 浜ゅ弶鍒嗘瀽琛ㄦ牸 -->
+      <!-- 6. 鍗曢脳绉戝/鐥呭尯 - 浜ゅ弶鎺掑悕琛ㄦ牸 -->
       <el-col :span="12">
         <el-card shadow="never" class="chart-card">
           <div slot="header" class="card-header">
-            <span>鍗曢脳绉戝 路 绗�22棰樺悇绉戝寰楀垎</span>
+            <span>鍗曢脳{{ crossDimLabel }} 路 寰楀垎鎺掑悕</span>
+            <div class="header-right">
+              <el-radio-group v-model="crossDimension" size="mini" @change="onCrossDimensionChange">
+                <el-radio-button label="dept">鎸夌瀹�</el-radio-button>
+                <el-radio-button label="ward">鎸夌梾鍖�</el-radio-button>
+              </el-radio-group>
+              <el-select v-model="crossDeptWards" multiple collapse-tags size="small" class="sel" clearable filterable :placeholder="'鍏ㄩ儴' + crossDimLabel" @change="refreshAll">
+                <el-option v-for="name in crossOptions" :key="name" :label="name" :value="name" />
+              </el-select>
+            </div>
           </div>
-          <el-table :data="crossDeptData" border size="small" max-height="400">
+          <el-table :data="crossRows" border size="small" max-height="400">
             <el-table-column type="index" label="鎺掑悕" width="60" align="center" />
-            <el-table-column prop="dept" label="绉戝" />
+            <el-table-column prop="name" :label="crossDimLabel" />
             <el-table-column prop="score" label="缁煎悎寰楀垎" width="120" align="center" />
           </el-table>
         </el-card>
@@ -90,113 +142,336 @@
 <script>
 import * as echarts from 'echarts';
 
+// 鍗曢閫夐」
+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",
+  name: 'ChartsPanel',
   data() {
     return {
-      singleQuestionData: [
-        { option: "闈炲父婊℃剰", count: 16513, rate: "62.95%" },
-        { option: "婊℃剰", count: 7088, rate: "27.02%" },
-        { option: "涓�鑸�", count: 1760, rate: "6.71%" },
-        { option: "涓嶆弧鎰�", count: 872, rate: "3.32%" },
-      ],
-      deptRankData: [
-        { dept: "淇濆仴绉�", score: "81.54" }, { dept: "浠嬪叆娌荤枟绉�", score: "80.72" },
-        { dept: "涓撶鎶ゅ+闂ㄨ瘖", score: "79.27" }, { dept: "搴峰鍖诲绉�", score: "78.65" },
-        { dept: "鐨偆绉�", score: "78.12" }, { dept: "鍐呭垎娉岀", score: "77.89" },
-        { dept: "娑堝寲鍐呯", score: "77.34" }, { dept: "鑲惧唴绉�", score: "76.50" },
-        { dept: "鍛煎惛鍐呯", score: "75.88" }, { dept: "蹇冭绠″唴绉�", score: "75.21" },
-      ],
-      crossDeptData: [
-        { dept: "淇濆仴绉�", score: "81.24" }, { dept: "浠嬪叆娌荤枟绉�", score: "81.22" },
-        { dept: "涓撶鎶ゅ+闂ㄨ瘖", score: "80.15" }, { dept: "搴峰鍖诲绉�", score: "79.88" },
-        { dept: "鐨偆绉�", score: "79.01" }, { dept: "鍐呭垎娉岀", score: "78.45" },
-        { dept: "娑堝寲鍐呯", score: "77.92" }, { dept: "鑲惧唴绉�", score: "77.13" },
-        { dept: "鍛煎惛鍐呯", score: "76.80" }, { dept: "蹇冭绠″唴绉�", score: "76.05" },
-        { dept: "绁炵粡鍐呯", score: "75.62" }, { dept: "琛�娑茬", score: "75.01" },
-        { dept: "椋庢箍鍏嶇柅绉�", score: "74.38" }, { dept: "鑲跨槫鍐呯", score: "73.85" },
-        { dept: "鎰熸煋绉�", score: "73.12" }, { dept: "鑰佸勾绉�", score: "72.56" },
-        { dept: "鏅绉�", score: "71.90" }, { dept: "楠ㄧ", score: "71.23" },
-        { dept: "娉屽翱澶栫", score: "70.55" }, { dept: "濡囦骇绉�", score: "69.88" },
-      ],
+      queryParams: {
+        dateRange: [],
+        interval: 'month',
+      },
+      deptOptions: DEPTS,
+      wardOptions: WARDS,
+      questionOptions: QUESTIONS,
+
+      // 鍗曢绛涢��
+      singleQuestionId: 22,
+      singleDeptWards: [],
+
+      // 鎺掑悕缁村害绛涢��
+      rankDimension: 'dept',
+      rankDeptWards: [],
+      crossDimension: 'dept',
+      crossDeptWards: [],
+
+      // 鍥捐〃鏁版嵁
+      trend: { buckets: [], data: [], seriesName: '闂ㄨ瘖鎮h�呮弧鎰忓害' },
+      categoryCompare: { categories: [], seriesNames: [], series1: [], series2: [] },
+      hospitalCompare: { hospitals: [], seriesNames: [], series1: [], series2: [] },
+
+      // 琛ㄦ牸鏁版嵁
+      singleQuestionData: [],
+      singleQuestionMeta: { total: '0', score: '0.00%' },
+      deptRankData: [],
+      wardRankData: [],
+      crossDeptData: [],
+      crossWardData: [],
     };
   },
-  mounted() { this.$nextTick(() => { this.initCharts(); }); },
+  computed: {
+    intervalLabel() {
+      return { day: '鎸夊ぉ', month: '鎸夋湀', year: '鎸夊勾' }[this.queryParams.interval] || '鎸夋湀';
+    },
+    trendMoM() {
+      const d = this.trend.data;
+      if (d.length < 2) return '0.00';
+      const diff = d[d.length - 1] - d[d.length - 2];
+      return (diff >= 0 ? '+' : '') + diff.toFixed(2);
+    },
+    currentSingleQuestion() {
+      return this.questionOptions.find((q) => q.id === this.singleQuestionId) || {};
+    },
+    rankDimLabel() {
+      return this.rankDimension === 'dept' ? '绉戝' : '鐥呭尯';
+    },
+    crossDimLabel() {
+      return this.crossDimension === 'dept' ? '绉戝' : '鐥呭尯';
+    },
+    rankOptions() {
+      return this.rankDimension === 'dept' ? this.deptOptions : this.wardOptions;
+    },
+    crossOptions() {
+      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));
+    },
+    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));
+    },
+  },
+  mounted() {
+    this.$nextTick(() => {
+      if (!this.queryParams.dateRange || !this.queryParams.dateRange.length) {
+        this.queryParams.dateRange = this.defaultRange();
+      }
+      this.initCharts();
+      this.refreshAll();
+    });
+  },
   methods: {
     initCharts() {
-      const quarters = ['绗竴瀛e害', '绗簩瀛e害', '绗笁瀛e害', '绗洓瀛e害'];
+      echarts.init(this.$refs.chart1);
+      echarts.init(this.$refs.chart2);
+      echarts.init(this.$refs.chart4);
+      window.addEventListener('resize', this.handleResize);
+    },
+    handleResize() {
+      ['chart1', 'chart2', 'chart4'].forEach((ref) => {
+        const c = echarts.getInstanceByDom(this.$refs[ref]);
+        if (c) c.resize();
+      });
+    },
+    handleReset() {
+      this.queryParams.dateRange = this.defaultRange();
+      this.queryParams.interval = 'month';
+      this.refreshAll();
+    },
+    onRankDimensionChange() {
+      this.rankDeptWards = [];
+      this.refreshAll();
+    },
+    onCrossDimensionChange() {
+      this.crossDeptWards = [];
+      this.refreshAll();
+    },
+    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)];
+    },
+    // 鍖洪棿绫诲瀷 => 鏃堕棿妗� + 鏌辩姸鍥惧姣斿簭鍒楀悕
+    intervalConfig() {
+      const buckets = this.genBuckets(this.queryParams.interval);
+      if (this.queryParams.interval === 'day') return { buckets, seriesNames: ['鏄ㄦ棩', '浠婃棩'] };
+      if (this.queryParams.interval === 'year') return { buckets, seriesNames: ['鍘诲勾', '浠婂勾'] };
+      return { buckets, seriesNames: ['涓婃湀', '鏈湀'] };
+    },
+    genBuckets(type) {
+      const end = this.queryParams.dateRange && this.queryParams.dateRange[1]
+        ? new Date(this.queryParams.dateRange[1])
+        : new Date();
+      const buckets = [];
+      if (type === 'day') {
+        for (let i = 14; i >= 0; i--) {
+          const d = new Date(end);
+          d.setDate(end.getDate() - i);
+          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 {
+        for (let i = 5; i >= 0; i--) {
+          const d = new Date(end.getFullYear(), end.getMonth() - i, 1);
+          buckets.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`);
+        }
+      }
+      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 }))));
+
+      this.renderCharts();
+    },
+    genChartData(rnd) {
+      const jitter = (lo, hi) => lo + rnd() * (hi - lo);
+      const cfg = this.intervalConfig();
+      const buckets = cfg.buckets;
+
+      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 = ['闄㈠尯涓�', '闄㈠尯浜�', '闄㈠尯涓�'];
-      const baseGrid = { top: 20, right: 30, bottom: 30, left: 50 };
+      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),
+      };
+    },
+    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) + '%',
+      }));
+      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);
+      const c2 = echarts.getInstanceByDom(this.$refs.chart2);
+      const c4 = echarts.getInstanceByDom(this.$refs.chart4);
+      if (!c1 || !c2 || !c4) return;
+
+      const lineGrid = { top: 32, right: 30, bottom: 32, left: 85 };
+      const barGrid = { top: 48, right: 30, bottom: 60, left: 85 };
       const baseTooltip = { backgroundColor: 'rgba(255,255,255,0.95)', borderColor: '#e8e8e8', textStyle: { color: '#333', fontSize: 13 }, extraCssText: 'box-shadow: 0 2px 12px rgba(0,0,0,0.1); border-radius: 6px; padding: 10px 14px;' };
       const baseXAxis = { axisLine: { lineStyle: { color: '#e0e0e0' } }, axisTick: { show: false }, axisLabel: { color: '#666', fontSize: 12 } };
       const baseYAxis = { splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } }, axisLine: { show: false }, axisTick: { show: false }, axisLabel: { color: '#999', fontSize: 11 } };
+      const areaGrad = new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+        { offset: 0, color: 'rgba(91,143,249,0.25)' },
+        { offset: 1, color: 'rgba(91,143,249,0.02)' },
+      ]);
 
       // 1. 鎶樼嚎鍥� - 鏃堕棿缁村害
-      const c1 = echarts.init(this.$refs.chart1);
       c1.setOption({
         color: ['#5B8FF9'],
-        grid: { ...baseGrid, top: 25 },
+        grid: lineGrid,
         tooltip: { ...baseTooltip, trigger: 'axis' },
-        xAxis: [{ ...baseXAxis, data: quarters, boundaryGap: false }],
-        yAxis: [{ ...baseYAxis, min: 86, max: 91, name: '婊℃剰搴﹀緱鍒�', nameTextStyle: { color: '#999', fontSize: 11 } }],
+        xAxis: [{ ...baseXAxis, data: this.trend.buckets, boundaryGap: false }],
+        yAxis: [{ ...baseYAxis, name: '婊℃剰搴﹀緱鍒�', nameTextStyle: { color: '#999', fontSize: 11 } }],
         series: [{
-          name: '闂ㄨ瘖鎮h�呮弧鎰忓害', type: 'line', smooth: true, symbol: 'circle', symbolSize: 8,
-          data: [88.20, 87.32, 88.50, 89.17],
+          name: this.trend.seriesName, type: 'line', smooth: true, symbol: 'circle', symbolSize: 8,
+          data: this.trend.data,
           lineStyle: { width: 3, shadowBlur: 8, shadowColor: 'rgba(91,143,249,0.3)' },
           itemStyle: { color: '#5B8FF9' },
-          areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: 'rgba(91,143,249,0.25)' }, { offset: 1, color: 'rgba(91,143,249,0.02)' }
-          ])},
+          areaStyle: { color: areaGrad },
         }],
       });
 
       // 2. 鍒嗙粍鏌辩姸鍥� - 鍒嗙被缁村害
-      const c2 = echarts.init(this.$refs.chart2);
       c2.setOption({
         color: ['#6DC8EC', '#5B8FF9'],
-        grid: baseGrid,
+        grid: barGrid,
         tooltip: { ...baseTooltip, trigger: 'axis', axisPointer: { type: 'shadow', shadowStyle: { color: 'rgba(0,0,0,0.03)' } } },
-        legend: { data: ['2023骞�', '2024骞�'], bottom: 0, textStyle: { color: '#666', fontSize: 12 } },
-        xAxis: [{ ...baseXAxis, data: categories, axisLabel: { ...baseXAxis.axisLabel, interval: 0 } }],
+        legend: { data: this.categoryCompare.seriesNames, bottom: 8, textStyle: { color: '#666', fontSize: 12 } },
+        xAxis: [{ ...baseXAxis, data: this.categoryCompare.categories, axisLabel: { ...baseXAxis.axisLabel, interval: 0 } }],
         yAxis: [{ ...baseYAxis, name: '寰楀垎', nameTextStyle: { color: '#999', fontSize: 11 } }],
         series: [
-          { name: '2023骞�', type: 'bar', barWidth: 22, barGap: '30%',
+          { name: this.categoryCompare.seriesNames[0], type: 'bar', barWidth: 22, 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: [86.34, 85.12, 83.45, 86.22, 88.55, 87.33] },
-          { name: '2024骞�', type: 'bar', barWidth: 22,
+            data: this.categoryCompare.series1 },
+          { name: this.categoryCompare.seriesNames[1], type: 'bar', barWidth: 22,
             itemStyle: { borderRadius: [6, 6, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#5B8FF9' }, { offset: 1, color: '#3D6FDB' }]) },
-            data: [88.06, 84.57, 82.88, 87.77, 88.12, 87.00] },
+            data: this.categoryCompare.series2 },
         ],
       });
 
       // 4. 闄㈠尯缁村害 - 鍒嗙粍鏌辩姸鍥�
-      const c4 = echarts.init(this.$refs.chart4);
       c4.setOption({
         color: ['#6DC8EC', '#5B8FF9'],
-        grid: baseGrid,
+        grid: barGrid,
         tooltip: { ...baseTooltip, trigger: 'axis', axisPointer: { type: 'shadow' } },
-        legend: { data: ['2023骞�', '2024骞�'], bottom: 0, textStyle: { color: '#666', fontSize: 12 } },
-        xAxis: [{ ...baseXAxis, data: hospitals }],
+        legend: { data: this.hospitalCompare.seriesNames, bottom: 8, textStyle: { color: '#666', fontSize: 12 } },
+        xAxis: [{ ...baseXAxis, data: this.hospitalCompare.hospitals }],
         yAxis: [{ ...baseYAxis, name: '婊℃剰搴﹀緱鍒�', nameTextStyle: { color: '#999', fontSize: 11 } }],
         series: [
-          { name: '2023骞�', type: 'bar', barWidth: 24, barGap: '30%',
+          { name: this.hospitalCompare.seriesNames[0], type: 'bar', barWidth: 24, 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: [86.20, 87.69, 85.55],
-            label: { show: true, position: 'top', color: '#666', fontSize: 12, fontWeight: 500, formatter: p => p.value.toFixed(2) } },
-          { name: '2024骞�', type: 'bar', barWidth: 24,
+            data: this.hospitalCompare.series1,
+            label: { show: true, position: 'top', color: '#666', fontSize: 12, fontWeight: 500, formatter: (p) => p.value.toFixed(2) } },
+          { name: this.hospitalCompare.seriesNames[1], type: 'bar', barWidth: 24,
             itemStyle: { borderRadius: [6, 6, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#5B8FF9' }, { offset: 1, color: '#3D6FDB' }]) },
-            data: [88.15, 88.14, 85.88],
-            label: { show: true, position: 'top', color: '#666', fontSize: 12, fontWeight: 500, formatter: p => p.value.toFixed(2) } },
+            data: this.hospitalCompare.series2,
+            label: { show: true, position: 'top', color: '#666', fontSize: 12, fontWeight: 500, formatter: (p) => p.value.toFixed(2) } },
         ],
       });
-
-      // 鍝嶅簲寮�
-      window.addEventListener('resize', () => { c1.resize(); c2.resize(); c4.resize(); });
     },
 
     async exportPDF() {
-      // 鍏堝皢 ECharts 瀹炰緥杞负鍥剧墖 dataURL
       const chartRefs = [
         { ref: this.$refs.chart1, id: 'chart1' },
         { ref: this.$refs.chart2, id: 'chart2' },
@@ -212,19 +487,7 @@
         }
       }
 
-      // 鍏嬮殕鍐呭骞舵浛鎹㈠浘琛ㄤ负鍥剧墖
       const printContent = this.$refs.pdfTarget.cloneNode(true);
-      for (const { ref, id } of chartRefs) {
-        if (chartImages[id] && ref) {
-          const boxes = printContent.querySelectorAll('.chart-box');
-          for (const box of boxes) {
-            if (box.innerHTML.includes(id) || boxes.length === chartRefs.length) {
-              box.innerHTML = `<img src="${chartImages[id]}" style="width:100%;height:auto;" />`;
-            }
-          }
-        }
-      }
-      // 鏇村彲闈犵殑鏂瑰紡锛氭寜浣嶇疆鍖归厤
       const chartBoxes = printContent.querySelectorAll('.chart-box');
       const imgKeys = Object.keys(chartImages);
       chartBoxes.forEach((box, i) => {
@@ -246,7 +509,7 @@
           th, td { border: 1px solid #d9d9d9; padding: 8px 12px; text-align: center; font-size: 12px; }
           th { background: #f0f2f5; font-weight: 600; }
           .table-footer { text-align: right; padding: 8px 0; font-size: 13px; color: #606266; }
-          .export-bar, .el-tag { display: none; }
+          .export-bar, .filter-bar, .el-tag, .el-select, .el-radio-group { display: none; }
           @media print { @page { size: A4; margin: 10mm; } }
         </style></head><body>
         ${printContent.innerHTML}
@@ -260,9 +523,11 @@
 </script>
 
 <style lang="scss" scoped>
-.export-bar { text-align: right; margin-bottom: 16px; }
+.export-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
+.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; }
 .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; }
 </style>

--
Gitblit v1.9.3