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/DataOverview.vue | 1133 +++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 files changed, 1,021 insertions(+), 112 deletions(-)
diff --git a/src/views/Satisfaction/sfstatistics/components/DataOverview.vue b/src/views/Satisfaction/sfstatistics/components/DataOverview.vue
index 18b5142..2cfa325 100644
--- a/src/views/Satisfaction/sfstatistics/components/DataOverview.vue
+++ b/src/views/Satisfaction/sfstatistics/components/DataOverview.vue
@@ -1,14 +1,130 @@
<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-radio-group
+ v-model="queryParams.statMode"
+ size="small"
+ @change="onStatModeChange"
+ >
+ <el-radio-button label="ward">鎸夌梾鍖虹粺璁�</el-radio-button>
+ <el-radio-button label="dept">鎸夌瀹ょ粺璁�</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="绉戝" v-if="queryParams.statMode === 'dept'">
+ <el-select
+ v-model="queryParams.deptCodes"
+ multiple
+ collapse-tags
+ clearable
+ filterable
+ placeholder="鍏ㄩ儴绉戝"
+ size="small"
+ class="filter-select"
+ >
+ <el-option
+ v-for="d in deptOptions"
+ :key="d.value"
+ :label="d.label"
+ :value="d.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐥呭尯" v-else>
+ <el-select
+ v-model="queryParams.wardCodes"
+ multiple
+ collapse-tags
+ clearable
+ filterable
+ placeholder="鍏ㄩ儴鐥呭尯"
+ size="small"
+ class="filter-select"
+ >
+ <el-option
+ v-for="w in wardOptions"
+ :key="w.value"
+ :label="w.label"
+ :value="w.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏈嶅姟绫诲瀷">
+ <el-select
+ v-model="queryParams.serviceTypeList"
+ multiple
+ collapse-tags
+ clearable
+ filterable
+ placeholder="鍏ㄩ儴绫诲瀷"
+ size="small"
+ class="filter-select"
+ >
+ <el-option
+ v-for="s in serviceTypeOptions"
+ :key="s.value"
+ :label="s.label"
+ :value="s.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button
+ type="primary"
+ size="small"
+ icon="el-icon-search"
+ :loading="loading"
+ @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>
- <el-button type="warning" size="small" icon="el-icon-download" @click="exportTable('table1')">瀵煎嚭Excel</el-button>
+ <el-button
+ type="warning"
+ size="small"
+ icon="el-icon-download"
+ @click="exportTable('table1')"
+ >瀵煎嚭Excel</el-button
+ >
</div>
- <el-table id="table1" :data="serviceSatisfaction" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
- <el-table-column prop="category" label="鍒嗙被/棰樼洰" min-width="220" fixed="left" />
- <el-table-column label="闈炲父婊℃剰" align="center">
+ <el-table
+ id="table1"
+ v-loading="loading"
+ element-loading-text="缁熻涓�..."
+ :data="serviceSatisfaction"
+ border
+ size="small"
+ :header-cell-style="{ background: '#f5f7fa', color: '#303133' }"
+ >
+ <el-table-column
+ prop="category"
+ label="鍒嗙被/棰樼洰"
+ min-width="220"
+ fixed="left"
+ />
+ <el-table-column label="寰堟弧鎰�" align="center">
<el-table-column prop="veryGoodCount" label="鏁伴噺" width="90" />
<el-table-column prop="veryGoodRate" label="姣斾緥" width="90" />
</el-table-column>
@@ -24,127 +140,861 @@
<el-table-column prop="badCount" label="鏁伴噺" width="90" />
<el-table-column prop="badRate" label="姣斾緥" width="90" />
</el-table-column>
- <el-table-column label="涓嶉�傜敤" align="center">
+ <el-table-column label="寰堜笉婊℃剰" align="center">
+ <el-table-column prop="veryBadCount" label="鏁伴噺" width="90" />
+ <el-table-column prop="veryBadRate" label="姣斾緥" width="90" />
+ </el-table-column>
+ <el-table-column label="鏈粡鍘�" align="center">
<el-table-column prop="naCount" label="鏁伴噺" width="90" />
<el-table-column prop="naRate" label="姣斾緥" width="90" />
</el-table-column>
- <el-table-column prop="score" label="缁煎悎寰楀垎" width="100" align="center" />
+ <el-table-column
+ prop="score"
+ label="缁煎悎寰楀垎"
+ width="100"
+ align="center"
+ fixed="right"
+ />
</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 class="header-right">
+ <el-input
+ v-model="questionSearch"
+ size="small"
+ clearable
+ prefix-icon="el-icon-search"
+ placeholder="鎸夐鐩ā绯婃煡璇�"
+ class="search-input"
+ />
+ <el-button
+ type="warning"
+ size="small"
+ icon="el-icon-download"
+ @click="exportTable('table2')"
+ >瀵煎嚭Excel</el-button
+ >
+ </div>
</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" v-loading="loading" element-loading-text="缁熻涓�...">
+ <div class="table2-scroll">
+ <div
+ class="question-block"
+ v-for="(q, qi) in filteredQuestionStats"
+ :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 v-if="!loading && !filteredQuestionStats.length" class="empty-tip">
+ {{ questionStats.length ? "鏈壘鍒板尮閰嶇殑棰樼洰" : "鏆傛棤鍗曢��/澶氶�夐缁熻鏁版嵁" }}
+ </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.value"
+ :label="d.label"
+ :value="'dept:' + d.value"
+ />
+ </template>
+ <template v-else>
+ <el-option
+ v-for="w in wardOptions"
+ :key="'ward:' + w.value"
+ :label="w.label"
+ :value="'ward:' + w.value"
+ />
+ </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-column label="绗�1棰�" align="center">
- <el-table-column prop="q1VeryGood" label="闈炲父婊℃剰" width="100" />
- <el-table-column prop="q1Good" label="婊℃剰" width="90" />
- <el-table-column prop="q1Normal" label="涓�鑸�" width="90" />
- <el-table-column prop="q1Bad" label="涓嶆弧鎰�" width="90" />
- <el-table-column prop="q1Score" label="寰楀垎" width="100" />
+ <el-table
+ id="table3"
+ v-loading="loading"
+ element-loading-text="缁熻涓�..."
+ :data="table3Rows"
+ border
+ size="small"
+ max-height="500"
+ :header-cell-style="{ background: '#f5f7fa', color: '#303133' }"
+ >
+ <el-table-column
+ prop="name"
+ :label="statDimension === 'dept' ? '绉戝' : '鐥呭尯'"
+ fixed="left"
+ align="center"
+ />
+ <el-table-column
+ v-for="q in table3Questions"
+ :key="q.title"
+ :label="q.title"
+ align="center"
+ >
+ <el-table-column
+ v-for="opt in q.options"
+ :key="opt"
+ :label="opt"
+ width="100"
+ >
+ <template slot-scope="{ row }">
+ {{
+ (row.cells[q.title] &&
+ row.cells[q.title][opt] &&
+ row.cells[q.title][opt].count) ||
+ 0
+ }}
+ </template>
+ </el-table-column>
+ <el-table-column label="寰楀垎" width="90">
+ <template slot-scope="{ row }">
+ {{ (row.cells[q.title] && row.cells[q.title]._score) || "0.00" }}
+ </template>
+ </el-table-column>
</el-table-column>
- <el-table-column label="绗�2棰�" align="center">
- <el-table-column prop="q2VeryGood" label="闈炲父婊℃剰" width="100" />
- <el-table-column prop="q2Good" label="婊℃剰" width="90" />
- <el-table-column prop="q2Normal" label="涓�鑸�" width="90" />
- <el-table-column prop="q2Bad" label="涓嶆弧鎰�" width="90" />
- <el-table-column prop="q2Score" label="寰楀垎" width="100" />
+ <el-table-column label="缁煎悎寰楀垎" align="center">
+ <template slot-scope="{ row }">
+ {{ row.totalScore }}
+ </template>
</el-table-column>
- <el-table-column prop="totalScore" label="缁煎悎寰楀垎" align="center" />
</el-table>
</el-card>
</div>
</template>
<script>
-import XLSX from 'xlsx';
+import { sltdMydTotalByScore,statQuestionOptionByKsOrBq } from "@/api/AiCentre/satisfaction";
+import XLSX from "xlsx";
+
+// 婊℃剰搴﹂鐨勫浐瀹氶�夐」锛堥『搴忓嵆琛ㄦ牸鍒楅『搴忥級鈥斺�旇繖浜涢�夐」鐨勯鐩繘琛ㄤ竴锛屽叾浣欒繘琛ㄤ簩
+const FIXED_OPTIONS = ["寰堟弧鎰�", "婊℃剰", "涓�鑸�", "涓嶆弧鎰�", "寰堜笉婊℃剰", "鏈粡鍘�"];
+// 鏃犲垎鍊奸�夐」锛堝"鏈粡鍘�"锛夋槸鍚﹁鍏ョ患鍚堝緱鍒嗗垎姣嶏細榛樿 false锛岄伩鍏嶆媺浣庡緱鍒�
+const NO_SCORE_IN_DENOMINATOR = false;
+
+// 鏈嶅姟绫诲瀷瀛楀吀锛堟帴鍙� serviceType锛�
+const SERVICE_TYPES = [
+ { value: "1", label: "鐩戞祴璇勪及" },
+ { value: "2", label: "鍑洪櫌闅忚" },
+ { value: "3", label: "闂ㄨ瘖闅忚" },
+ { value: "4", label: "瀹f暀鍏虫��" },
+ { value: "5", label: "澶嶈瘖閫氱煡" },
+ { value: "6", label: "婊℃剰搴﹁皟鏌�" },
+ { value: "7", label: "鎮h�呮姤鍛�" },
+ { value: "8", label: "鍏朵粬閫氱煡" },
+ { value: "9", label: "浣撴闅忚" },
+ { value: "10", label: "鍖绘妧闅忚" },
+ { value: "11", label: "褰卞儚涓撶闅忚" },
+ { value: "12", label: "蹇冪數涓撶闅忚" },
+ { value: "13", label: "涓撶闅忚" },
+];
+
+// 鏈嶅姟绫诲瀷榛樿閫変腑椤癸紙鎸� label 鍖归厤锛岄�傞厤涓嶅悓瀛楀吀鐨� value 宸紓锛�
+const DEFAULT_SERVICE_LABELS = ["浣忛櫌婊℃剰搴�", "闂ㄨ瘖婊℃剰搴�"];
+
+// 鍏滃簳锛歴tore 涓棤绉戝/鐥呭尯鏁版嵁鏃朵娇鐢紙琛ㄤ笁瀵规瘮 + 绛涢�変笅鎷夛級
+const DEPTS = ["鍐呭垎娉岀", "鐨偆绉�", "鑲跨槫鍐呯", "娑堝寲鍐呯", "鑲惧唴绉�"];
+const WARDS = ["涓�鐥呭尯", "浜岀梾鍖�", "涓夌梾鍖�", "鍥涚梾鍖�", "浜旂梾鍖�"];
+
+/**
+ * 鎺ュ彛鏁版嵁 鈫� 椤甸潰鍐呴儴棰樼洰缁撴瀯
+ * 鍏ュ弬锛氭帴鍙� data锛圠ist<QuestionResultDTO>锛�
+ * 鍑哄弬锛歔{ title, type, total, options: [{ option, count, score }] }]
+ * 鍚屼竴棰樼洰鍙兘鍒嗘暎鍦ㄥ涓换鍔★紙taskname锛夐噷锛岃繖閲屾寜銆岄鐩� + 閫夐」銆嶅悎骞剁疮鍔犳暟閲�
+ */
+function normalizeQuestions(list) {
+ const map = new Map();
+
+ // 鍏ュ弬鍙兘鏄暟缁� / {list} / {rows} / 鍗曞璞★紝缁熶竴瑙勬暣鎴愭暟缁�
+ const arr = Array.isArray(list)
+ ? list
+ : (list &&
+ (Array.isArray(list.list)
+ ? list.list
+ : Array.isArray(list.rows)
+ ? list.rows
+ : [list])) ||
+ [];
+
+ // 棰樼洰鏂囨湰瀛楁鍊欓�夛紙鏈郴缁熼鐩唴瀹圭粺涓�鍙� scriptContent锛屽吋瀹瑰巻鍙插懡鍚嶏級
+ const pickTitle = (item) => {
+ const keys = [
+ "scriptContent",
+ "questionText",
+ "qeustionText",
+ "questiontext",
+ "scriptcontent",
+ "title",
+ "name",
+ "questionName",
+ ];
+ for (const k of keys) {
+ if (item && item[k] != null && String(item[k]).trim()) {
+ return String(item[k]).trim();
+ }
+ }
+ return "";
+ };
+
+ // 閫夐」鏄庣粏鍒楄〃瀛楁鍊欓��
+ const pickDetails = (item) => {
+ const keys = [
+ "subtaskDetailRatioExportList",
+ "details",
+ "optionList",
+ "options",
+ "optionResultList",
+ "subtaskDetailRatioExportlist",
+ "list",
+ ];
+ for (const k of keys) {
+ if (item && Array.isArray(item[k])) return item[k];
+ }
+ return [];
+ };
+
+ // 閫夐」鏂囨湰瀛楁鍊欓��
+ const pickOption = (d) => {
+ const keys = [
+ "optionresult",
+ "optioncontent",
+ "optionText",
+ "targetvalue",
+ "optionName",
+ "name",
+ "option",
+ "content",
+ ];
+ for (const k of keys) {
+ if (d && d[k] != null && String(d[k]).trim()) {
+ return String(d[k]).trim();
+ }
+ }
+ return "";
+ };
+
+ // 鏁伴噺瀛楁鍊欓��
+ const pickCount = (d) => {
+ const keys = ["count", "chosenQuantity", "num", "total", "chosenquantity"];
+ for (const k of keys) {
+ if (d && d[k] != null && !isNaN(Number(d[k]))) {
+ return Number(d[k]) || 0;
+ }
+ }
+ return 0;
+ };
+
+ arr.forEach((item) => {
+ if (!item || typeof item !== "object") return;
+ const title = pickTitle(item) || "鏈懡鍚嶉鐩�";
+ const details = pickDetails(item);
+ let node = map.get(title);
+ if (!node) {
+ node = { title, type: "", options: new Map() };
+ map.set(title, node);
+ }
+
+ details.forEach((d) => {
+ const option = pickOption(d);
+ if (!option) return;
+ const count = pickCount(d);
+ const rawScore = Number(d && d.score);
+ const score = isNaN(rawScore) ? null : rawScore;
+ // 棰樺瀷锛氫紭鍏� scriptType锛�1鍗曢�� 2澶氶�� 4闂瓟锛夛紝鍏舵 valueType锛�1鍗曢�� 2澶氶�� 3闂瓟 4濉┖ 5鍏跺畠锛�
+ const typeVal = String(
+ (item && item.scriptType) ||
+ (d && d.scriptType) ||
+ (d && d.valueType) ||
+ (item && item.valueType) ||
+ ""
+ );
+ if (!node.type) {
+ node.type =
+ typeVal === "2" ? "multiple" : typeVal === "1" ? "single" : "qa";
+ }
+
+ const old = node.options.get(option);
+ if (old) {
+ old.count += count;
+ if (old.score == null) old.score = score;
+ } else {
+ node.options.set(option, { option, count, score });
+ }
+ });
+ });
+
+ return Array.from(map.values()).map((n) => {
+ const options = Array.from(n.options.values());
+ const item = { title: n.title, type: n.type, options };
+ // 澶氶�夐锛氱敤鍚勯�夐」鏁伴噺涔嬪拰浣滀负濉姤鎬婚噺锛堟帴鍙f湭鍗曠嫭鎻愪緵浜烘锛�
+ if (n.type === "multiple")
+ item.total = options.reduce((s, o) => s + o.count, 0);
+ return item;
+ });
+}
export default {
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: [],
+ deptCodes: [], // 绉戝缂栫爜闆嗗悎 => leaveldeptcodes
+ wardCodes: [], // 鍑洪櫌鐥呭尯缂栫爜闆嗗悎 => leavehospitaldistrictcodes锛堟帴鍙e繀濉級
+ serviceTypeList: [], // 鏈嶅姟绫诲瀷澶氶�夛紝鎻愪氦鏃剁敤閫楀彿鎷兼帴
+ deptOrDistrict: "1", // 绉戝/鐥呭尯缁勫悎鏂瑰紡锛岄粯璁� 1锛氱闄㈢梾鍖� and 绂婚櫌绉戝
+ statMode: "ward", // 缁熻缁村害锛歸ard=鎸夌梾鍖虹粺璁� dept=鎸夌瀹ょ粺璁�
+ },
+ SERVICE_TYPES,
+ loading: false,
+
+ // 琛ㄤ簩锛氶鐩ā绯婃煡璇㈠叧閿瓧锛堢函鍓嶇杩囨护锛屼笉璇锋眰鍚庣锛�
+ questionSearch: "",
+
+ // 琛ㄦ牸涓夛細缁熻缁村害 + 鐙珛绉戝/鐥呭尯鏉′欢锛�'inherit' = 娌跨敤椤堕儴锛�
+ statDimension: "ward",
+ statDeptWard: "inherit",
+
+ // 鎺ュ彛杩斿洖鐨勫師濮嬫暟鎹紙宸插綊涓�鍖栫殑棰樼洰鍒楄〃锛�
+ questionList: [],
+ // 琛ㄤ笁锛氱瀹�/鐥呭尯璇勫垎瀵规瘮锛坮ows=琛屾暟鎹紝questions=鍔ㄦ�佸垪锛�
+ deptComparison: { rows: [], questions: [] },
+ wardComparison: { rows: [], questions: [] },
};
},
+ computed: {
+ // 绉戝閫夐」锛氫紭鍏堝彇褰撳墠鐢ㄦ埛鎵�灞炵瀹わ紝鏃犲垯鍏滃簳
+ deptOptions() {
+ const list = this.$store.getters.belongDepts || [];
+ return list.length
+ ? list.map((d) => ({ label: d.deptName, value: d.deptCode }))
+ : DEPTS.map((d) => ({ label: d, value: d }));
+ },
+ // 鐥呭尯閫夐」锛氫紭鍏堝彇褰撳墠鐢ㄦ埛鎵�灞炵梾鍖猴紝鏃犲垯鍏滃簳
+ wardOptions() {
+ const list = this.$store.getters.belongWards || [];
+ return list.length
+ ? list.map((w) => ({ label: w.districtName, value: w.districtCode }))
+ : WARDS.map((w) => ({ label: w, value: w }));
+ },
+ // 鏈嶅姟绫诲瀷涓嬫媺锛氬彇 store 鐨勯殢璁跨被鍨嬪瓧鍏� tasktypes锛屽彇涓嶅埌鏃跺厹搴曟湰鍦� SERVICE_TYPES
+ serviceTypeOptions() {
+ const toOptions = (list) =>
+ (Array.isArray(list) ? list : [])
+ .map((i) => ({
+ label: String(i.label || "").trim(),
+ value: String(i.value),
+ }))
+ .filter((i) => i.label && i.value && i.value !== "undefined");
+ const taskTypes = toOptions(this.$store.getters.tasktypes);
+ return taskTypes.length ? taskTypes : SERVICE_TYPES;
+ },
+ // 鏈�夌梾鍖烘椂鐨勯粯璁ゆ煡璇㈣寖鍥达細鍏ㄩ儴鐥呭尯缂栫爜
+ defaultWardCodes() {
+ return Array.from(new Set(this.wardOptions.map((w) => w.value)));
+ },
+ // 鏈�夌瀹ゆ椂鐨勯粯璁ゆ煡璇㈣寖鍥达細鍏ㄩ儴绉戝缂栫爜
+ defaultDeptCodes() {
+ return Array.from(new Set(this.deptOptions.map((d) => d.value)));
+ },
+ // 鍥哄畾鍏」閫夐」鐨勯鐩� => 琛ㄤ竴
+ satisfactionQuestions() {
+ return this.questionList.filter((q) => this.isSatisfactionQuestion(q));
+ },
+ // 鍏朵綑棰樼洰 => 琛ㄤ簩
+ otherQuestions() {
+ return this.questionList.filter((q) => !this.isSatisfactionQuestion(q));
+ },
+ // 琛ㄤ竴锛氭弧鎰忓害缁熻锛堝惈鎬昏琛岋級
+ serviceSatisfaction() {
+ // 鍙栨寚瀹氶�夐」鐨� { count, score }锛屾湭杩斿洖鍒欒椤逛负 0
+ const pick = (q, name) => {
+ const hit = q.options.find((o) => o.option === name);
+ return hit
+ ? { count: Number(hit.count) || 0, score: hit.score }
+ : { count: 0, score: null };
+ };
+ const NAMES = FIXED_OPTIONS;
+
+ const rows = this.satisfactionQuestions.map((q) => {
+ const veryGood = pick(q, "寰堟弧鎰�");
+ const good = pick(q, "婊℃剰");
+ const normal = pick(q, "涓�鑸�");
+ const bad = pick(q, "涓嶆弧鎰�");
+ const veryBad = pick(q, "寰堜笉婊℃剰");
+ const na = pick(q, "鏈粡鍘�");
+ const items = [veryGood, good, normal, bad, veryBad, na];
+ const total = items.reduce((s, it) => s + it.count, 0);
+ return {
+ category: q.title,
+ veryGoodCount: veryGood.count,
+ veryGoodRate: this.rateOf(veryGood.count, total),
+ goodCount: good.count,
+ goodRate: this.rateOf(good.count, total),
+ normalCount: normal.count,
+ normalRate: this.rateOf(normal.count, total),
+ badCount: bad.count,
+ badRate: this.rateOf(bad.count, total),
+ veryBadCount: veryBad.count,
+ veryBadRate: this.rateOf(veryBad.count, total),
+ naCount: na.count,
+ naRate: this.rateOf(na.count, total),
+ score: this.scoreOfItems(items),
+ };
+ });
+
+ // 鎬昏琛岋細鍚勯鍚岄�夐」鏁伴噺绱姞锛屽垎鍊兼部鐢ㄨ閫夐」鍒嗗��
+ const sum = NAMES.map(() => ({ count: 0, score: null }));
+ this.satisfactionQuestions.forEach((q) => {
+ NAMES.forEach((name, i) => {
+ const it = pick(q, name);
+ sum[i].count += it.count;
+ if (sum[i].score == null) sum[i].score = it.score;
+ });
+ });
+ const total = sum.reduce((s, it) => s + it.count, 0);
+ rows.push({
+ category: "鎬昏",
+ veryGoodCount: sum[0].count,
+ veryGoodRate: this.rateOf(sum[0].count, total),
+ goodCount: sum[1].count,
+ goodRate: this.rateOf(sum[1].count, total),
+ normalCount: sum[2].count,
+ normalRate: this.rateOf(sum[2].count, total),
+ badCount: sum[3].count,
+ badRate: this.rateOf(sum[3].count, total),
+ veryBadCount: sum[4].count,
+ veryBadRate: this.rateOf(sum[4].count, total),
+ naCount: sum[5].count,
+ naRate: this.rateOf(sum[5].count, total),
+ score: this.scoreOfItems(sum),
+ });
+ 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),
+ })),
+ };
+ });
+ },
+ // 琛ㄤ簩锛氭寜棰樼洰妯$硦鏌ヨ锛堢函鍓嶇杩囨护锛屼笉璇锋眰鍚庣锛�
+ filteredQuestionStats() {
+ const kw = (this.questionSearch || "").trim().toLowerCase();
+ if (!kw) return this.questionStats;
+ return this.questionStats.filter((q) =>
+ (q.title || "").toLowerCase().indexOf(kw) > -1
+ );
+ },
+ // 琛ㄤ笁锛氭寜缁村害 + 鐙珛鏉′欢杩囨护鍚庣殑琛�
+ table3Rows() {
+ const comp =
+ this.statDimension === "dept"
+ ? this.deptComparison
+ : this.wardComparison;
+ const list = (comp && comp.rows) || [];
+ const names = this.statFilterNames();
+ if (!names || !names.length) return list;
+ return list.filter((r) => names.indexOf(r.name) > -1);
+ },
+ // 琛ㄤ笁锛氬綋鍓嶇淮搴︿笅鐨勯鐩垪锛堝姩鎬佸垪锛�
+ table3Questions() {
+ const comp =
+ this.statDimension === "dept"
+ ? this.deptComparison
+ : this.wardComparison;
+ return (comp && comp.questions) || [];
+ },
+ },
+ created() {
+ if (!this.queryParams.dateRange || !this.queryParams.dateRange.length) {
+ this.queryParams.dateRange = this.defaultRange();
+ }
+ this.queryParams.serviceTypeList = this.defaultServiceTypes();
+ },
+ mounted() {
+ this.fetchData();
+ },
methods: {
+ // 榛樿閫変腑鐨勬湇鍔$被鍨嬶紙鎸� label 鍖归厤锛屽彇涓嶅埌灏辫繑鍥炵┖鏁扮粍 = 涓嶉檺锛�
+ defaultServiceTypes() {
+ const hit = DEFAULT_SERVICE_LABELS.map((label) => {
+ const item = this.serviceTypeOptions.find((o) => o.label === label);
+ return item ? item.value : null;
+ }).filter((v) => v != null);
+ return Array.from(new Set(hit));
+ },
+ // 琛ㄤ笁鐙珛鏉′欢锛�'inherit' 鏃舵部鐢ㄩ《閮ㄦ墍閫夛紱杩斿洖閫変腑鐨勫悕绉版暟缁勶紙鎺ュ彛鍙洖鍚嶇О锛夛紝绌烘暟缁勮〃绀轰笉闄�
+ statFilterNames() {
+ const opts =
+ this.statDimension === "dept" ? this.deptOptions : this.wardOptions;
+ const nameOf = (code) => {
+ const hit = opts.find((o) => String(o.value) === String(code));
+ return hit ? hit.label : null;
+ };
+ if (this.statDeptWard === "inherit") {
+ const codes =
+ this.statDimension === "dept"
+ ? this.queryParams.deptCodes
+ : this.queryParams.wardCodes;
+ return (codes || []).map(nameOf).filter(Boolean);
+ }
+ // 鍒囨崲缁村害鍚庯紝鑻ュ凡閫夋潯浠朵笉灞炰簬褰撳墠缁村害鍒欒涓轰笉闄�
+ if (this.statDeptWard.startsWith(this.statDimension + ":")) {
+ const name = nameOf(this.statDeptWard.slice(this.statDimension.length + 1));
+ return name ? [name] : [];
+ }
+ return [];
+ },
+ onStatDimensionChange() {
+ // 鍒囨崲缁村害鍚庯紝鑻ュ綋鍓嶅凡閫夋潯浠朵笉灞炰簬鏂扮淮搴︼紝鍒欐仮澶嶁�滄部鐢ㄩ《閮ㄢ��
+ if (
+ this.statDeptWard !== "inherit" &&
+ !this.statDeptWard.startsWith(this.statDimension + ":")
+ ) {
+ this.statDeptWard = "inherit";
+ }
+ },
+ handleReset() {
+ this.queryParams.dateRange = this.defaultRange();
+ this.queryParams.deptCodes = [];
+ this.queryParams.wardCodes = [];
+ this.queryParams.statMode = "ward";
+ this.statDimension = "ward";
+ this.statDeptWard = "inherit";
+ this.queryParams.serviceTypeList = this.defaultServiceTypes();
+ this.fetchData();
+ },
+ // 鍒囨崲缁熻缁村害锛堢瀹�/鐥呭尯锛夋椂娓呯┖鍙︿竴缁村害鐨勯�夋嫨锛屼繚璇佷簰鏂�
+ onStatModeChange() {
+ if (this.queryParams.statMode === "dept") {
+ this.queryParams.wardCodes = [];
+ } else {
+ this.queryParams.deptCodes = [];
+ }
+ // 鍗曞悜鍚屾鍒般�屽悇绉戝/鐥呭尯婊℃剰搴﹁瘎鍒嗗姣斻�嶈〃锛氶《閮� 鈫� 琛ㄤ笁
+ this.statDimension = this.queryParams.statMode;
+ this.onStatDimensionChange();
+ },
+ // 鍒ゆ柇棰樼洰鏄惁涓烘弧鎰忓害鍏」閫夐」棰橈細鎵�鏈夐�夐」閮借惤鍦ㄥ浐瀹氬叚椤瑰唴鍗冲綊琛ㄤ竴
+ isSatisfactionQuestion(q) {
+ if (!q.options || !q.options.length) return false;
+ return q.options.every((o) => FIXED_OPTIONS.includes(o.option));
+ },
+ 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) + "%";
+ },
+ /**
+ * 缁煎悎寰楀垎锛堢湡瀹炲彛寰勶級锛毼�(鍚勯�夐」鍒嗗�� 脳 濉姤閲�) / 危(濉姤閲�)
+ * items: [{ count, score }]锛宻core 鍙栨帴鍙h繑鍥炵殑 score 瀛楁
+ * 鏃犲垎鍊奸�夐」锛堝"鏈粡鍘�"锛夐粯璁や笉璁″叆鍒嗗瓙涓庡垎姣嶏紝閬垮厤鎷変綆寰楀垎
+ */
+ scoreOfItems(items) {
+ let numerator = 0;
+ let denominator = 0;
+ items.forEach((it) => {
+ const count = Number(it && it.count) || 0;
+ const score = Number(it && it.score);
+ const hasScore = !isNaN(score);
+ if (!hasScore && !NO_SCORE_IN_DENOMINATOR) return;
+ numerator += (hasScore ? score : 0) * count;
+ denominator += count;
+ });
+ if (!denominator) return "0.00";
+ return (numerator / denominator).toFixed(2);
+ },
+ defaultRange() {
+ const end = new Date();
+ const start = new Date();
+ start.setDate(start.getDate() - 91);
+ 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)];
+ },
+ // 鎻愮ず淇℃伅锛堝吋瀹� $modal 涓� $message锛�
+ notify(msg, type) {
+ const map = {
+ warning: ["msgWarning", "warning"],
+ error: ["msgError", "error"],
+ success: ["msgSuccess", "success"],
+ };
+ const [modalFn, msgFn] = map[type] || map.success;
+ if (this.$modal && this.$modal[modalFn]) {
+ this.$modal[modalFn](msg);
+ } else if (this.$message && this.$message[msgFn]) {
+ this.$message[msgFn](msg);
+ }
+ },
+ // 鎷夊彇婊℃剰搴︾粺璁℃暟鎹紙鎺ュ彛锛�/smartor/serviceSubtaskDetail/sltdMydTotalByScore锛�
+ async fetchData() {
+ const range = this.queryParams.dateRange || [];
+ if (range.length !== 2 || !range[0] || !range[1]) {
+ this.notify("璇峰厛閫夋嫨鍑洪櫌鏃ユ湡鑼冨洿", "warning");
+ return;
+ }
+
+ // 鐥呭尯 => leavehospitaldistrictcodes锛岀瀹� => leaveldeptcodes
+ // 绉戝/鐥呭尯浜掓枼锛氭寜鐥呭尯鍙紶 leavehospitaldistrictcodes锛屾寜绉戝鍙紶 leaveldeptcodes
+ const wards = this.queryParams.wardCodes || [];
+ const depts = this.queryParams.deptCodes || [];
+
+ const payload = {
+ deptOrDistrict: this.queryParams.deptOrDistrict || "1",
+ startFinishTime: range[0],
+ endFinishTime: range[1],
+ };
+ if (this.queryParams.statMode === "dept") {
+ payload.leaveldeptcodes = depts.length
+ ? Array.from(new Set(depts))
+ : this.defaultDeptCodes;
+ } else {
+ payload.leavehospitaldistrictcodes = wards.length
+ ? Array.from(new Set(wards))
+ : this.defaultWardCodes;
+ }
+ // 鏈嶅姟绫诲瀷澶氶�夛細閫楀彿鍒嗛殧锛堟帴鍙e瓧娈典负 serviceType 瀛楃涓诧級
+ const serviceTypes = this.queryParams.serviceTypeList || [];
+ if (serviceTypes.length) payload.serviceType = serviceTypes.join(",");
+ payload.serviceTypeList = serviceTypes;
+ this.loading = true;
+ try {
+ const res = await sltdMydTotalByScore(payload);
+ // 鎺掓煡鐢細鎵撳嵃鎺ュ彛鍘熷杩斿洖锛屼究浜庢牳瀵瑰瓧娈靛悕锛堢‘璁ゆ棤璇悗鍙垹闄わ級
+ console.log(
+ "[sltdMydTotalByScore] code=",
+ res && res.code,
+ "data=",
+ JSON.stringify(res && res.data).slice(0, 2000)
+ );
+ if (res && res.code === 200) {
+ this.questionList = normalizeQuestions(res.data);
+ if (!this.questionList.length)
+ this.notify("褰撳墠鏉′欢涓嬫殏鏃犵粺璁℃暟鎹�", "warning");
+ } else {
+ this.questionList = [];
+ this.notify((res && res.msg) || "缁熻鏁版嵁鑾峰彇澶辫触", "error");
+ }
+ // 琛ㄤ笁锛氬悇绉戝/鐥呭尯婊℃剰搴﹁瘎鍒嗗姣旓紙鐪熷疄鎺ュ彛锛屽唴閮ㄨ嚜琛屽鐞嗛敊璇級
+ await this.fetchComparison();
+ } catch (e) {
+ this.questionList = [];
+ this.notify("缁熻鏁版嵁璇锋眰澶辫触锛岃绋嶅悗閲嶈瘯", "error");
+ } finally {
+ this.loading = false;
+ }
+ },
+ // 鎷夊彇鍚勭瀹�/鐥呭尯婊℃剰搴﹁瘎鍒嗗姣旓紙鎺ュ彛锛�/smartor/serviceSubtaskDetail/statQuestionOptionByKsOrBq锛�
+ async fetchComparison() {
+ const range = this.queryParams.dateRange || [];
+ if (range.length !== 2 || !range[0] || !range[1]) {
+ this.deptComparison = { rows: [], questions: [] };
+ this.wardComparison = { rows: [], questions: [] };
+ return;
+ }
+ const base = {
+ deptOrDistrict: this.queryParams.deptOrDistrict || "1",
+ startFinishTime: range[0],
+ endFinishTime: range[1],
+ };
+ try {
+ // 鎸夌瀹ょ粺璁★紙leaveldeptcodes 浼樺厛锛変笌鎸夌梾鍖虹粺璁★紙leavehospitaldistrictcodes锛夊悇鏌ヤ竴娆�
+ const [deptRes, wardRes] = await Promise.all([
+ statQuestionOptionByKsOrBq({ ...base, leaveldeptcodes: this.defaultDeptCodes }),
+ statQuestionOptionByKsOrBq({ ...base, leavehospitaldistrictcodes: this.defaultWardCodes }),
+ ]);
+ // 鎺掓煡鐢細鎵撳嵃鎺ュ彛鍘熷杩斿洖锛屼究浜庢牳瀵逛袱绾у祵濂� Map 缁撴瀯锛堢‘璁ゆ棤璇悗鍙垹闄わ級
+ console.log(
+ "[statQuestionOptionByKsOrBq] dept code=",
+ deptRes && deptRes.code,
+ "data=",
+ JSON.stringify(deptRes && deptRes.data).slice(0, 2000),
+ "| ward code=",
+ wardRes && wardRes.code,
+ "data=",
+ JSON.stringify(wardRes && wardRes.data).slice(0, 2000)
+ );
+ this.deptComparison = this.buildComparison(
+ deptRes && deptRes.code === 200 ? deptRes.data : {},
+ "dept"
+ );
+ this.wardComparison = this.buildComparison(
+ wardRes && wardRes.code === 200 ? wardRes.data : {},
+ "ward"
+ );
+ } catch (e) {
+ this.deptComparison = { rows: [], questions: [] };
+ this.wardComparison = { rows: [], questions: [] };
+ console.error("鑾峰彇绉戝/鐥呭尯璇勫垎瀵规瘮澶辫触:", e);
+ }
+ },
+ // 缁勮涓鸿〃涓夋墍闇�鐨勮/鍒楃粨鏋勶細琛�=绉戝/鐥呭尯锛屽垪=棰樼洰脳閫夐」 + 寰楀垎 + 缁煎悎寰楀垎
+ // 鏂版帴鍙h繑鍥炰袱绾у祵濂� Map锛歿 绉戝/鐥呭尯鍚嶇О: { 棰樼洰鍐呭: [QuestionCountKsOrBqDTO] } }
+ // 鍏煎鏃ф墎骞虫暟缁勶細[{ questionText, deptname, leavehospitaldistrictname, targetvalue, answerCount, singleScore }]
+ buildComparison(data, mode) {
+ const questions = [];
+ const qMap = new Map();
+ const rowMap = new Map();
+
+ // 灏嗕竴鏉� (鍚嶇О, 棰樼洰, 閫夐」, 鏁伴噺, 寰楀垎) 鏄庣粏绱姞杩涜/鍒楃粨鏋�
+ const collect = (name, title, option, count, single) => {
+ if (!title || !name) return;
+ let q = qMap.get(title);
+ if (!q) {
+ q = { title, options: [] };
+ qMap.set(title, q);
+ questions.push(q);
+ }
+ if (option && q.options.indexOf(option) === -1) q.options.push(option);
+
+ let row = rowMap.get(name);
+ if (!row) {
+ row = { name, code: this.codeFor(mode, name), cells: {} };
+ rowMap.set(name, row);
+ }
+ if (!row.cells[title]) row.cells[title] = {};
+ const cell = row.cells[title][option] || { count: 0, score: 0 };
+ cell.count += count;
+ cell.score += single;
+ row.cells[title][option] = cell;
+ };
+
+ if (Array.isArray(data)) {
+ // 鏃х粨鏋勶細鎵佸钩鏁扮粍
+ data.forEach((it) => {
+ const title = it.questionText || it.qeustionText || "";
+ const name =
+ mode === "dept"
+ ? it.deptname || ""
+ : it.leavehospitaldistrictname || "";
+ const option = it.targetvalue || it.asrtext || "";
+ collect(name, title, option, Number(it.answerCount) || 0, Number(it.singleScore) || 0);
+ });
+ } else if (data && typeof data === "object") {
+ // 鏂扮粨鏋勶細涓ょ骇宓屽 Map锛岀涓�灞� key=绉戝/鐥呭尯鍚嶇О锛岀浜屽眰 key=棰樼洰锛寁alue=閫夐」鏄庣粏鍒楄〃
+ Object.keys(data).forEach((name) => {
+ const questionMap = data[name] || {};
+ Object.keys(questionMap).forEach((title) => {
+ const list = Array.isArray(questionMap[title]) ? questionMap[title] : [];
+ list.forEach((it) => {
+ const option = it.targetvalue || it.asrtext || "";
+ collect(name, title, option, Number(it.answerCount) || 0, Number(it.singleScore) || 0);
+ });
+ });
+ });
+ }
+
+ const rows = Array.from(rowMap.values());
+ rows.forEach((row) => {
+ let totalCount = 0;
+ let totalScore = 0;
+ questions.forEach((q) => {
+ const cells = row.cells[q.title] || {};
+ let qCount = 0;
+ let qScore = 0;
+ q.options.forEach((opt) => {
+ const c = cells[opt];
+ if (c) {
+ qCount += c.count;
+ qScore += c.score;
+ }
+ });
+ // 璇ラ鐩姞鏉冨钩鍧囧垎锛毼� singleScore / 危 answerCount
+ cells._score = qCount ? (qScore / qCount).toFixed(2) : "0.00";
+ totalCount += qCount;
+ totalScore += qScore;
+ row.cells[q.title] = cells;
+ });
+ row.totalScore = totalCount ? (totalScore / totalCount).toFixed(2) : "0.00";
+ });
+
+ return { rows, questions };
+ },
+ // 鍚嶇О 鈫� 缂栫爜锛堢敤浜庤〃涓夋寜椤堕儴鎵�閫夌紪鐮佽繃婊わ級
+ codeFor(mode, name) {
+ const opts = mode === "dept" ? this.deptOptions : this.wardOptions;
+ const hit = opts.find((o) => String(o.label) === String(name));
+ return hit ? hit.value : name;
+ },
exportTable(tableId) {
- const tableNames = { table1: '鍚勬湇鍔¢」鐩弧鎰忓害缁熻', table2: '鍗曢�夐澶氶�夐缁熻', table3: '鍚勭瀹ゆ弧鎰忓害璇勫垎瀵规瘮' };
- const name = tableNames[tableId] || '缁熻鏁版嵁';
+ const tableNames = {
+ table1: "鍚勬湇鍔¢」鐩弧鎰忓害缁熻",
+ table2: "鍗曢�夐澶氶�夐缁熻",
+ table3: "鍚勭瀹ゆ弧鎰忓害璇勫垎瀵规瘮",
+ };
+ const name = tableNames[tableId] || "缁熻鏁版嵁";
const table = document.getElementById(tableId);
if (!table) return;
@@ -152,10 +1002,10 @@
// 浠庤〃鏍兼彁鍙栨暟鎹瀯寤哄甫鏍峰紡鐨� 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 => {
- row.push((td.innerText || '').trim());
+ tr.querySelectorAll("th,td").forEach((td) => {
+ row.push((td.innerText || "").trim());
});
if (row.length) data.push(row);
});
@@ -163,21 +1013,25 @@
const ws = XLSX.utils.aoa_to_sheet(data);
// 璁剧疆鍒楀
const colWidths = data[0] ? data[0].map(() => ({ wch: 16 })) : [];
- ws['!cols'] = colWidths;
+ ws["!cols"] = colWidths;
- XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
- XLSX.writeFile(wb, name + '.xlsx');
+ XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
+ XLSX.writeFile(wb, name + ".xlsx");
} catch (e) {
// fallback: CSV
- let csv = '锘�';
- table.querySelectorAll('tr').forEach(tr => {
+ let csv = "锘�";
+ table.querySelectorAll("tr").forEach((tr) => {
const row = [];
- tr.querySelectorAll('th,td').forEach(td => row.push('"' + (td.innerText || '').replace(/"/g, '""') + '"'));
- csv += row.join(',') + '\n';
+ 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();
+ 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();
}
},
},
@@ -185,11 +1039,66 @@
</script>
<style lang="scss" scoped>
-.data-overview { .overview-card { margin-bottom: 20px; } }
-.card-header { display: flex; justify-content: space-between; align-items: center; }
-.card-title { font-size: 16px; font-weight: 600; color: #303133; }
-.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; }
+.data-overview {
+ .overview-card {
+ margin-bottom: 20px;
+ }
+}
+.filter-card {
+ .filter-form {
+ margin-bottom: -10px;
+ }
+ .filter-select {
+ width: 220px;
+ }
+ .filter-select-sm {
+ width: 150px;
+ }
+}
+.empty-tip {
+ padding: 40px 0;
+ text-align: center;
+ color: #909399;
+ font-size: 13px;
+}
+.table2-scroll {
+ max-height: 560px;
+ overflow-y: auto;
+}
+.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;
+ }
+ .search-input {
+ width: 200px;
+ margin-right: 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;
+ }
}
</style>
--
Gitblit v1.9.3