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/patient/dedicated/tubeFeed/index.vue | 391 ++++++++++++
随访外链通用9.12.zip | 0
src/views/Satisfaction/sfstatistics/components/ChartsPanel.vue | 443 +++++++++++--
src/views/patient/dedicated/heart/index.vue | 388 ++++++++++++
src/views/Satisfaction/sfstatistics/components/DataOverview.vue | 467 ++++++++++++--
src/views/repositoryai/templateku/configurat/index.vue | 4
随访通用 9.14.zip | 0
src/views/outsideChainnew.vue | 73 ++
src/views/followvisit/discharge/index.vue | 74 ++
src/views/outsideChainwtnew.vue | 70 ++
10 files changed, 1,737 insertions(+), 173 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>
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; }
diff --git a/src/views/followvisit/discharge/index.vue b/src/views/followvisit/discharge/index.vue
index da3ba46..7b355eb 100644
--- a/src/views/followvisit/discharge/index.vue
+++ b/src/views/followvisit/discharge/index.vue
@@ -243,6 +243,17 @@
</div>
</div>
</el-col>
+ <div class="color-legend">
+ <span class="legend-item" v-if="orgname == '鐪佺珛鍚屽痉缈犺嫅闄㈠尯'">
+ <span class="legend-block legend-blue"></span>鏈埌闅忚鏃堕棿
+ </span>
+ <span class="legend-item">
+ <span class="legend-block legend-red"></span>濉姤鍐呭瀛樺湪寮傚父
+ </span>
+ <span class="legend-item">
+ <span class="legend-block legend-yellow"></span>濉姤鍐呭瀛樺湪璀﹀憡
+ </span>
+ </div>
</el-row>
<div class="selected-info">
宸查�変腑
@@ -2309,14 +2320,41 @@
this.getList();
});
},
- // 寮傚父鍒楁覆鏌�
+ // 琛岄鑹叉覆鏌擄細钃濊壊(鏈埌闅忚鏃堕棿) / 绾㈣壊(寮傚父) / 榛勮壊(璀﹀憡)
tableRowClassName({ row, rowIndex }) {
+ // 褰撳墠鏃堕棿灏忎簬搴旈殢璁挎椂闂翠笖浠诲姟鐘舵�佷负寰呴殢璁� 鈫� 鏁磋钃濊壊锛堝崡鍗庡ぇ瀛﹂檮灞炵涓�鍖婚櫌涓嶇敓鏁堬級
+ if (
+ this.orgname == "鍗楀崕澶у闄勫睘绗竴鍖婚櫌" &&
+ row.sendstate == 2 &&
+ this.isBeforeVisitTime(row.visitTime)
+ ) {
+ return "blue-row";
+ }
if (row.excep == 1) {
return "warning-row";
} else if (row.excep == 2) {
return "remind-row";
}
return "";
+ },
+ // 鍒ゆ柇褰撳墠鏃堕棿鏄惁鏃╀簬搴旈殢璁挎椂闂达紙鎸夆�滃ぉ鈥濇瘮杈冿級
+ isBeforeVisitTime(visitTime) {
+ if (!visitTime) return false;
+ let date;
+ if (typeof visitTime === "number") {
+ date = new Date(visitTime);
+ } else {
+ date = new Date(String(visitTime).replace(/-/g, "/"));
+ }
+ if (isNaN(date.getTime())) return false;
+ const now = new Date();
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+ const visitDay = new Date(
+ date.getFullYear(),
+ date.getMonth(),
+ date.getDate()
+ );
+ return today.getTime() < visitDay.getTime();
},
restoreSelection() {
if (!this.$refs.userform) {
@@ -2549,12 +2587,46 @@
::v-deep.el-table .remind-row {
background: #fcf5aa;
}
+::v-deep.el-table .blue-row {
+ background: #c6e2ff;
+}
.documentf {
display: flex;
justify-content: flex-end;
}
+.color-legend {
+ float: right;
+ display: flex;
+ align-items: center;
+ height: 40px;
+ gap: 16px;
+ margin-right: 5px;
+}
+.legend-item {
+ display: inline-flex;
+ align-items: center;
+ font-size: 16px;
+ color: #666;
+}
+.legend-block {
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ border-radius: 3px;
+ margin-right: 6px;
+}
+.legend-blue {
+ background: #9cc9fa;
+}
+.legend-red {
+ background: #f1aaaa;
+}
+.legend-yellow {
+ background: #faf18d;
+}
+
.download {
text-align: center;
diff --git a/src/views/outsideChainnew.vue b/src/views/outsideChainnew.vue
index 1132d17..3feadf1 100644
--- a/src/views/outsideChainnew.vue
+++ b/src/views/outsideChainnew.vue
@@ -22,9 +22,15 @@
v-for="(item, index) in questionList"
:key="item.aaa"
>
- <div class="scriptTopic-dev" :key="index" v-if="item.scriptType == 1">
+ <div
+ class="scriptTopic-dev"
+ :key="index"
+ v-if="item.scriptType == 1"
+ :class="{ 'required-error': item.requiredError }"
+ >
<div class="dev-text">
{{ index + 1 }}銆�<span style="line-height: 1.5"
+ ><span v-if="isRequired(item)" class="required-star">*</span
>{{ item.scriptContent }}
<span style="color: #3ba2f7">[鍗曢�塢</span></span
>
@@ -54,9 +60,15 @@
</div>
</div>
<!-- 澶氶�� -->
- <div class="scriptTopic-dev" :key="index" v-if="item.scriptType == 2">
+ <div
+ class="scriptTopic-dev"
+ :key="index"
+ v-if="item.scriptType == 2"
+ :class="{ 'required-error': item.requiredError }"
+ >
<div class="dev-text">
{{ index + 1 }}銆�<span style="line-height: 1.5"
+ ><span v-if="isRequired(item)" class="required-star">*</span
>{{ item.scriptContent }}
<span style="color: #3ba2f7">[澶氶�塢</span></span
>
@@ -88,9 +100,11 @@
class="scriptTopic-dev"
:key="index"
v-if="item.scriptType == 4 || item.scriptType == 3"
+ :class="{ 'required-error': item.requiredError }"
>
<div class="dev-text">
{{ index + 1 }}銆�<span style="line-height: 1.5"
+ ><span v-if="isRequired(item)" class="required-star">*</span
>{{ item.scriptContent
}}<span style="color: #3ba2f7">[闂瓟]</span></span
>
@@ -421,6 +435,10 @@
// 缂撳瓨
cache(subm) {
console.log("杩涘叆缂撳瓨");
+ // 鎻愪氦鍦烘櫙锛氬厛鍋氬繀濉」鏍¢獙
+ if (subm === true && !this.validateRequired()) {
+ return;
+ }
let form = {
param1: this.taskid,
param2: this.patid,
@@ -459,6 +477,45 @@
}
}
});
+ },
+ // 鏄惁蹇呭~锛�1蹇呭~ 2涓嶅繀濉級
+ isRequired(item) {
+ return item.ismandatory == 1 || item.ismandatory == "1";
+ },
+ // 鍒ゆ柇棰樼洰鏄惁鏈~鍐�
+ isQuestionEmpty(item) {
+ const val = item.questionResult;
+ if (item.scriptType == 2) {
+ return !val || (Array.isArray(val) && val.length === 0);
+ }
+ return val === undefined || val === null || val === "";
+ },
+ // 淇濆瓨鍓嶅繀濉」鏍¢獙
+ validateRequired() {
+ const list = this.questionList || [];
+ list.forEach((item) => {
+ if (item.requiredError) this.$set(item, "requiredError", false);
+ });
+ const unfilled = [];
+ list.forEach((item) => {
+ if (!this.isRequired(item)) return;
+ if (this.isQuestionEmpty(item)) unfilled.push(item);
+ });
+ if (unfilled.length > 0) {
+ unfilled.forEach((item) => this.$set(item, "requiredError", true));
+ this.$modal.msgWarning(
+ `杩樻湁 ${unfilled.length} 閬撳繀濉鏈~鍐欙紝璇疯ˉ鍏呭悗鍐嶆彁浜
+ );
+ this.$nextTick(() => {
+ const idx = this.questionList.indexOf(unfilled[0]);
+ const els = this.$el.querySelectorAll(".scriptTopic-dev");
+ if (els[idx]) {
+ els[idx].scrollIntoView({ behavior: "smooth", block: "center" });
+ }
+ });
+ return false;
+ }
+ return true;
},
// 澶勭悊鍗曢�夐�夐」
handleOptionChange(selectedvalue, index, arr) {
@@ -610,6 +667,18 @@
}
.toptitle {
}
+.required-star {
+ color: #f56c6c;
+ margin-right: 2px;
+ font-weight: 600;
+}
+.scriptTopic-dev.required-error {
+ border: 1px solid #f56c6c;
+ border-left: 4px solid #f56c6c;
+ background: #fef0f0;
+ border-radius: 4px;
+ padding: 8px;
+}
.bottom-fixed {
position: fixed;
bottom: 0;
diff --git a/src/views/outsideChainwtnew.vue b/src/views/outsideChainwtnew.vue
index 5dd3cb3..f37bec9 100644
--- a/src/views/outsideChainwtnew.vue
+++ b/src/views/outsideChainwtnew.vue
@@ -64,6 +64,7 @@
item.prompt &&
item.scriptResult &&
(item.scriptType !== 2 || item.scriptResult.length > 0),
+ 'required-error': item.requiredError,
}"
>
<!-- 棰樼洰棰樺共 -->
@@ -71,7 +72,11 @@
<span class="question-number"
>{{ getQuestionNumber(group, index) }}.</span
>
- <span class="question-text">{{ item.scriptContent }}</span>
+ <span class="question-text"
+ ><span v-if="isRequired(item)" class="required-star"
+ >*</span
+ >{{ item.scriptContent }}</span
+ >
<span class="question-type-tag">
{{
item.scriptType == 1
@@ -201,6 +206,7 @@
item.prompt &&
item.scriptResult &&
(item.scriptType !== 2 || item.scriptResult.length > 0),
+ 'required-error': item.requiredError,
}"
>
<!-- 棰樼洰棰樺共 -->
@@ -208,7 +214,11 @@
<span class="question-number"
>{{ getVisibleQuestionIndex(index) }}.</span
>
- <span class="question-text">{{ item.scriptContent }}</span>
+ <span class="question-text"
+ ><span v-if="isRequired(item)" class="required-star"
+ >*</span
+ >{{ item.scriptContent }}</span
+ >
<span class="question-type-tag">
{{
item.scriptType == 1
@@ -695,6 +705,10 @@
// 缂撳瓨
cache(subm) {
console.log("杩涘叆缂撳瓨");
+ // 鎻愪氦鍦烘櫙锛氬厛鍋氬繀濉」鏍¢獙
+ if (subm === true && !this.validateRequired()) {
+ return;
+ }
// 鎻愪氦鍦烘櫙锛氬紑鍚姞杞界姸鎬侊紝闃叉閲嶅鐐瑰嚮
if (subm === true) {
this.submitting = true;
@@ -764,6 +778,46 @@
this.$modal.msgError("鎻愪氦澶辫触锛岃閲嶈瘯");
}
});
+ },
+ // 鏄惁蹇呭~锛�1蹇呭~ 2涓嶅繀濉級
+ isRequired(item) {
+ return item.ismandatory == 1 || item.ismandatory == "1";
+ },
+ // 鍒ゆ柇棰樼洰鏄惁鏈~鍐�
+ isQuestionEmpty(item) {
+ const val = item.scriptResult;
+ if (item.scriptType == 2) {
+ return !val || (Array.isArray(val) && val.length === 0);
+ }
+ return val === undefined || val === null || val === "";
+ },
+ // 淇濆瓨鍓嶅繀濉」鏍¢獙
+ validateRequired() {
+ const list = this.questionList || [];
+ list.forEach((item) => {
+ if (item.requiredError) this.$set(item, "requiredError", false);
+ });
+ const unfilled = [];
+ list.forEach((item) => {
+ if (item.ishide) return;
+ if (!this.isRequired(item)) return;
+ if (this.isQuestionEmpty(item)) unfilled.push(item);
+ });
+ if (unfilled.length > 0) {
+ unfilled.forEach((item) => this.$set(item, "requiredError", true));
+ this.$modal.msgWarning(
+ `杩樻湁 ${unfilled.length} 閬撳繀濉鏈~鍐欙紝璇疯ˉ鍏呭悗鍐嶆彁浜
+ );
+ this.$nextTick(() => {
+ const idx = this.visibleQuestions.indexOf(unfilled[0]);
+ const els = this.$el.querySelectorAll(".question-item");
+ if (els[idx]) {
+ els[idx].scrollIntoView({ behavior: "smooth", block: "center" });
+ }
+ });
+ return false;
+ }
+ return true;
},
// 鑾峰彇鍙棰樼洰鐨勬纭簭鍙凤紙瑙e喅璺抽鍚庡簭鍙蜂笉杩炵画鐨勯棶棰橈級
getVisibleQuestionIndex(index) {
@@ -1041,6 +1095,12 @@
border-left: 4px solid #e6a23c;
}
+ &.required-error {
+ border-color: #f56c6c;
+ border-left: 4px solid #f56c6c;
+ background: #fef0f0;
+ }
+
&:last-child {
margin-bottom: 0;
}
@@ -1074,6 +1134,12 @@
min-width: 16px;
}
+.required-star {
+ color: #f56c6c;
+ margin-right: 2px;
+ font-weight: 600;
+}
+
// .question-text {
// line-height: 1.5;
// color: #2c3e50;
diff --git a/src/views/patient/dedicated/heart/index.vue b/src/views/patient/dedicated/heart/index.vue
new file mode 100644
index 0000000..092e749
--- /dev/null
+++ b/src/views/patient/dedicated/heart/index.vue
@@ -0,0 +1,388 @@
+<template>
+ <div class="app-container">
+ <!-- 鏌ヨ鏍� -->
+ <el-form :inline="true" :model="queryParams" size="small" class="query-form">
+ <el-form-item label="鎮h�呭鍚�">
+ <el-input v-model="queryParams.name" placeholder="璇疯緭鍏ュ鍚�" clearable @keyup.enter.native="handleQuery" />
+ </el-form-item>
+ <el-form-item label="涓绘不鍖荤敓">
+ <el-input v-model="queryParams.drname" placeholder="璇疯緭鍏ュ尰鐢熷鍚�" clearable @keyup.enter.native="handleQuery" />
+ </el-form-item>
+ <el-form-item label="绉戝/鐥呭尯">
+ <el-select v-model="queryParams.deptWard" placeholder="鍏ㄩ儴" clearable filterable>
+ <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 label="蹇冨姛鑳藉垎绾�">
+ <el-select v-model="queryParams.nyhaClass" placeholder="鍏ㄩ儴" clearable>
+ <el-option v-for="g in NYHA_CLASSES" :key="g" :label="g" :value="g" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="el-icon-search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="el-icon-refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <!-- 鎿嶄綔鏍� -->
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" icon="el-icon-plus" size="mini" @click="openAddDialog">娣诲姞鎮h��</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport">瀵煎嚭</el-button>
+ </el-col>
+ </el-row>
+
+ <!-- 鍒楄〃 -->
+ <el-table :data="currentPageData" v-loading="loading" border @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="50" align="center" />
+ <el-table-column label="濮撳悕" prop="name" min-width="90" align="center" fixed="left" />
+ <el-table-column label="鎬у埆" prop="sex" width="60" align="center" />
+ <el-table-column label="骞撮緞" prop="age" width="70" align="center" />
+ <el-table-column label="鑱旂郴鐢佃瘽" prop="telcode" width="120" align="center" />
+ <el-table-column label="绉戝" prop="deptname" min-width="110" align="center" show-overflow-tooltip />
+ <el-table-column label="鐥呭尯" prop="wardname" min-width="90" align="center" />
+ <el-table-column label="涓绘不鍖荤敓" prop="drname" width="90" align="center" />
+ <el-table-column label="蹇冭剰璇婃柇" prop="heartDiagnosis" min-width="120" align="center" show-overflow-tooltip />
+ <el-table-column label="蹇冨姛鑳藉垎绾�" prop="nyhaClass" width="100" align="center">
+ <template slot-scope="scope">
+ <el-tag :type="nyhaTagType(scope.row.nyhaClass)" size="small">{{ scope.row.nyhaClass }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="蹇冪巼(娆�/鍒�)" prop="heartRate" width="100" align="center" />
+ <el-table-column label="琛�鍘�(mmHg)" prop="bloodPressure" width="120" align="center" />
+ <el-table-column label="闅忚璁″垝" prop="followPlan" width="100" align="center" />
+ <el-table-column label="鏉ユ簮" prop="source" width="90" align="center" />
+ <el-table-column label="鍔犲叆鏃堕棿" prop="addTime" width="150" align="center" />
+ <el-table-column label="鎿嶄綔" width="120" align="center" fixed="right">
+ <template slot-scope="scope">
+ <el-button type="text" size="mini" icon="el-icon-view" @click="openDetail(scope.row)">璇︽儏</el-button>
+ <el-button type="text" size="mini" icon="el-icon-close" class="danger-text" @click="removeRow(scope.row)">绉婚櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <el-pagination
+ class="pagination-wrap"
+ background
+ layout="total, sizes, prev, pager, next, jumper"
+ :total="filteredList.length"
+ :current-page.sync="pageNum"
+ :page-size.sync="pageSize"
+ :page-sizes="[10, 20, 50]"
+ />
+
+ <!-- 娣诲姞鎮h�呭脊绐� -->
+ <el-dialog title="娣诲姞涓撶梾鎮h��" :visible.sync="addVisible" width="80%" top="6vh">
+ <el-tabs v-model="activeSource" @tab-click="onSourceTabChange">
+ <el-tab-pane label="鎮h�呮。妗�" name="archive" />
+ <el-tab-pane label="鍑洪櫌璁板綍" name="hospital" />
+ <el-tab-pane label="闂ㄨ瘖璁板綍" name="outpatient" />
+ <el-tab-pane label="鍦ㄩ櫌璁板綍" name="inpatient" />
+ </el-tabs>
+ <el-form :inline="true" size="small">
+ <el-form-item label="濮撳悕/璇婃柇">
+ <el-input v-model="poolKeyword" placeholder="杈撳叆濮撳悕鎴栬瘖鏂叧閿瓧" clearable style="width: 220px" @input="handlePoolSearch" />
+ </el-form-item>
+ <span class="pool-tip">鍏� {{ filteredPool.length }} 鏉★紝宸查�� {{ addSelected.length }} 鏉�</span>
+ </el-form>
+ <el-table ref="addTable" :data="filteredPool" border size="small" max-height="420" @selection-change="onAddSelectionChange">
+ <el-table-column type="selection" width="50" align="center" />
+ <el-table-column label="濮撳悕" prop="name" width="90" align="center" />
+ <el-table-column label="鎬у埆" prop="sex" width="60" align="center" />
+ <el-table-column label="骞撮緞" prop="age" width="70" align="center" />
+ <el-table-column label="鑱旂郴鐢佃瘽" prop="telcode" width="120" align="center" />
+ <el-table-column label="绉戝" prop="deptname" min-width="110" align="center" show-overflow-tooltip />
+ <el-table-column label="鐥呭尯" prop="wardname" min-width="90" align="center" />
+ <el-table-column label="璇婃柇" prop="diagname" min-width="130" align="center" show-overflow-tooltip />
+ <el-table-column label="鍖荤敓" prop="drname" width="90" align="center" />
+ <el-table-column label="鏃ユ湡" prop="sourceDate" width="110" align="center" />
+ </el-table>
+ <div slot="footer" class="dialog-footer">
+ <el-button @click="addVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="confirmAdd">纭畾娣诲姞</el-button>
+ </div>
+ </el-dialog>
+
+ <!-- 璇︽儏寮圭獥 -->
+ <el-dialog title="涓撶梾鎮h�呰鎯�" :visible.sync="detailVisible" width="720px">
+ <el-descriptions :column="2" border size="medium" v-if="detailRow">
+ <el-descriptions-item label="濮撳悕">{{ detailRow.name }}</el-descriptions-item>
+ <el-descriptions-item label="鎬у埆">{{ detailRow.sex }}</el-descriptions-item>
+ <el-descriptions-item label="骞撮緞">{{ detailRow.age }}</el-descriptions-item>
+ <el-descriptions-item label="鑱旂郴鐢佃瘽">{{ detailRow.telcode }}</el-descriptions-item>
+ <el-descriptions-item label="绉戝">{{ detailRow.deptname }}</el-descriptions-item>
+ <el-descriptions-item label="鐥呭尯">{{ detailRow.wardname || '鈥�' }}</el-descriptions-item>
+ <el-descriptions-item label="涓绘不鍖荤敓">{{ detailRow.drname }}</el-descriptions-item>
+ <el-descriptions-item label="蹇冭剰璇婃柇">{{ detailRow.heartDiagnosis }}</el-descriptions-item>
+ <el-descriptions-item label="蹇冨姛鑳藉垎绾�">{{ detailRow.nyhaClass }}</el-descriptions-item>
+ <el-descriptions-item label="蹇冪巼">{{ detailRow.heartRate }} 娆�/鍒�</el-descriptions-item>
+ <el-descriptions-item label="琛�鍘�">{{ detailRow.bloodPressure }} mmHg</el-descriptions-item>
+ <el-descriptions-item label="闅忚璁″垝">{{ detailRow.followPlan }}</el-descriptions-item>
+ <el-descriptions-item label="鎮h�呮潵婧�">{{ detailRow.source }}</el-descriptions-item>
+ <el-descriptions-item label="鍔犲叆鏃堕棿">{{ detailRow.addTime }}</el-descriptions-item>
+ </el-descriptions>
+ </el-dialog>
+ </div>
+</template>
+
+<script>
+// 蹇冭剰涓撶梾鎮h�� 鈥斺�� 妯℃嫙鏁版嵁锛屾帴鍙e氨缁悗鏇挎崲 getList / confirmAdd 涓殑鏈湴鏁版嵁閫昏緫
+const PERSONS = [
+ { name: '寮犱紵', sex: '鐢�', age: 68 },
+ { name: '鏉庡', sex: '濂�', age: 55 },
+ { name: '鐜嬪己', sex: '鐢�', age: 72 },
+ { name: '鍒樻晱', sex: '濂�', age: 61 },
+ { name: '闄堥潤', sex: '濂�', age: 66 },
+ { name: '鏉ㄥ啗', sex: '鐢�', age: 59 },
+ { name: '璧典附', sex: '濂�', age: 70 },
+ { name: '榛勫媷', sex: '鐢�', age: 64 },
+ { name: '鍛ㄨ壋', sex: '濂�', age: 58 },
+ { name: '鍚村垰', sex: '鐢�', age: 75 },
+];
+const HEART_DIAGNOSES = ['鍐犲績鐥�', '蹇冨姏琛扮', '楂樿鍘�', '蹇冩埧棰ゅ姩', '蹇冭倢姊楁', '蹇冨緥澶卞父', '蹇冭剰鐡h啘鐥�', '鎵╁紶鍨嬪績鑲岀梾', '鑲ュ帤鍨嬪績鑲岀梾', '鎬ユ�у啝鑴夌患鍚堝緛'];
+const DEPTS = ['蹇冭绠″唴绉�', '鑰佸勾绉�', '閲嶇棁鍖诲绉�', '搴峰鍖诲绉�', '蹇冨绉�', '鍐呭垎娉岀', '绁炵粡鍐呯', '鍛煎惛鍐呯'];
+const WARDS = ['涓�鐥呭尯', '浜岀梾鍖�', '涓夌梾鍖�', '鍥涚梾鍖�', '浜旂梾鍖�'];
+const DOCTORS = ['鐜嬩富浠�', '鏉庡尰鐢�', '寮犲尰鐢�', '鍒樺尰鐢�', '闄堝尰鐢�'];
+const NYHA_CLASSES = ['鈪犵骇', '鈪$骇', '鈪㈢骇', '鈪g骇'];
+const FOLLOW_PLANS = ['姣忓懆', '姣忎袱鍛�', '姣忔湀', '姣忓搴�'];
+const SOURCE_LABEL = { archive: '鎮h�呮。妗�', hospital: '鍑洪櫌璁板綍', outpatient: '闂ㄨ瘖璁板綍', inpatient: '鍦ㄩ櫌璁板綍' };
+
+export default {
+ name: 'HeartDedicated',
+ data() {
+ return {
+ loading: false,
+ // 涓撶梾鎮h�呭垪琛紙宸插姞鍏ワ級
+ patientList: [],
+ ids: [],
+ single: true,
+ multiple: true,
+ // 鏌ヨ鏉′欢
+ queryParams: { name: '', drname: '', deptWard: '', nyhaClass: '' },
+ pageNum: 1,
+ pageSize: 10,
+ deptOptions: DEPTS,
+ wardOptions: WARDS,
+ NYHA_CLASSES,
+ // 娣诲姞寮圭獥
+ addVisible: false,
+ activeSource: 'archive',
+ poolKeyword: '',
+ addSelected: [],
+ mockPool: { archive: [], hospital: [], outpatient: [], inpatient: [] },
+ // 璇︽儏寮圭獥
+ detailVisible: false,
+ detailRow: null,
+ };
+ },
+ computed: {
+ filteredList() {
+ const { name, drname, deptWard, nyhaClass } = this.queryParams;
+ return this.patientList.filter((r) => {
+ if (name && !r.name.includes(name)) return false;
+ if (drname && !r.drname.includes(drname)) return false;
+ if (deptWard) {
+ const label = deptWard.split(':')[1];
+ if (label && r.deptname !== label && r.wardname !== label) return false;
+ }
+ if (nyhaClass && r.nyhaClass !== nyhaClass) return false;
+ return true;
+ });
+ },
+ currentPageData() {
+ const start = (this.pageNum - 1) * this.pageSize;
+ return this.filteredList.slice(start, start + this.pageSize);
+ },
+ filteredPool() {
+ const kw = this.poolKeyword.trim();
+ const list = this.mockPool[this.activeSource] || [];
+ if (!kw) return list;
+ return list.filter((r) => r.name.includes(kw) || r.diagname.includes(kw));
+ },
+ },
+ created() {
+ this.buildPool();
+ this.buildInitialList();
+ },
+ methods: {
+ // 鐢熸垚鍚勬潵婧愮殑妯℃嫙鎮h�呮睜
+ buildPool() {
+ const mk = (idx, source, noPrefix) => {
+ const p = PERSONS[idx % PERSONS.length];
+ const isWard = source === 'hospital' || source === 'inpatient';
+ return {
+ id: noPrefix + (idx + 1),
+ name: p.name,
+ sex: p.sex,
+ age: p.age,
+ telcode: '139' + (10000000 + idx * 1111111),
+ deptname: DEPTS[idx % DEPTS.length],
+ wardname: isWard ? WARDS[idx % WARDS.length] : '',
+ drname: DOCTORS[idx % DOCTORS.length],
+ diagname: HEART_DIAGNOSES[idx % HEART_DIAGNOSES.length],
+ sourceDate: this.recentDate(idx * 3 + (source === 'outpatient' ? 2 : 5)),
+ };
+ };
+ this.mockPool = {
+ archive: PERSONS.map((_, i) => mk(i, 'archive', 'da')),
+ hospital: PERSONS.map((_, i) => mk(i, 'hospital', 'zy')),
+ outpatient: PERSONS.map((_, i) => mk(i, 'outpatient', 'mz')),
+ inpatient: PERSONS.map((_, i) => mk(i, 'inpatient', 'zy')),
+ };
+ },
+ // 棰勭疆鑻ュ共宸插姞鍏ョ殑鎮h�咃紝淇濊瘉棣栧睆鏈夋暟鎹�
+ buildInitialList() {
+ const take = (source, n) =>
+ this.mockPool[source].slice(0, n).map((row) => this.toDedicated(row, source));
+ this.patientList = [
+ ...take('hospital', 3),
+ ...take('inpatient', 2),
+ ...take('archive', 2),
+ ...take('outpatient', 1),
+ ];
+ },
+ // 姹犳偅鑰� => 涓撶梾鎮h�咃紙琛ュ厖蹇冭剰鐩稿叧瀛楁锛�
+ toDedicated(row, source) {
+ const n = Number(String(row.id).replace(/\D/g, '')) || 0;
+ const nyhaClass = NYHA_CLASSES[n % NYHA_CLASSES.length];
+ const heartRate = 58 + (n % 5) * 8;
+ const high = 118 + (n % 4) * 8;
+ const low = 72 + (n % 3) * 4;
+ return {
+ id: row.id,
+ name: row.name,
+ sex: row.sex,
+ age: row.age,
+ telcode: row.telcode,
+ deptname: row.deptname,
+ wardname: row.wardname,
+ drname: row.drname,
+ heartDiagnosis: row.diagname,
+ nyhaClass,
+ heartRate,
+ bloodPressure: `${high}/${low}`,
+ followPlan: FOLLOW_PLANS[n % FOLLOW_PLANS.length],
+ source: SOURCE_LABEL[source] || source,
+ addTime: this.now(),
+ };
+ },
+ nyhaTagType(grade) {
+ const map = { '鈪犵骇': 'success', '鈪$骇': 'primary', '鈪㈢骇': 'warning', '鈪g骇': 'danger' };
+ return map[grade] || 'info';
+ },
+ handleQuery() {
+ this.pageNum = 1;
+ },
+ resetQuery() {
+ this.queryParams = { name: '', drname: '', deptWard: '', nyhaClass: '' };
+ this.pageNum = 1;
+ },
+ handleSelectionChange(selection) {
+ this.ids = selection.map((item) => item.id);
+ this.single = selection.length !== 1;
+ this.multiple = !selection.length;
+ },
+ openAddDialog() {
+ this.addSelected = [];
+ this.poolKeyword = '';
+ this.activeSource = 'archive';
+ this.addVisible = true;
+ this.$nextTick(() => this.$refs.addTable && this.$refs.addTable.clearSelection());
+ },
+ onSourceTabChange() {
+ this.addSelected = [];
+ this.$nextTick(() => this.$refs.addTable && this.$refs.addTable.clearSelection());
+ },
+ handlePoolSearch() {
+ // 鏈湴杩囨护锛岀敱 filteredPool 璁$畻灞炴�у畬鎴�
+ },
+ onAddSelectionChange(selection) {
+ this.addSelected = selection;
+ },
+ confirmAdd() {
+ if (!this.addSelected.length) {
+ this.$modal.msgWarning('璇峰厛閫夋嫨瑕佹坊鍔犵殑鎮h��');
+ return;
+ }
+ const existing = new Set(this.patientList.map((r) => r.id));
+ const toAdd = this.addSelected
+ .filter((row) => !existing.has(row.id))
+ .map((row) => this.toDedicated(row, this.activeSource));
+ this.patientList = [...toAdd, ...this.patientList];
+ this.addVisible = false;
+ this.$modal.msgSuccess(`鎴愬姛娣诲姞 ${toAdd.length} 浣嶆偅鑰卄);
+ },
+ removeRow(row) {
+ this.$modal
+ .confirm(`鏄惁纭灏嗘偅鑰�"${row.name}"绉诲嚭蹇冭剰涓撶梾鍒楄〃锛焋)
+ .then(() => {
+ this.patientList = this.patientList.filter((r) => r.id !== row.id);
+ this.$modal.msgSuccess('绉婚櫎鎴愬姛');
+ })
+ .catch(() => {});
+ },
+ handleDelete() {
+ this.$modal
+ .confirm(`鏄惁纭鍒犻櫎閫変腑鐨� ${this.ids.length} 浣嶆偅鑰咃紵`)
+ .then(() => {
+ this.patientList = this.patientList.filter((r) => !this.ids.includes(r.id));
+ this.$modal.msgSuccess('鍒犻櫎鎴愬姛');
+ })
+ .catch(() => {});
+ },
+ openDetail(row) {
+ this.detailRow = row;
+ this.detailVisible = true;
+ },
+ handleExport() {
+ const rows = this.filteredList;
+ if (!rows.length) {
+ this.$modal.msgWarning('鏆傛棤鏁版嵁鍙鍑�');
+ return;
+ }
+ const head = ['濮撳悕', '鎬у埆', '骞撮緞', '鑱旂郴鐢佃瘽', '绉戝', '鐥呭尯', '涓绘不鍖荤敓', '蹇冭剰璇婃柇', '蹇冨姛鑳藉垎绾�', '蹇冪巼(娆�/鍒�)', '琛�鍘�(mmHg)', '闅忚璁″垝', '鏉ユ簮', '鍔犲叆鏃堕棿'];
+ const keys = ['name', 'sex', 'age', 'telcode', 'deptname', 'wardname', 'drname', 'heartDiagnosis', 'nyhaClass', 'heartRate', 'bloodPressure', 'followPlan', 'source', 'addTime'];
+ const csv = [head.join(',')]
+ .concat(rows.map((r) => keys.map((k) => `"${(r[k] ?? '').toString().replace(/"/g, '""')}"`).join(',')))
+ .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 = `蹇冭剰涓撶梾鎮h�卂${Date.now()}.csv`;
+ link.click();
+ URL.revokeObjectURL(link.href);
+ },
+ recentDate(daysAgo) {
+ const d = new Date();
+ d.setDate(d.getDate() - daysAgo);
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
+ const dd = String(d.getDate()).padStart(2, '0');
+ return `${d.getFullYear()}-${mm}-${dd}`;
+ },
+ now() {
+ const d = new Date();
+ const pad = (x) => String(x).padStart(2, '0');
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.query-form { margin-bottom: 4px; }
+.mb8 { margin-bottom: 8px; }
+.pagination-wrap { margin-top: 16px; text-align: right; }
+.pool-tip { font-size: 12px; color: #909399; margin-left: 8px; }
+.danger-text { color: #f56c6c; }
+</style>
diff --git a/src/views/patient/dedicated/tubeFeed/index.vue b/src/views/patient/dedicated/tubeFeed/index.vue
new file mode 100644
index 0000000..e9e2b8a
--- /dev/null
+++ b/src/views/patient/dedicated/tubeFeed/index.vue
@@ -0,0 +1,391 @@
+<template>
+ <div class="app-container">
+ <!-- 鏌ヨ鏍� -->
+ <el-form :inline="true" :model="queryParams" size="small" class="query-form">
+ <el-form-item label="鎮h�呭鍚�">
+ <el-input v-model="queryParams.name" placeholder="璇疯緭鍏ュ鍚�" clearable @keyup.enter.native="handleQuery" />
+ </el-form-item>
+ <el-form-item label="涓绘不鍖荤敓">
+ <el-input v-model="queryParams.drname" placeholder="璇疯緭鍏ュ尰鐢熷鍚�" clearable @keyup.enter.native="handleQuery" />
+ </el-form-item>
+ <el-form-item label="绉戝/鐥呭尯">
+ <el-select v-model="queryParams.deptWard" placeholder="鍏ㄩ儴" clearable filterable>
+ <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 label="绠¢ゲ鏂瑰紡">
+ <el-select v-model="queryParams.feedType" placeholder="鍏ㄩ儴" clearable>
+ <el-option v-for="t in TUBE_FEED_TYPES" :key="t" :label="t" :value="t" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="绠¢ゲ鐘舵��">
+ <el-select v-model="queryParams.feedStatus" placeholder="鍏ㄩ儴" clearable>
+ <el-option v-for="s in TUBE_STATUS" :key="s" :label="s" :value="s" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="el-icon-search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="el-icon-refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <!-- 鎿嶄綔鏍� -->
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" icon="el-icon-plus" size="mini" @click="openAddDialog">娣诲姞鎮h��</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport">瀵煎嚭</el-button>
+ </el-col>
+ </el-row>
+
+ <!-- 鍒楄〃 -->
+ <el-table :data="currentPageData" v-loading="loading" border @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="50" align="center" />
+ <el-table-column label="濮撳悕" prop="name" min-width="90" align="center" fixed="left" />
+ <el-table-column label="鎬у埆" prop="sex" width="60" align="center" />
+ <el-table-column label="骞撮緞" prop="age" width="70" align="center" />
+ <el-table-column label="鑱旂郴鐢佃瘽" prop="telcode" width="120" align="center" />
+ <el-table-column label="绉戝" prop="deptname" min-width="110" align="center" show-overflow-tooltip />
+ <el-table-column label="鐥呭尯" prop="wardname" min-width="90" align="center" />
+ <el-table-column label="涓绘不鍖荤敓" prop="drname" width="90" align="center" />
+ <el-table-column label="璇婃柇" prop="diagname" min-width="120" align="center" show-overflow-tooltip />
+ <el-table-column label="绠¢ゲ鏂瑰紡" prop="feedType" width="100" align="center" />
+ <el-table-column label="绠¢ゲ寮�濮嬫棩鏈�" prop="feedStartDate" width="120" align="center" />
+ <el-table-column label="钀ュ吇鏂规" prop="nutrition" min-width="140" align="center" show-overflow-tooltip />
+ <el-table-column label="姣忔棩鑳介噺(kcal)" prop="energy" width="110" align="center" />
+ <el-table-column label="绠¢ゲ鐘舵��" prop="feedStatus" width="90" align="center">
+ <template slot-scope="scope">
+ <el-tag :type="scope.row.feedStatus === '宸插仠鐢�' ? 'info' : 'success'" size="small">{{ scope.row.feedStatus }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏉ユ簮" prop="source" width="90" align="center" />
+ <el-table-column label="鍔犲叆鏃堕棿" prop="addTime" width="150" align="center" />
+ <el-table-column label="鎿嶄綔" width="120" align="center" fixed="right">
+ <template slot-scope="scope">
+ <el-button type="text" size="mini" icon="el-icon-view" @click="openDetail(scope.row)">璇︽儏</el-button>
+ <el-button type="text" size="mini" icon="el-icon-close" class="danger-text" @click="removeRow(scope.row)">绉婚櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <el-pagination
+ class="pagination-wrap"
+ background
+ layout="total, sizes, prev, pager, next, jumper"
+ :total="filteredList.length"
+ :current-page.sync="pageNum"
+ :page-size.sync="pageSize"
+ :page-sizes="[10, 20, 50]"
+ />
+
+ <!-- 娣诲姞鎮h�呭脊绐� -->
+ <el-dialog title="娣诲姞涓撶梾鎮h��" :visible.sync="addVisible" width="80%" top="6vh">
+ <el-tabs v-model="activeSource" @tab-click="onSourceTabChange">
+ <el-tab-pane label="鎮h�呮。妗�" name="archive" />
+ <el-tab-pane label="鍑洪櫌璁板綍" name="hospital" />
+ <el-tab-pane label="闂ㄨ瘖璁板綍" name="outpatient" />
+ <el-tab-pane label="鍦ㄩ櫌璁板綍" name="inpatient" />
+ </el-tabs>
+ <el-form :inline="true" size="small">
+ <el-form-item label="濮撳悕/璇婃柇">
+ <el-input v-model="poolKeyword" placeholder="杈撳叆濮撳悕鎴栬瘖鏂叧閿瓧" clearable style="width: 220px" @input="handlePoolSearch" />
+ </el-form-item>
+ <span class="pool-tip">鍏� {{ filteredPool.length }} 鏉★紝宸查�� {{ addSelected.length }} 鏉�</span>
+ </el-form>
+ <el-table ref="addTable" :data="filteredPool" border size="small" max-height="420" @selection-change="onAddSelectionChange">
+ <el-table-column type="selection" width="50" align="center" />
+ <el-table-column label="濮撳悕" prop="name" width="90" align="center" />
+ <el-table-column label="鎬у埆" prop="sex" width="60" align="center" />
+ <el-table-column label="骞撮緞" prop="age" width="70" align="center" />
+ <el-table-column label="鑱旂郴鐢佃瘽" prop="telcode" width="120" align="center" />
+ <el-table-column label="绉戝" prop="deptname" min-width="110" align="center" show-overflow-tooltip />
+ <el-table-column label="鐥呭尯" prop="wardname" min-width="90" align="center" />
+ <el-table-column label="璇婃柇" prop="diagname" min-width="130" align="center" show-overflow-tooltip />
+ <el-table-column label="鍖荤敓" prop="drname" width="90" align="center" />
+ <el-table-column label="鏃ユ湡" prop="sourceDate" width="110" align="center" />
+ </el-table>
+ <div slot="footer" class="dialog-footer">
+ <el-button @click="addVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="confirmAdd">纭畾娣诲姞</el-button>
+ </div>
+ </el-dialog>
+
+ <!-- 璇︽儏寮圭獥 -->
+ <el-dialog title="涓撶梾鎮h�呰鎯�" :visible.sync="detailVisible" width="720px">
+ <el-descriptions :column="2" border size="medium" v-if="detailRow">
+ <el-descriptions-item label="濮撳悕">{{ detailRow.name }}</el-descriptions-item>
+ <el-descriptions-item label="鎬у埆">{{ detailRow.sex }}</el-descriptions-item>
+ <el-descriptions-item label="骞撮緞">{{ detailRow.age }}</el-descriptions-item>
+ <el-descriptions-item label="鑱旂郴鐢佃瘽">{{ detailRow.telcode }}</el-descriptions-item>
+ <el-descriptions-item label="绉戝">{{ detailRow.deptname }}</el-descriptions-item>
+ <el-descriptions-item label="鐥呭尯">{{ detailRow.wardname || '鈥�' }}</el-descriptions-item>
+ <el-descriptions-item label="涓绘不鍖荤敓">{{ detailRow.drname }}</el-descriptions-item>
+ <el-descriptions-item label="璇婃柇">{{ detailRow.diagname }}</el-descriptions-item>
+ <el-descriptions-item label="绠¢ゲ鏂瑰紡">{{ detailRow.feedType }}</el-descriptions-item>
+ <el-descriptions-item label="绠¢ゲ寮�濮嬫棩鏈�">{{ detailRow.feedStartDate }}</el-descriptions-item>
+ <el-descriptions-item label="钀ュ吇鏂规">{{ detailRow.nutrition }}</el-descriptions-item>
+ <el-descriptions-item label="姣忔棩鑳介噺">{{ detailRow.energy }} kcal</el-descriptions-item>
+ <el-descriptions-item label="绠¢ゲ鐘舵��">{{ detailRow.feedStatus }}</el-descriptions-item>
+ <el-descriptions-item label="鎮h�呮潵婧�">{{ detailRow.source }}</el-descriptions-item>
+ <el-descriptions-item label="鍔犲叆鏃堕棿">{{ detailRow.addTime }}</el-descriptions-item>
+ </el-descriptions>
+ </el-dialog>
+ </div>
+</template>
+
+<script>
+// 绠¢ゲ涓撶梾鎮h�� 鈥斺�� 妯℃嫙鏁版嵁锛屾帴鍙e氨缁悗鏇挎崲 getList / confirmAdd 涓殑鏈湴鏁版嵁閫昏緫
+const PERSONS = [
+ { name: '寮犱紵', sex: '鐢�', age: 68 },
+ { name: '鏉庡', sex: '濂�', age: 55 },
+ { name: '鐜嬪己', sex: '鐢�', age: 72 },
+ { name: '鍒樻晱', sex: '濂�', age: 61 },
+ { name: '闄堥潤', sex: '濂�', age: 66 },
+ { name: '鏉ㄥ啗', sex: '鐢�', age: 59 },
+ { name: '璧典附', sex: '濂�', age: 70 },
+ { name: '榛勫媷', sex: '鐢�', age: 64 },
+ { name: '鍛ㄨ壋', sex: '濂�', age: 58 },
+ { name: '鍚村垰', sex: '鐢�', age: 75 },
+];
+const TUBE_DIAGNOSES = ['鑴戝崚涓�', '鍚炲捊鍥伴毦', '椋熺鐧屾湳鍚�', '鏄忚糠', '閲嶇棁鑲虹値', '杩愬姩绁炵粡鍏冪梾', '棰呰剳鎹熶激', '甯曢噾妫梾', '鑴戝嚭琛�鎭㈠鏈�', '鑲岃悗缂╀晶绱㈢‖鍖�'];
+const DEPTS = ['绁炵粡鍐呯', '搴峰鍖诲绉�', '鑰佸勾绉�', '鍛煎惛鍐呯', '鏅绉�', '閲嶇棁鍖诲绉�', '鍐呭垎娉岀', '娑堝寲鍐呯'];
+const WARDS = ['涓�鐥呭尯', '浜岀梾鍖�', '涓夌梾鍖�', '鍥涚梾鍖�', '浜旂梾鍖�'];
+const DOCTORS = ['鐜嬩富浠�', '鏉庡尰鐢�', '寮犲尰鐢�', '鍒樺尰鐢�', '闄堝尰鐢�'];
+const TUBE_FEED_TYPES = ['榧昏儍绠�', '鑳冮�犵槝绠�', '绌鸿偁閫犵槝绠�', '榧昏偁绠�'];
+const TUBE_STATUS = ['绠¢ゲ涓�', '闂存瓏绠¢ゲ', '宸插仠鐢�'];
+const NUTRITIONS = ['鑲犲唴钀ュ吇涔冲墏(TPF)', '鑲犲唴钀ュ吇娣锋偓娑�(TPF-D)', '鑷埗鍖�娴嗚喅', '鐭偨鍨嬭惀鍏绘恫'];
+const SOURCE_LABEL = { archive: '鎮h�呮。妗�', hospital: '鍑洪櫌璁板綍', outpatient: '闂ㄨ瘖璁板綍', inpatient: '鍦ㄩ櫌璁板綍' };
+
+export default {
+ name: 'TubeFeedDedicated',
+ data() {
+ return {
+ loading: false,
+ // 涓撶梾鎮h�呭垪琛紙宸插姞鍏ワ級
+ patientList: [],
+ ids: [],
+ single: true,
+ multiple: true,
+ // 鏌ヨ鏉′欢
+ queryParams: { name: '', drname: '', deptWard: '', feedType: '', feedStatus: '' },
+ pageNum: 1,
+ pageSize: 10,
+ deptOptions: DEPTS,
+ wardOptions: WARDS,
+ TUBE_FEED_TYPES,
+ TUBE_STATUS,
+ // 娣诲姞寮圭獥
+ addVisible: false,
+ activeSource: 'archive',
+ poolKeyword: '',
+ addSelected: [],
+ mockPool: { archive: [], hospital: [], outpatient: [], inpatient: [] },
+ // 璇︽儏寮圭獥
+ detailVisible: false,
+ detailRow: null,
+ };
+ },
+ computed: {
+ filteredList() {
+ const { name, drname, deptWard, feedType, feedStatus } = this.queryParams;
+ return this.patientList.filter((r) => {
+ if (name && !r.name.includes(name)) return false;
+ if (drname && !r.drname.includes(drname)) return false;
+ if (deptWard) {
+ const label = deptWard.split(':')[1];
+ if (label && r.deptname !== label && r.wardname !== label) return false;
+ }
+ if (feedType && r.feedType !== feedType) return false;
+ if (feedStatus && r.feedStatus !== feedStatus) return false;
+ return true;
+ });
+ },
+ currentPageData() {
+ const start = (this.pageNum - 1) * this.pageSize;
+ return this.filteredList.slice(start, start + this.pageSize);
+ },
+ filteredPool() {
+ const kw = this.poolKeyword.trim();
+ const list = this.mockPool[this.activeSource] || [];
+ if (!kw) return list;
+ return list.filter((r) => r.name.includes(kw) || r.diagname.includes(kw));
+ },
+ },
+ created() {
+ this.buildPool();
+ this.buildInitialList();
+ },
+ methods: {
+ // 鐢熸垚鍚勬潵婧愮殑妯℃嫙鎮h�呮睜
+ buildPool() {
+ const mk = (idx, source, noPrefix) => {
+ const p = PERSONS[idx % PERSONS.length];
+ const isWard = source === 'hospital' || source === 'inpatient';
+ return {
+ id: noPrefix + (idx + 1),
+ name: p.name,
+ sex: p.sex,
+ age: p.age,
+ telcode: '138' + (10000000 + idx * 1111111),
+ deptname: DEPTS[idx % DEPTS.length],
+ wardname: isWard ? WARDS[idx % WARDS.length] : '',
+ drname: DOCTORS[idx % DOCTORS.length],
+ diagname: TUBE_DIAGNOSES[idx % TUBE_DIAGNOSES.length],
+ sourceDate: this.recentDate(idx * 3 + (source === 'outpatient' ? 2 : 5)),
+ };
+ };
+ this.mockPool = {
+ archive: PERSONS.map((_, i) => mk(i, 'archive', 'da')),
+ hospital: PERSONS.map((_, i) => mk(i, 'hospital', 'zy')),
+ outpatient: PERSONS.map((_, i) => mk(i, 'outpatient', 'mz')),
+ inpatient: PERSONS.map((_, i) => mk(i, 'inpatient', 'zy')),
+ };
+ },
+ // 棰勭疆鑻ュ共宸插姞鍏ョ殑鎮h�咃紝淇濊瘉棣栧睆鏈夋暟鎹�
+ buildInitialList() {
+ const take = (source, n) =>
+ this.mockPool[source].slice(0, n).map((row) => this.toDedicated(row, source));
+ this.patientList = [
+ ...take('hospital', 3),
+ ...take('inpatient', 2),
+ ...take('archive', 2),
+ ...take('outpatient', 1),
+ ];
+ },
+ // 姹犳偅鑰� => 涓撶梾鎮h�咃紙琛ュ厖绠¢ゲ鐩稿叧瀛楁锛�
+ toDedicated(row, source) {
+ const n = Number(String(row.id).replace(/\D/g, '')) || 0;
+ return {
+ id: row.id,
+ name: row.name,
+ sex: row.sex,
+ age: row.age,
+ telcode: row.telcode,
+ deptname: row.deptname,
+ wardname: row.wardname,
+ drname: row.drname,
+ diagname: row.diagname,
+ feedType: TUBE_FEED_TYPES[n % TUBE_FEED_TYPES.length],
+ feedStartDate: this.recentDate(n % 10),
+ nutrition: NUTRITIONS[n % NUTRITIONS.length],
+ energy: 1500 + (n % 5) * 100,
+ feedStatus: TUBE_STATUS[n % 2],
+ source: SOURCE_LABEL[source] || source,
+ addTime: this.now(),
+ };
+ },
+ handleQuery() {
+ this.pageNum = 1;
+ },
+ resetQuery() {
+ this.queryParams = { name: '', drname: '', deptWard: '', feedType: '', feedStatus: '' };
+ this.pageNum = 1;
+ },
+ handleSelectionChange(selection) {
+ this.ids = selection.map((item) => item.id);
+ this.single = selection.length !== 1;
+ this.multiple = !selection.length;
+ },
+ openAddDialog() {
+ this.addSelected = [];
+ this.poolKeyword = '';
+ this.activeSource = 'archive';
+ this.addVisible = true;
+ this.$nextTick(() => this.$refs.addTable && this.$refs.addTable.clearSelection());
+ },
+ onSourceTabChange() {
+ this.addSelected = [];
+ this.$nextTick(() => this.$refs.addTable && this.$refs.addTable.clearSelection());
+ },
+ handlePoolSearch() {
+ // 鏈湴杩囨护锛岀敱 filteredPool 璁$畻灞炴�у畬鎴�
+ },
+ onAddSelectionChange(selection) {
+ this.addSelected = selection;
+ },
+ confirmAdd() {
+ if (!this.addSelected.length) {
+ this.$modal.msgWarning('璇峰厛閫夋嫨瑕佹坊鍔犵殑鎮h��');
+ return;
+ }
+ const existing = new Set(this.patientList.map((r) => r.id));
+ const toAdd = this.addSelected
+ .filter((row) => !existing.has(row.id))
+ .map((row) => this.toDedicated(row, this.activeSource));
+ this.patientList = [...toAdd, ...this.patientList];
+ this.addVisible = false;
+ this.$modal.msgSuccess(`鎴愬姛娣诲姞 ${toAdd.length} 浣嶆偅鑰卄);
+ },
+ removeRow(row) {
+ this.$modal
+ .confirm(`鏄惁纭灏嗘偅鑰�"${row.name}"绉诲嚭绠¢ゲ涓撶梾鍒楄〃锛焋)
+ .then(() => {
+ this.patientList = this.patientList.filter((r) => r.id !== row.id);
+ this.$modal.msgSuccess('绉婚櫎鎴愬姛');
+ })
+ .catch(() => {});
+ },
+ handleDelete() {
+ this.$modal
+ .confirm(`鏄惁纭鍒犻櫎閫変腑鐨� ${this.ids.length} 浣嶆偅鑰咃紵`)
+ .then(() => {
+ this.patientList = this.patientList.filter((r) => !this.ids.includes(r.id));
+ this.$modal.msgSuccess('鍒犻櫎鎴愬姛');
+ })
+ .catch(() => {});
+ },
+ openDetail(row) {
+ this.detailRow = row;
+ this.detailVisible = true;
+ },
+ handleExport() {
+ const rows = this.filteredList;
+ if (!rows.length) {
+ this.$modal.msgWarning('鏆傛棤鏁版嵁鍙鍑�');
+ return;
+ }
+ const head = ['濮撳悕', '鎬у埆', '骞撮緞', '鑱旂郴鐢佃瘽', '绉戝', '鐥呭尯', '涓绘不鍖荤敓', '璇婃柇', '绠¢ゲ鏂瑰紡', '绠¢ゲ寮�濮嬫棩鏈�', '钀ュ吇鏂规', '姣忔棩鑳介噺(kcal)', '绠¢ゲ鐘舵��', '鏉ユ簮', '鍔犲叆鏃堕棿'];
+ const keys = ['name', 'sex', 'age', 'telcode', 'deptname', 'wardname', 'drname', 'diagname', 'feedType', 'feedStartDate', 'nutrition', 'energy', 'feedStatus', 'source', 'addTime'];
+ const csv = [head.join(',')]
+ .concat(rows.map((r) => keys.map((k) => `"${(r[k] ?? '').toString().replace(/"/g, '""')}"`).join(',')))
+ .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 = `绠¢ゲ涓撶梾鎮h�卂${Date.now()}.csv`;
+ link.click();
+ URL.revokeObjectURL(link.href);
+ },
+ recentDate(daysAgo) {
+ const d = new Date();
+ d.setDate(d.getDate() - daysAgo);
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
+ const dd = String(d.getDate()).padStart(2, '0');
+ return `${d.getFullYear()}-${mm}-${dd}`;
+ },
+ now() {
+ const d = new Date();
+ const pad = (x) => String(x).padStart(2, '0');
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.query-form { margin-bottom: 4px; }
+.mb8 { margin-bottom: 8px; }
+.pagination-wrap { margin-top: 16px; text-align: right; }
+.pool-tip { font-size: 12px; color: #909399; margin-left: 8px; }
+.danger-text { color: #f56c6c; }
+</style>
diff --git a/src/views/repositoryai/templateku/configurat/index.vue b/src/views/repositoryai/templateku/configurat/index.vue
index a34f016..c6c0700 100644
--- a/src/views/repositoryai/templateku/configurat/index.vue
+++ b/src/views/repositoryai/templateku/configurat/index.vue
@@ -533,7 +533,7 @@
<el-col :span="12"
><el-form-item label="鏄惁蹇呭~">
<el-select
- v-model="topicobj.isMust"
+ v-model="topicobj.ismandatory"
placeholder="榛樿蹇呭~"
>
<el-option
@@ -1086,7 +1086,7 @@
<el-col :span="12"
><el-form-item label="鏄惁蹇呭~">
<el-select
- v-model="indexform.isMust"
+ v-model="indexform.ismandatory"
placeholder="榛樿蹇呭~"
>
<el-option
diff --git "a/\351\232\217\350\256\277\345\244\226\351\223\276\351\200\232\347\224\2509.12.zip" "b/\351\232\217\350\256\277\345\244\226\351\223\276\351\200\232\347\224\2509.12.zip"
new file mode 100644
index 0000000..e05720c
--- /dev/null
+++ "b/\351\232\217\350\256\277\345\244\226\351\223\276\351\200\232\347\224\2509.12.zip"
Binary files differ
diff --git "a/\351\232\217\350\256\277\351\200\232\347\224\250 9.14.zip" "b/\351\232\217\350\256\277\351\200\232\347\224\250 9.14.zip"
new file mode 100644
index 0000000..87b3f17
--- /dev/null
+++ "b/\351\232\217\350\256\277\351\200\232\347\224\250 9.14.zip"
Binary files differ
--
Gitblit v1.9.3