| | |
| | | <el-card shadow="never" class="overview-card"> |
| | | <div slot="header" class="card-header"> |
| | | <span class="card-title">各服务项目满意度统计</span> |
| | | <span class="card-subtitle">(六档评分题:很满意/满意/一般/不满意/很不满意/未经历)</span> |
| | | <el-button |
| | | type="warning" |
| | | size="small" |
| | |
| | | <!-- 表格二:单选题/多选题统计(其余题目) --> |
| | | <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" |
| | |
| | | |
| | | <script> |
| | | import { sltdMydTotalByScore,statQuestionOptionByKsOrBq } from "@/api/AiCentre/satisfaction"; |
| | | import { deptTreeSelect } from "@/api/system/user"; |
| | | import XLSX from "xlsx"; |
| | | |
| | | // 满意度题的固定选项(顺序即表格列顺序)——这些选项的题目进表一,其余进表二 |
| | |
| | | // 服务类型默认选中项(按 label 匹配,适配不同字典的 value 差异) |
| | | const DEFAULT_SERVICE_LABELS = ["住院满意度", "门诊满意度"]; |
| | | |
| | | // 兜底:store 中无科室/病区数据时使用(表三对比 + 筛选下拉) |
| | | const DEPTS = ["内分泌科", "皮肤科", "肿瘤内科", "消化内科", "肾内科"]; |
| | | const WARDS = ["一病区", "二病区", "三病区", "四病区", "五病区"]; |
| | | // 注意:这里以前用 DEPTS / WARDS 两个「演示用」名称数组兜底, |
| | | // 结果被当成编码塞进 leavehospitaldistrictcodes / leaveldeptcodes, |
| | | // 后端按 leavehospitaldistrictcode IN ('一病区','二病区'…) 过滤 → 一条也匹配不上。 |
| | | // 科室/病区一律只用真实编码:有归属取 store,无归属(如管理员)取 deptTreeSelect 字典。 |
| | | |
| | | /** |
| | | * 接口数据 → 页面内部题目结构 |
| | |
| | | // 表三:科室/病区评分对比(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() { |
| | |
| | | } |
| | | 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() { |
| | |
| | | endFinishTime: range[1], |
| | | }; |
| | | if (this.queryParams.statMode === "dept") { |
| | | const deptCodes = depts.length |
| | | ? Array.from(new Set(depts)) |
| | | : this.defaultDeptCodes; |
| | | if (this.isInHospitalSatisfaction) { |
| | | payload.deptcodes = deptCodes; |
| | | } else { |
| | | payload.leaveldeptcodes = deptCodes; |
| | | const deptCodes = this.codeList(depts, this.defaultDeptCodes); |
| | | // 有编码才传;空数组等价于「全部」(后端 mapper 对空集合不拼条件) |
| | | if (deptCodes.length) { |
| | | if (this.isInHospitalSatisfaction) { |
| | | payload.deptcodes = deptCodes; |
| | | } else { |
| | | payload.leaveldeptcodes = deptCodes; |
| | | } |
| | | } |
| | | } else { |
| | | const wardCodes = wards.length |
| | | ? Array.from(new Set(wards)) |
| | | : this.defaultWardCodes; |
| | | if (this.isInHospitalSatisfaction) { |
| | | payload.hospitaldistrictcodes = wardCodes; |
| | | } else { |
| | | payload.leavehospitaldistrictcodes = wardCodes; |
| | | // 只传「用户真正选中的病区」:没选(= 全部病区)就不传,后端按不限处理。 |
| | | // 门诊满意度等任务的 leavehospitaldistrictcode 为空, |
| | | // 原来把「全部病区编码」传下去会让这些任务被 IN 条件整段过滤掉(25 题只剩 10 题)。 |
| | | const wardCodes = this.codeList(wards, []); |
| | | if (wardCodes.length) { |
| | | if (this.isInHospitalSatisfaction) { |
| | | payload.hospitaldistrictcodes = wardCodes; |
| | | } else { |
| | | payload.leavehospitaldistrictcodes = wardCodes; |
| | | } |
| | | } |
| | | } |
| | | // 服务类型多选:逗号分隔(接口字段为 serviceType 字符串) |
| | |
| | | try { |
| | | // 按科室统计、按病区统计各查一次,条件随「满意度类型」切换: |
| | | // 在院满意度 => deptcodes / hospitaldistrictcodes;离院满意度 => leaveldeptcodes / leavehospitaldistrictcodes |
| | | const deptParam = this.isInHospitalSatisfaction |
| | | ? { deptcodes: this.defaultDeptCodes } |
| | | : { leaveldeptcodes: this.defaultDeptCodes }; |
| | | const wardParam = this.isInHospitalSatisfaction |
| | | ? { hospitaldistrictcodes: this.defaultWardCodes } |
| | | : { leavehospitaldistrictcodes: this.defaultWardCodes }; |
| | | // 没有真实编码时不传该字段(= 全部),绝不把名称当编码传 |
| | | 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, ...deptParam }), |
| | | statQuestionOptionByKsOrBq({ ...base, ...wardParam }), |
| | |
| | | exportTable(tableId) { |
| | | const tableNames = { |
| | | table1: "各服务项目满意度统计", |
| | | table2: "单选题多选题统计", |
| | | table2: "其他题目统计", |
| | | table3: "各科室满意度评分对比", |
| | | }; |
| | | const name = tableNames[tableId] || "统计数据"; |
| | |
| | | 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; |