| | |
| | | <!-- 顶部筛选:时间范围 + 科室/病区,控制本页所有数据 --> |
| | | <el-card shadow="never" class="overview-card filter-card"> |
| | | <el-form inline class="filter-form"> |
| | | <el-form-item label="满意度类型"> |
| | | <el-select |
| | | v-model="satisfactionScope" |
| | | size="small" |
| | | style="width: 140px" |
| | | clearable |
| | | placeholder="默认离院" |
| | | @change="fetchData" |
| | | > |
| | | <el-option label="离院满意度" value="leave" /> |
| | | <el-option label="在院满意度" value="hospital" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="出院日期"> |
| | | <el-date-picker |
| | | v-model="queryParams.dateRange" |
| | |
| | | <el-button size="small" icon="el-icon-refresh" @click="handleReset" |
| | | >重置</el-button |
| | | > |
| | | <el-button |
| | | type="warning" |
| | | size="small" |
| | | icon="el-icon-download" |
| | | @click="exportTable()" |
| | | >导出Excel</el-button |
| | | > |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | |
| | | <el-card shadow="never" class="overview-card"> |
| | | <div slot="header" class="card-header"> |
| | | <span class="card-title">各服务项目满意度统计</span> |
| | | <el-button |
| | | type="warning" |
| | | size="small" |
| | | icon="el-icon-download" |
| | | @click="exportTable('table1')" |
| | | >导出Excel</el-button |
| | | > |
| | | <span class="card-subtitle">(六档评分题:很满意/满意/一般/不满意/很不满意/未经历)</span> |
| | | </div> |
| | | <el-table |
| | | id="table1" |
| | |
| | | <!-- 表格二:单选题/多选题统计(其余题目) --> |
| | | <el-card shadow="never" class="overview-card"> |
| | | <div slot="header" class="card-header"> |
| | | <span class="card-title">单选题/多选题统计</span> |
| | | <span class="card-title">其他题目统计</span> |
| | | <span class="card-subtitle">(六档满意度题已在上表,这里只列其余题型:推荐、抱怨回应、意见等)</span> |
| | | <div class="header-right"> |
| | | <el-input |
| | | v-model="questionSearch" |
| | |
| | | placeholder="按题目模糊查询" |
| | | class="search-input" |
| | | /> |
| | | <el-button |
| | | type="warning" |
| | | size="small" |
| | | icon="el-icon-download" |
| | | @click="exportTable('table2')" |
| | | >导出Excel</el-button |
| | | > |
| | | </div> |
| | | </div> |
| | | <div id="table2" v-loading="loading" element-loading-text="统计中..."> |
| | |
| | | /> |
| | | </template> |
| | | </el-select> |
| | | <el-button |
| | | type="warning" |
| | | size="small" |
| | | icon="el-icon-download" |
| | | @click="exportTable('table3')" |
| | | >导出Excel</el-button |
| | | > |
| | | </div> |
| | | </div> |
| | | <el-table |
| | |
| | | |
| | | <script> |
| | | import { sltdMydTotalByScore,statQuestionOptionByKsOrBq } from "@/api/AiCentre/satisfaction"; |
| | | import XLSX from "xlsx"; |
| | | import { deptTreeSelect } from "@/api/system/user"; |
| | | import * as XLSX from "xlsx"; |
| | | |
| | | // 满意度题的固定选项(顺序即表格列顺序)——这些选项的题目进表一,其余进表二 |
| | | const FIXED_OPTIONS = ["很满意", "满意", "一般", "不满意", "很不满意", "未经历"]; |
| | |
| | | // 服务类型默认选中项(按 label 匹配,适配不同字典的 value 差异) |
| | | const DEFAULT_SERVICE_LABELS = ["住院满意度", "门诊满意度"]; |
| | | |
| | | // 兜底:store 中无科室/病区数据时使用(表三对比 + 筛选下拉) |
| | | const DEPTS = ["内分泌科", "皮肤科", "肿瘤内科", "消化内科", "肾内科"]; |
| | | const WARDS = ["一病区", "二病区", "三病区", "四病区", "五病区"]; |
| | | // 注意:这里以前用 DEPTS / WARDS 两个「演示用」名称数组兜底, |
| | | // 结果被当成编码塞进 leavehospitaldistrictcodes / leaveldeptcodes, |
| | | // 后端按 leavehospitaldistrictcode IN ('一病区','二病区'…) 过滤 → 一条也匹配不上。 |
| | | // 科室/病区一律只用真实编码:有归属取 store,无归属(如管理员)取 deptTreeSelect 字典。 |
| | | |
| | | /** |
| | | * 接口数据 → 页面内部题目结构 |
| | |
| | | const details = pickDetails(item); |
| | | let node = map.get(title); |
| | | if (!node) { |
| | | node = { title, type: "", options: new Map() }; |
| | | node = { title, type: "", options: new Map(), avgScore: null }; |
| | | map.set(title, node); |
| | | } |
| | | // 综合得分:后端返回的题目级 avgScore(与题目同级) |
| | | if (node.avgScore == null) { |
| | | const rawAvg = Number(item && item.avgScore); |
| | | node.avgScore = isNaN(rawAvg) ? null : rawAvg; |
| | | } |
| | | |
| | | details.forEach((d) => { |
| | |
| | | |
| | | return Array.from(map.values()).map((n) => { |
| | | const options = Array.from(n.options.values()); |
| | | const item = { title: n.title, type: n.type, options }; |
| | | const item = { title: n.title, type: n.type, options, avgScore: n.avgScore }; |
| | | // 多选题:用各选项数量之和作为填报总量(接口未单独提供人次) |
| | | if (n.type === "multiple") |
| | | item.total = options.reduce((s, o) => s + o.count, 0); |
| | |
| | | name: "DataOverview", |
| | | data() { |
| | | return { |
| | | // 满意度类型:leave=离院满意度(离院科室/病区),hospital=在院满意度(在院科室/病区) |
| | | satisfactionScope: "leave", |
| | | // 顶部全局筛选 |
| | | queryParams: { |
| | | dateRange: [], |
| | | deptCodes: [], // 科室编码集合 => leaveldeptcodes |
| | | wardCodes: [], // 出院病区编码集合 => leavehospitaldistrictcodes(接口必填) |
| | | // 病区编码集合:住院满意度按在院病区(hospitaldistrictcodes),其余按出院病区(leavehospitaldistrictcodes) |
| | | wardCodes: [], |
| | | serviceTypeList: [], // 服务类型多选,提交时用逗号拼接 |
| | | deptOrDistrict: "1", // 科室/病区组合方式,默认 1:离院病区 and 离院科室 |
| | | statMode: "ward", // 统计维度:ward=按病区统计 dept=按科室统计 |
| | |
| | | // 表三:科室/病区评分对比(rows=行数据,questions=动态列) |
| | | deptComparison: { rows: [], questions: [] }, |
| | | wardComparison: { rows: [], questions: [] }, |
| | | |
| | | // 科室/病区字典:当前账号没有归属科室/病区时(如管理员)从 deptTreeSelect 拉取, |
| | | // 保证下拉里放的是真实编码(deptCode / districtCode),而不是名称 |
| | | deptDict: [], |
| | | wardDict: [], |
| | | }; |
| | | }, |
| | | computed: { |
| | | // 科室选项:优先取当前用户所属科室,无则兜底 |
| | | // 科室选项:优先取当前用户所属科室;没有归属时用科室字典(真实 deptCode) |
| | | deptOptions() { |
| | | const list = this.$store.getters.belongDepts || []; |
| | | return list.length |
| | | ? list.map((d) => ({ label: d.deptName, value: d.deptCode })) |
| | | : DEPTS.map((d) => ({ label: d, value: d })); |
| | | const list = (this.$store.getters.belongDepts || []) |
| | | .map((d) => ({ label: d.deptName, value: d.deptCode })) |
| | | .filter((d) => d.label && d.value); |
| | | return list.length ? list : this.deptDict; |
| | | }, |
| | | // 病区选项:优先取当前用户所属病区,无则兜底 |
| | | // 病区选项:优先取当前用户所属病区;没有归属时用病区字典(真实 districtCode) |
| | | wardOptions() { |
| | | const list = this.$store.getters.belongWards || []; |
| | | return list.length |
| | | ? list.map((w) => ({ label: w.districtName, value: w.districtCode })) |
| | | : WARDS.map((w) => ({ label: w, value: w })); |
| | | const list = (this.$store.getters.belongWards || []) |
| | | .map((w) => ({ label: w.districtName, value: w.districtCode })) |
| | | .filter((w) => w.label && w.value); |
| | | return list.length ? list : this.wardDict; |
| | | }, |
| | | // 服务类型下拉:取 store 的随访类型字典 tasktypes,取不到时兜底本地 SERVICE_TYPES |
| | | serviceTypeOptions() { |
| | |
| | | const taskTypes = toOptions(this.$store.getters.tasktypes); |
| | | return taskTypes.length ? taskTypes : SERVICE_TYPES; |
| | | }, |
| | | // 未选病区时的默认查询范围:全部病区编码 |
| | | // 未选病区时的默认查询范围:全部病区编码(空数组 = 不限,后端不拼过滤条件) |
| | | defaultWardCodes() { |
| | | return Array.from(new Set(this.wardOptions.map((w) => w.value))); |
| | | return Array.from( |
| | | new Set(this.wardOptions.map((w) => w.value).filter(Boolean)) |
| | | ); |
| | | }, |
| | | // 未选科室时的默认查询范围:全部科室编码 |
| | | // 是否按「在院满意度」口径查询:在院满意度用 deptcodes/hospitaldistrictcodes, |
| | | // 离院满意度(以及清空下拉时的默认口径)用 leaveldeptcodes/leavehospitaldistrictcodes |
| | | isInHospitalSatisfaction() { |
| | | return this.satisfactionScope === "hospital"; |
| | | }, |
| | | // 未选科室时的默认查询范围:全部科室编码(空数组 = 不限) |
| | | defaultDeptCodes() { |
| | | return Array.from(new Set(this.deptOptions.map((d) => d.value))); |
| | | return Array.from( |
| | | new Set(this.deptOptions.map((d) => d.value).filter(Boolean)) |
| | | ); |
| | | }, |
| | | // 固定六项选项的题目 => 表一 |
| | | satisfactionQuestions() { |
| | |
| | | veryBadRate: this.rateOf(veryBad.count, total), |
| | | naCount: na.count, |
| | | naRate: this.rateOf(na.count, total), |
| | | score: this.scoreOfItems(items), |
| | | total, |
| | | score: this.formatScore(q.avgScore), |
| | | }; |
| | | }); |
| | | |
| | |
| | | }); |
| | | }); |
| | | const total = sum.reduce((s, it) => s + it.count, 0); |
| | | // 总计综合得分:按各题填报量加权平均各题 avgScore |
| | | let totalScoreNum = 0; |
| | | let totalScoreDen = 0; |
| | | this.satisfactionQuestions.forEach((q, i) => { |
| | | const avg = Number(q.avgScore); |
| | | const qTotal = rows[i] ? rows[i].total : 0; |
| | | if (!isNaN(avg) && qTotal) { |
| | | totalScoreNum += avg * qTotal; |
| | | totalScoreDen += qTotal; |
| | | } |
| | | }); |
| | | rows.push({ |
| | | category: "总计", |
| | | veryGoodCount: sum[0].count, |
| | |
| | | veryBadRate: this.rateOf(sum[4].count, total), |
| | | naCount: sum[5].count, |
| | | naRate: this.rateOf(sum[5].count, total), |
| | | score: this.scoreOfItems(sum), |
| | | score: totalScoreDen ? (totalScoreNum / totalScoreDen).toFixed(2) : "0.00", |
| | | }); |
| | | return rows; |
| | | }, |
| | |
| | | } |
| | | this.queryParams.serviceTypeList = this.defaultServiceTypes(); |
| | | }, |
| | | mounted() { |
| | | async mounted() { |
| | | // 先把科室/病区字典补齐,保证首次查询用的就是真实编码 |
| | | await this.ensureOrgDict(); |
| | | this.fetchData(); |
| | | }, |
| | | methods: { |
| | |
| | | return item ? item.value : null; |
| | | }).filter((v) => v != null); |
| | | return Array.from(new Set(hit)); |
| | | }, |
| | | // 归一化科室/病区编码:选中项优先,其次默认全部;去重、去空 |
| | | // 返回空数组表示「不限」(后端 mapper 用 size() > 0 判断,空集合不拼过滤条件) |
| | | codeList(selected, fallback) { |
| | | const list = (selected && selected.length ? selected : fallback) || []; |
| | | return Array.from( |
| | | new Set( |
| | | list.filter((v) => v !== null && v !== undefined && String(v).trim() !== "") |
| | | ) |
| | | ); |
| | | }, |
| | | // 补全科室/病区字典:仅当 store 里没有归属数据时(典型是管理员账号)才请求 |
| | | async ensureOrgDict() { |
| | | const tasks = []; |
| | | if (!(this.$store.getters.belongDepts || []).length) { |
| | | tasks.push(this.loadOrgDict(1)); |
| | | } |
| | | if (!(this.$store.getters.belongWards || []).length) { |
| | | tasks.push(this.loadOrgDict(2)); |
| | | } |
| | | if (tasks.length) await Promise.all(tasks); |
| | | }, |
| | | // deptType:1=科室,2=病区;结果既作下拉选项,也作「全部」时的编码来源 |
| | | async loadOrgDict(deptType) { |
| | | let flat = []; |
| | | try { |
| | | const res = await deptTreeSelect(deptType); |
| | | const walk = (nodes) => { |
| | | (Array.isArray(nodes) ? nodes : []).forEach((n) => { |
| | | // deptTreeSelect 返回 TreeSelect:{ id, label, deptCode, children } |
| | | // 只认 deptCode,缺失就跳过——用 deptId 冒充编码同样查不到数据 |
| | | if (n.label && n.deptCode) { |
| | | flat.push({ label: n.label, value: String(n.deptCode) }); |
| | | } |
| | | if (n.children && n.children.length) walk(n.children); |
| | | }); |
| | | }; |
| | | walk(res && res.data); |
| | | } catch (e) { |
| | | console.warn("[DataOverview] 获取科室/病区字典失败,将按“全部”查询", e); |
| | | } |
| | | if (deptType === 1) { |
| | | this.deptDict = flat; |
| | | } else { |
| | | this.wardDict = flat; |
| | | } |
| | | }, |
| | | // 表三独立条件:'inherit' 时沿用顶部所选;返回选中的名称数组(接口只回名称),空数组表示不限 |
| | | statFilterNames() { |
| | |
| | | if (!denominator) return "0.00"; |
| | | return (numerator / denominator).toFixed(2); |
| | | }, |
| | | // 综合得分展示:直接使用后端 avgScore,格式化为两位小数 |
| | | formatScore(val) { |
| | | const n = Number(val); |
| | | return isNaN(n) ? "0.00" : n.toFixed(2); |
| | | }, |
| | | defaultRange() { |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | |
| | | return; |
| | | } |
| | | |
| | | // 病区 => leavehospitaldistrictcodes,科室 => leaveldeptcodes |
| | | // 科室/病区互斥:按病区只传 leavehospitaldistrictcodes,按科室只传 leaveldeptcodes |
| | | // 科室/病区条件随「满意度类型」切换: |
| | | // 在院满意度 => deptcodes / hospitaldistrictcodes(在院科室、在院病区) |
| | | // 离院满意度 => leaveldeptcodes / leavehospitaldistrictcodes(离院科室、离院病区) |
| | | // 科室/病区互斥:按病区只传病区编码,按科室只传科室编码 |
| | | const wards = this.queryParams.wardCodes || []; |
| | | const depts = this.queryParams.deptCodes || []; |
| | | |
| | |
| | | endFinishTime: range[1], |
| | | }; |
| | | if (this.queryParams.statMode === "dept") { |
| | | payload.leaveldeptcodes = depts.length |
| | | ? Array.from(new Set(depts)) |
| | | : this.defaultDeptCodes; |
| | | const deptCodes = this.codeList(depts, this.defaultDeptCodes); |
| | | // 有编码才传;空数组等价于「全部」(后端 mapper 对空集合不拼条件) |
| | | if (deptCodes.length) { |
| | | if (this.isInHospitalSatisfaction) { |
| | | payload.deptcodes = deptCodes; |
| | | } else { |
| | | payload.leaveldeptcodes = deptCodes; |
| | | } |
| | | } |
| | | } else { |
| | | payload.leavehospitaldistrictcodes = wards.length |
| | | ? Array.from(new Set(wards)) |
| | | : this.defaultWardCodes; |
| | | // 只传「用户真正选中的病区」:没选(= 全部病区)就不传,后端按不限处理。 |
| | | // 门诊满意度等任务的 leavehospitaldistrictcode 为空, |
| | | // 原来把「全部病区编码」传下去会让这些任务被 IN 条件整段过滤掉(25 题只剩 10 题)。 |
| | | const wardCodes = this.codeList(wards, []); |
| | | if (wardCodes.length) { |
| | | if (this.isInHospitalSatisfaction) { |
| | | payload.hospitaldistrictcodes = wardCodes; |
| | | } else { |
| | | payload.leavehospitaldistrictcodes = wardCodes; |
| | | } |
| | | } |
| | | } |
| | | // 服务类型多选:逗号分隔(接口字段为 serviceType 字符串) |
| | | const serviceTypes = this.queryParams.serviceTypeList || []; |
| | |
| | | endFinishTime: range[1], |
| | | }; |
| | | try { |
| | | // 按科室统计(leaveldeptcodes 优先)与按病区统计(leavehospitaldistrictcodes)各查一次 |
| | | // 按科室统计、按病区统计各查一次,条件随「满意度类型」切换: |
| | | // 在院满意度 => deptcodes / hospitaldistrictcodes;离院满意度 => leaveldeptcodes / leavehospitaldistrictcodes |
| | | // 没有真实编码时不传该字段(= 全部),绝不把名称当编码传 |
| | | const deptParam = {}; |
| | | const deptCodes = this.codeList([], this.defaultDeptCodes); |
| | | if (deptCodes.length) { |
| | | if (this.isInHospitalSatisfaction) { |
| | | deptParam.deptcodes = deptCodes; |
| | | } else { |
| | | deptParam.leaveldeptcodes = deptCodes; |
| | | } |
| | | } |
| | | const wardParam = {}; |
| | | const wardCodes = this.codeList([], this.defaultWardCodes); |
| | | if (wardCodes.length) { |
| | | if (this.isInHospitalSatisfaction) { |
| | | wardParam.hospitaldistrictcodes = wardCodes; |
| | | } else { |
| | | wardParam.leavehospitaldistrictcodes = wardCodes; |
| | | } |
| | | } |
| | | const [deptRes, wardRes] = await Promise.all([ |
| | | statQuestionOptionByKsOrBq({ ...base, leaveldeptcodes: this.defaultDeptCodes }), |
| | | statQuestionOptionByKsOrBq({ ...base, leavehospitaldistrictcodes: this.defaultWardCodes }), |
| | | statQuestionOptionByKsOrBq({ ...base, ...deptParam }), |
| | | statQuestionOptionByKsOrBq({ ...base, ...wardParam }), |
| | | ]); |
| | | // 排查用:打印接口原始返回,便于核对两级嵌套 Map 结构(确认无误后可删除) |
| | | console.log( |
| | |
| | | const hit = opts.find((o) => String(o.label) === String(name)); |
| | | return hit ? hit.value : name; |
| | | }, |
| | | // 导出 Excel:一次导出整页三个表(各一个 sheet),和页面显示保持一致 |
| | | exportTable(tableId) { |
| | | const tableNames = { |
| | | table1: "各服务项目满意度统计", |
| | | table2: "单选题多选题统计", |
| | | table3: "各科室满意度评分对比", |
| | | }; |
| | | const name = tableNames[tableId] || "统计数据"; |
| | | const table = document.getElementById(tableId); |
| | | if (!table) return; |
| | | |
| | | try { |
| | | // 从表格提取数据构建带样式的 Excel |
| | | const order = ["table1", "table2", "table3"]; |
| | | if (tableId && order.indexOf(tableId) > 0) { |
| | | order.splice(order.indexOf(tableId), 1); |
| | | order.unshift(tableId); |
| | | } |
| | | const builders = { |
| | | table1: () => this.buildExportTable1(), |
| | | table2: () => this.buildExportTable2(), |
| | | table3: () => this.buildExportTable3(), |
| | | }; |
| | | const wb = XLSX.utils.book_new(); |
| | | const data = []; |
| | | table.querySelectorAll("tr").forEach((tr) => { |
| | | const row = []; |
| | | tr.querySelectorAll("th,td").forEach((td) => { |
| | | row.push((td.innerText || "").trim()); |
| | | }); |
| | | if (row.length) data.push(row); |
| | | let count = 0; |
| | | order.forEach((id) => { |
| | | const sheet = builders[id](); |
| | | if (!sheet) return; |
| | | const ws = XLSX.utils.aoa_to_sheet(sheet.data); |
| | | ws["!merges"] = sheet.merges || []; |
| | | const widthRow = sheet.data.find((r) => r && r.length) || []; |
| | | ws["!cols"] = widthRow.map((_, i) => ({ wch: i === 0 ? 32 : 12 })); |
| | | XLSX.utils.book_append_sheet(wb, ws, sheet.name); |
| | | count++; |
| | | }); |
| | | |
| | | const ws = XLSX.utils.aoa_to_sheet(data); |
| | | // 设置列宽 |
| | | const colWidths = data[0] ? data[0].map(() => ({ wch: 16 })) : []; |
| | | ws["!cols"] = colWidths; |
| | | |
| | | XLSX.utils.book_append_sheet(wb, ws, "Sheet1"); |
| | | XLSX.writeFile(wb, name + ".xlsx"); |
| | | if (!count) { |
| | | this.$message.warning("没有可导出的数据"); |
| | | return; |
| | | } |
| | | XLSX.writeFile(wb, "满意度数据总览.xlsx"); |
| | | this.$message.success("导出成功"); |
| | | } catch (e) { |
| | | // fallback: CSV |
| | | let csv = ""; |
| | | table.querySelectorAll("tr").forEach((tr) => { |
| | | const row = []; |
| | | tr.querySelectorAll("th,td").forEach((td) => |
| | | row.push('"' + (td.innerText || "").replace(/"/g, '""') + '"') |
| | | ); |
| | | csv += row.join(",") + "\n"; |
| | | }); |
| | | 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(); |
| | | console.error("导出失败:", e); |
| | | this.$message.error("导出失败:" + (e.message || e)); |
| | | } |
| | | }, |
| | | |
| | | // 表一:各服务项目满意度统计 |
| | | buildExportTable1() { |
| | | const rows = this.serviceSatisfaction || []; |
| | | if (!rows.length) return null; |
| | | const data = [ |
| | | ["各服务项目满意度统计(六档评分题:很满意/满意/一般/不满意/很不满意/未经历)"], |
| | | [ |
| | | "分类/题目", |
| | | "很满意", "", "满意", "", "一般", "", |
| | | "不满意", "", "很不满意", "", "未经历", "", |
| | | "综合得分", |
| | | ], |
| | | [ |
| | | "", |
| | | "数量", "比例", "数量", "比例", "数量", "比例", |
| | | "数量", "比例", "数量", "比例", "数量", "比例", |
| | | "", |
| | | ], |
| | | ]; |
| | | rows.forEach((r) => { |
| | | data.push([ |
| | | r.category, |
| | | r.veryGoodCount, r.veryGoodRate, |
| | | r.goodCount, r.goodRate, |
| | | r.normalCount, r.normalRate, |
| | | r.badCount, r.badRate, |
| | | r.veryBadCount, r.veryBadRate, |
| | | r.naCount, r.naRate, |
| | | r.score, |
| | | ]); |
| | | }); |
| | | const merges = [ |
| | | { s: { r: 0, c: 0 }, e: { r: 0, c: 13 } }, |
| | | { s: { r: 1, c: 0 }, e: { r: 2, c: 0 } }, |
| | | ]; |
| | | for (let i = 0; i < 6; i++) { |
| | | merges.push({ s: { r: 1, c: 1 + i * 2 }, e: { r: 1, c: 2 + i * 2 } }); |
| | | } |
| | | merges.push({ s: { r: 1, c: 13 }, e: { r: 2, c: 13 } }); |
| | | return { name: "各服务项目满意度", data, merges }; |
| | | }, |
| | | |
| | | // 表二:其他题目统计(每题一块:题目标题 → 选项/数量/比例 → 本题填报总量) |
| | | buildExportTable2() { |
| | | const list = this.filteredQuestionStats || []; |
| | | if (!list.length) return null; |
| | | const data = [ |
| | | ["其他题目统计(六档满意度题已在上表,这里只列其余题型:推荐、抱怨回应、意见等)"], |
| | | [], |
| | | ]; |
| | | const merges = [{ s: { r: 0, c: 0 }, e: { r: 0, c: 2 } }]; |
| | | list.forEach((q) => { |
| | | data.push([q.title, "", ""]); |
| | | merges.push({ |
| | | s: { r: data.length - 1, c: 0 }, |
| | | e: { r: data.length - 1, c: 2 }, |
| | | }); |
| | | data.push(["选项", "数量", "比例"]); |
| | | const opts = |
| | | q.options && q.options.length |
| | | ? q.options |
| | | : [{ option: "(无选项)", count: 0, rate: "-" }]; |
| | | opts.forEach((o) => { |
| | | data.push([o.option, o.count, o.rate]); |
| | | }); |
| | | data.push([`本题填报总量:${q.total}`, "", ""]); |
| | | merges.push({ |
| | | s: { r: data.length - 1, c: 0 }, |
| | | e: { r: data.length - 1, c: 2 }, |
| | | }); |
| | | data.push([]); |
| | | }); |
| | | return { name: "其他题目统计", data, merges }; |
| | | }, |
| | | |
| | | // 表三:各科室/病区满意度评分对比(两行动态表头) |
| | | buildExportTable3() { |
| | | const rows = this.table3Rows || []; |
| | | const questions = this.table3Questions || []; |
| | | if (!rows.length) return null; |
| | | const head1 = [this.statDimension === "dept" ? "科室" : "病区"]; |
| | | const head2 = [""]; |
| | | const groups = []; |
| | | questions.forEach((q) => { |
| | | const opts = q.options || []; |
| | | const startCol = head1.length; |
| | | head1.push(q.title); |
| | | for (let i = 1; i < opts.length + 1; i++) head1.push(""); |
| | | groups.push({ start: startCol, end: startCol + opts.length }); |
| | | opts.forEach((opt) => head2.push(opt)); |
| | | head2.push("得分"); |
| | | }); |
| | | head1.push("综合得分"); |
| | | head2.push(""); |
| | | const data = [["各科室/病区满意度评分对比"], head1, head2]; |
| | | const merges = [ |
| | | { s: { r: 0, c: 0 }, e: { r: 0, c: head1.length - 1 } }, |
| | | { s: { r: 1, c: 0 }, e: { r: 2, c: 0 } }, |
| | | { |
| | | s: { r: 1, c: head1.length - 1 }, |
| | | e: { r: 2, c: head1.length - 1 }, |
| | | }, |
| | | ]; |
| | | groups.forEach((g) => { |
| | | if (g.end > g.start) { |
| | | merges.push({ s: { r: 1, c: g.start }, e: { r: 1, c: g.end } }); |
| | | } |
| | | }); |
| | | rows.forEach((row) => { |
| | | const line = [row.name]; |
| | | questions.forEach((q) => { |
| | | const cell = (row.cells && row.cells[q.title]) || {}; |
| | | (q.options || []).forEach((opt) => { |
| | | line.push((cell[opt] && cell[opt].count) || 0); |
| | | }); |
| | | line.push(cell._score || "0.00"); |
| | | }); |
| | | line.push(row.totalScore); |
| | | data.push(line); |
| | | }); |
| | | return { name: "各科室病区评分对比", data, merges }; |
| | | }, |
| | | }, |
| | | }; |
| | |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | .card-subtitle { |
| | | margin-left: 6px; |
| | | font-size: 12px; |
| | | font-weight: 400; |
| | | color: #909399; |
| | | } |
| | | .header-right { |
| | | display: flex; |
| | | align-items: center; |