| | |
| | | <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> |
| | | |
| | |
| | | <el-card shadow="never" class="chart-card"> |
| | | <div slot="header" class="card-header"> |
| | | <span>æ¶é´ç»´åº¦ · é¨è¯æ£è
满æåº¦è¶å¿</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> |
| | |
| | | <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> |
| | |
| | | </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> |
| | | |
| | |
| | | <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> |
| | |
| | | </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>ç§å®¤ç»´åº¦ · é¨è¯æ£è
满æåº¦å¾åæå</span> |
| | | <span>{{ rankDimLabel }}维度 · é¨è¯æ£è
满æåº¦å¾åæå</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> |
| | |
| | | <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: 'é¨è¯æ£è
满æåº¦' }, |
| | | 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 = ['第ä¸å£åº¦', '第äºå£åº¦', '第ä¸å£åº¦', '第åå£åº¦']; |
| | | 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: 'é¨è¯æ£è
满æåº¦', |
| | | 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: 'é¨è¯æ£è
满æåº¦', 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' }, |
| | |
| | | } |
| | | } |
| | | |
| | | // å
éå
å®¹å¹¶æ¿æ¢å¾è¡¨ä¸ºå¾ç |
| | | 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) => { |
| | |
| | | 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} |
| | |
| | | </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> |
| | |
| | | <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> |
| | |
| | | </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" /> |
| | |
| | | <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> |
| | |
| | | <script> |
| | | import XLSX from 'xlsx'; |
| | | |
| | | // 满æåº¦é¢çåºå®é项ï¼é¡ºåºå³è¡¨æ ¼å顺åºï¼ |
| | | const FIXED_OPTIONS = ['é常满æ', '满æ', 'ä¸è¬', '䏿»¡æ', 'ä¸éç¨']; |
| | | const DEPTS = ['å
åæ³ç§', 'ç®è¤ç§', 'è¿ç¤å
ç§', 'æ¶åå
ç§', 'è¾å
ç§']; |
| | | const WARDS = ['ä¸ç
åº', 'äºç
åº', 'ä¸ç
åº', 'åç
åº', 'äºç
åº']; |
| | | |
| | | // 满æåº¦é¢ï¼åºå®äºé¡¹é项ï¼counts 顺åºå¯¹åº 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]], |
| | | ['å»çæ¯å¦ç¨æ¨å¬å¾æçæ¹å¼è§£éé®é¢ï¼', [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]], |
| | | ['æ¤å£«æ¯å¦ç¨æ¨å¬å¾æçæ¹å¼è§£éé®é¢ï¼', [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: "å»çæ¯å¦ç¨æ¨å¬å¾æçæ¹å¼è§£éé®é¢ï¼", 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: "æ¤å£«æ¯å¦ç¨æ¨å¬å¾æçæ¹å¼è§£éé®é¢ï¼", 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; |
| | | }, |
| | | // æåæ°æ®ï¼å½åä¸ºæ¨¡ææ°æ®ï¼æ¥å£å°±ç»ªåæ¿æ¢ä¸ºçå®è¯·æ±ï¼ |
| | | 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] || 'ç»è®¡æ°æ®'; |
| | |
| | | // ä»è¡¨æ ¼æåæ°æ®æå»ºå¸¦æ ·å¼ç 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); |
| | |
| | | } 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(); |
| | | } |
| | | }, |
| | | }, |
| | |
| | | |
| | | <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; } |
| | |
| | | </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"> |
| | | å·²éä¸ |
| | |
| | | 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) { |
| | |
| | | ::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; |
| | | |
| | |
| | | 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 |
| | | > |
| | |
| | | </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 |
| | | > |
| | |
| | | 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 |
| | | > |
| | |
| | | // ç¼å |
| | | cache(subm) { |
| | | console.log("è¿å
¥ç¼å"); |
| | | // æäº¤åºæ¯ï¼å
åå¿
å¡«é¡¹æ ¡éª |
| | | if (subm === true && !this.validateRequired()) { |
| | | return; |
| | | } |
| | | let form = { |
| | | param1: this.taskid, |
| | | param2: this.patid, |
| | |
| | | } |
| | | } |
| | | }); |
| | | }, |
| | | // æ¯å¦å¿
å¡«ï¼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) { |
| | |
| | | } |
| | | .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; |
| | |
| | | item.prompt && |
| | | item.scriptResult && |
| | | (item.scriptType !== 2 || item.scriptResult.length > 0), |
| | | 'required-error': item.requiredError, |
| | | }" |
| | | > |
| | | <!-- é¢ç®é¢å¹² --> |
| | |
| | | <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 |
| | |
| | | item.prompt && |
| | | item.scriptResult && |
| | | (item.scriptType !== 2 || item.scriptResult.length > 0), |
| | | 'required-error': item.requiredError, |
| | | }" |
| | | > |
| | | <!-- é¢ç®é¢å¹² --> |
| | |
| | | <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 |
| | |
| | | // ç¼å |
| | | cache(subm) { |
| | | console.log("è¿å
¥ç¼å"); |
| | | // æäº¤åºæ¯ï¼å
åå¿
å¡«é¡¹æ ¡éª |
| | | if (subm === true && !this.validateRequired()) { |
| | | return; |
| | | } |
| | | // æäº¤åºæ¯ï¼å¼å¯å è½½ç¶æï¼é²æ¢éå¤ç¹å» |
| | | if (subm === true) { |
| | | this.submitting = true; |
| | |
| | | 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; |
| | | }, |
| | | // è·åå¯è§é¢ç®çæ£ç¡®åºå·ï¼è§£å³è·³é¢ååºå·ä¸è¿ç»çé®é¢ï¼ |
| | | getVisibleQuestionIndex(index) { |
| | |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | &.required-error { |
| | | border-color: #f56c6c; |
| | | border-left: 4px solid #f56c6c; |
| | | background: #fef0f0; |
| | | } |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | |
| | | min-width: 16px; |
| | | } |
| | | |
| | | .required-star { |
| | | color: #f56c6c; |
| | | margin-right: 2px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | // .question-text { |
| | | // line-height: 1.5; |
| | | // color: #2c3e50; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- æ¥è¯¢æ --> |
| | | <el-form :inline="true" :model="queryParams" size="small" class="query-form"> |
| | | <el-form-item label="æ£è
å§å"> |
| | | <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">æ·»å æ£è
</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]" |
| | | /> |
| | | |
| | | <!-- æ·»å æ£è
å¼¹çª --> |
| | | <el-dialog title="æ·»å ä¸ç
æ£è
" :visible.sync="addVisible" width="80%" top="6vh"> |
| | | <el-tabs v-model="activeSource" @tab-click="onSourceTabChange"> |
| | | <el-tab-pane label="æ£è
æ¡£æ¡" 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="ä¸ç
æ£è
详æ
" :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="æ£è
æ¥æº">{{ detailRow.source }}</el-descriptions-item> |
| | | <el-descriptions-item label="å å
¥æ¶é´">{{ detailRow.addTime }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | // å¿èä¸ç
æ£è
ââ æ¨¡ææ°æ®ï¼æ¥å£å°±ç»ªåæ¿æ¢ 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 = ['å å¿ç
', 'å¿åè¡°ç«', 'é«è¡å', 'å¿æ¿é¢¤å¨', 'å¿èæ¢æ»', 'å¿å¾å¤±å¸¸', 'å¿èç£èç
', 'æ©å¼ åå¿èç
', 'è¥ååå¿èç
', 'æ¥æ§å è综åå¾']; |
| | | const DEPTS = ['å¿è¡ç®¡å
ç§', 'èå¹´ç§', 'éçå»å¦ç§', '康å¤å»å¦ç§', 'å¿å¤ç§', 'å
åæ³ç§', 'ç¥ç»å
ç§', 'å¼å¸å
ç§']; |
| | | const WARDS = ['ä¸ç
åº', 'äºç
åº', 'ä¸ç
åº', 'åç
åº', 'äºç
åº']; |
| | | const DOCTORS = ['ç主任', 'æå»ç', 'å¼ å»ç', 'åå»ç', 'éå»ç']; |
| | | const NYHA_CLASSES = ['â
级', 'â
¡çº§', 'â
¢çº§', 'â
£çº§']; |
| | | const FOLLOW_PLANS = ['æ¯å¨', 'æ¯ä¸¤å¨', 'æ¯æ', 'æ¯å£åº¦']; |
| | | const SOURCE_LABEL = { archive: 'æ£è
æ¡£æ¡', hospital: 'åºé¢è®°å½', outpatient: 'é¨è¯è®°å½', inpatient: 'å¨é¢è®°å½' }; |
| | | |
| | | export default { |
| | | name: 'HeartDedicated', |
| | | data() { |
| | | return { |
| | | loading: false, |
| | | // ä¸ç
æ£è
å表ï¼å·²å å
¥ï¼ |
| | | 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: { |
| | | // çæåæ¥æºçæ¨¡ææ£è
æ± |
| | | 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')), |
| | | }; |
| | | }, |
| | | // é¢ç½®è¥å¹²å·²å å
¥çæ£è
ï¼ä¿è¯é¦å±ææ°æ® |
| | | 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), |
| | | ]; |
| | | }, |
| | | // æ± æ£è
=> ä¸ç
æ£è
ï¼è¡¥å
å¿èç¸å
³åæ®µï¼ |
| | | 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', 'â
£çº§': '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('请å
éæ©è¦æ·»å çæ£è
'); |
| | | 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 = `å¿èä¸ç
æ£è
_${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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- æ¥è¯¢æ --> |
| | | <el-form :inline="true" :model="queryParams" size="small" class="query-form"> |
| | | <el-form-item label="æ£è
å§å"> |
| | | <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">æ·»å æ£è
</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]" |
| | | /> |
| | | |
| | | <!-- æ·»å æ£è
å¼¹çª --> |
| | | <el-dialog title="æ·»å ä¸ç
æ£è
" :visible.sync="addVisible" width="80%" top="6vh"> |
| | | <el-tabs v-model="activeSource" @tab-click="onSourceTabChange"> |
| | | <el-tab-pane label="æ£è
æ¡£æ¡" 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="ä¸ç
æ£è
详æ
" :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="æ£è
æ¥æº">{{ detailRow.source }}</el-descriptions-item> |
| | | <el-descriptions-item label="å å
¥æ¶é´">{{ detailRow.addTime }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | // 管饲ä¸ç
æ£è
ââ æ¨¡ææ°æ®ï¼æ¥å£å°±ç»ªåæ¿æ¢ 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: 'æ£è
æ¡£æ¡', hospital: 'åºé¢è®°å½', outpatient: 'é¨è¯è®°å½', inpatient: 'å¨é¢è®°å½' }; |
| | | |
| | | export default { |
| | | name: 'TubeFeedDedicated', |
| | | data() { |
| | | return { |
| | | loading: false, |
| | | // ä¸ç
æ£è
å表ï¼å·²å å
¥ï¼ |
| | | 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: { |
| | | // çæåæ¥æºçæ¨¡ææ£è
æ± |
| | | 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')), |
| | | }; |
| | | }, |
| | | // é¢ç½®è¥å¹²å·²å å
¥çæ£è
ï¼ä¿è¯é¦å±ææ°æ® |
| | | 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), |
| | | ]; |
| | | }, |
| | | // æ± æ£è
=> ä¸ç
æ£è
ï¼è¡¥å
管饲ç¸å
³åæ®µï¼ |
| | | 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('请å
éæ©è¦æ·»å çæ£è
'); |
| | | 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 = `管饲ä¸ç
æ£è
_${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> |
| | |
| | | <el-col :span="12" |
| | | ><el-form-item label="æ¯å¦å¿
å¡«"> |
| | | <el-select |
| | | v-model="topicobj.isMust" |
| | | v-model="topicobj.ismandatory" |
| | | placeholder="é»è®¤å¿
å¡«" |
| | | > |
| | | <el-option |
| | |
| | | <el-col :span="12" |
| | | ><el-form-item label="æ¯å¦å¿
å¡«"> |
| | | <el-select |
| | | v-model="indexform.isMust" |
| | | v-model="indexform.ismandatory" |
| | | placeholder="é»è®¤å¿
å¡«" |
| | | > |
| | | <el-option |