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