<template>
|
<div class="followup-statistics">
|
<div class="query-section">
|
<el-form
|
:model="queryParams"
|
ref="queryForm"
|
size="medium"
|
:inline="true"
|
label-width="100px"
|
class="query-form"
|
>
|
<el-form-item
|
v-if="isTongdeOrg"
|
label="满意度类型"
|
prop="satisfactionScope"
|
>
|
<el-select
|
v-model="satisfactionScope"
|
placeholder="默认在院"
|
clearable
|
style="width: 150px"
|
@change="handleSatisfactionScopeChange"
|
>
|
<el-option label="离院满意度" value="leave" />
|
<el-option label="在院满意度" value="hospital" />
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="统计类型" prop="statisticaltype">
|
<el-select
|
v-model="queryParams.statisticaltype"
|
placeholder="请选择统计类型"
|
clearable
|
@change="handleStatisticalTypeChange"
|
>
|
<el-option
|
v-for="item in Statisticallist"
|
:key="item.value"
|
:label="item.label"
|
:value="item.value"
|
/>
|
</el-select>
|
</el-form-item>
|
|
<!-- 病区选择 -->
|
<el-form-item
|
v-if="queryParams.statisticaltype == 1"
|
label="病区"
|
prop="leavehospitaldistrictcodes"
|
>
|
<el-select
|
v-model="queryParams.leavehospitaldistrictcodes"
|
placeholder="请选择病区"
|
multiple
|
collapse-tags
|
filterable
|
clearable
|
style="width: 300px"
|
>
|
<el-option
|
v-for="item in flatArrayhospit"
|
:key="item.value"
|
:label="item.label"
|
:value="item.value"
|
/>
|
</el-select>
|
</el-form-item>
|
|
<!-- 科室选择 -->
|
<el-form-item
|
v-if="queryParams.statisticaltype == 2"
|
label="科室"
|
prop="deptcodes"
|
>
|
<el-select
|
v-model="queryParams.deptcodes"
|
placeholder="请选择科室"
|
multiple
|
collapse-tags
|
filterable
|
clearable
|
style="width: 300px"
|
>
|
<el-option
|
v-for="item in flatArraydept"
|
:key="item.value"
|
:label="item.label"
|
:value="item.value"
|
/>
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="服务类型" prop="serviceType">
|
<el-select
|
v-model="queryParams.serviceType"
|
placeholder="请选择服务类型"
|
multiple
|
collapse-tags
|
clearable
|
style="width: 300px"
|
>
|
<el-option
|
v-for="item in options"
|
:key="item.value"
|
:label="item.label"
|
:value="item.value"
|
/>
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="随访时间" prop="dateRange">
|
<el-date-picker
|
v-model="queryParams.dateRange"
|
type="daterange"
|
range-separator="至"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
value-format="yyyy-MM-dd"
|
:picker-options="pickerOptions"
|
style="width: 380px"
|
/>
|
</el-form-item>
|
|
<el-form-item>
|
<el-button
|
type="primary"
|
icon="el-icon-search"
|
@click="handleQuery"
|
:loading="loading"
|
>
|
搜索
|
</el-button>
|
<el-button icon="el-icon-refresh" @click="resetQuery">
|
重置
|
</el-button>
|
<el-button
|
type="warning"
|
icon="el-icon-download"
|
@click="handleExport"
|
:disabled="!userList.length"
|
>
|
导出
|
</el-button>
|
</el-form-item>
|
</el-form>
|
</div>
|
|
<div class="table-section">
|
<el-table
|
v-loading="loading"
|
:data="userList"
|
:border="true"
|
style="width: 100%"
|
@selection-change="handleSelectionChange"
|
:row-key="getRowKey"
|
show-summary
|
:summary-method="getSummaries"
|
>
|
<!-- 病区统计:在院病区 + 离院病区 -->
|
<el-table-column
|
v-if="queryParams.statisticaltype == 1"
|
label="在院病区"
|
align="center"
|
sortable
|
key="hospitaldistrictname"
|
prop="hospitaldistrictname"
|
:show-overflow-tooltip="true"
|
:sort-method="sortHospitalWard"
|
min-width="120"
|
>
|
<template slot-scope="scope">
|
{{ scope.row.hospitaldistrictname || "-" }}
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
v-if="queryParams.statisticaltype == 1"
|
label="离院病区"
|
align="center"
|
sortable
|
key="leavehospitaldistrictname"
|
prop="leavehospitaldistrictname"
|
:show-overflow-tooltip="true"
|
:sort-method="sortLeaveWard"
|
min-width="120"
|
>
|
<template slot-scope="scope">
|
{{ scope.row.leavehospitaldistrictname || "-" }}
|
</template>
|
</el-table-column>
|
|
<!-- 科室统计:在院科室 + 离院科室 -->
|
<el-table-column
|
v-if="queryParams.statisticaltype == 2"
|
label="在院科室"
|
align="center"
|
key="hospitaldeptname"
|
prop="hospitaldeptname"
|
:show-overflow-tooltip="true"
|
min-width="120"
|
>
|
<template slot-scope="scope">
|
{{ scope.row.hospitaldeptname || "-" }}
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
v-if="queryParams.statisticaltype == 2"
|
label="离院科室"
|
align="center"
|
key="deptname"
|
prop="deptname"
|
:show-overflow-tooltip="true"
|
min-width="120"
|
>
|
<template slot-scope="scope">
|
{{ scope.row.deptname || "-" }}
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="出院人次"
|
align="center"
|
key="dischargeCount"
|
prop="dischargeCount"
|
min-width="100"
|
/>
|
|
<el-table-column
|
label="无需随访人次"
|
align="center"
|
key="nonFollowUp"
|
prop="nonFollowUp"
|
min-width="120"
|
/>
|
|
<el-table-column
|
label="应随访人次"
|
align="center"
|
key="followUpNeeded"
|
prop="followUpNeeded"
|
min-width="120"
|
/>
|
|
<el-table-column
|
label="随访率"
|
align="center"
|
key="followUpRate"
|
prop="followUpRate"
|
min-width="100"
|
>
|
<template slot-scope="scope">
|
<span
|
v-if="
|
scope.row.followUpRate !== null &&
|
scope.row.followUpRate !== undefined
|
"
|
>
|
{{ scope.row.followUpRate }}
|
</span>
|
<span v-else>-</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
v-if="orgname == '景宁畲族自治县人民医院'"
|
label="及时率"
|
align="center"
|
key="rate"
|
prop="rate"
|
min-width="100"
|
>
|
<template slot-scope="scope">
|
<el-button
|
v-if="scope.row.rate !== null && scope.row.rate !== undefined"
|
type="text"
|
@click="handleSeedetails(scope.row)"
|
>
|
{{ formatPercent(scope.row.rate) }}
|
</el-button>
|
<span v-else style="color: #909399">-</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="满意度题目总量"
|
align="center"
|
key="joyAllCount"
|
prop="joyAllCount"
|
min-width="140"
|
/>
|
|
<el-table-column
|
label="满意度填报量"
|
align="center"
|
key="joyCount"
|
prop="joyCount"
|
min-width="120"
|
/>
|
|
<el-table-column
|
label="完成比率"
|
align="center"
|
key="joyTotal"
|
prop="joyTotal"
|
min-width="100"
|
>
|
<template slot-scope="scope">
|
<span
|
v-if="
|
scope.row.joyTotal !== null && scope.row.joyTotal !== undefined
|
"
|
>
|
{{ formatPercent(scope.row.joyTotal) }}
|
</span>
|
<span v-else>-</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="操作" align="center" fixed="right" width="120">
|
<template slot-scope="scope">
|
<el-button type="text" @click="getinfo(scope.row)">
|
<i class="el-icon-s-order" style="margin-right: 4px"></i>
|
查看详情
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
|
<!-- 分页 -->
|
<div class="pagination-section" v-if="total > 0">
|
<el-pagination
|
background
|
layout="total, sizes, prev, pager, next, jumper"
|
:current-page="queryParams.pageNum"
|
:page-size="queryParams.pageSize"
|
:page-sizes="[10, 20, 30, 50]"
|
:total="total"
|
@size-change="handleSizeChange"
|
@current-change="handlePageChange"
|
/>
|
</div>
|
|
<!-- 未及时随访详情对话框 -->
|
<el-dialog
|
title="未及时随访患者服务"
|
:visible.sync="SeedetailsVisible"
|
width="80%"
|
:close-on-click-modal="false"
|
>
|
<SeedetailsDialog
|
v-if="SeedetailsVisible"
|
:row-data="currentRow"
|
:query-params="queryParams"
|
@close="SeedetailsVisible = false"
|
/>
|
</el-dialog>
|
|
<!-- 满意度详情对话框 -->
|
<el-dialog
|
:visible.sync="topicVisible"
|
width="60%"
|
:close-on-click-modal="false"
|
>
|
<template #title>
|
<div style="display: flex; align-items: center">
|
<i
|
class="el-icon-s-data"
|
style="margin-right: 8px; color: #409eff"
|
></i>
|
<span>{{ topicvalue.name }}</span>
|
<span style="margin-left: 10px; color: #666; font-size: 14px"
|
>满意度指标详情</span
|
>
|
</div>
|
</template>
|
<topic-dialog
|
v-if="topicVisible"
|
:row-data="currentRow"
|
:topicList="topiclist"
|
:query-params="queryParams"
|
:topType="2"
|
@close="topicVisible = false"
|
/>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script>
|
import {
|
getSfStatisticsJoy,
|
getSfStatisticsJoyInfo,
|
selectTimelyRate,
|
} from "@/api/system/user";
|
import ExcelJS from "exceljs";
|
import { saveAs } from "file-saver";
|
import { getTaskservelist } from "@/api/AiCentre/SingleTask";
|
import SeedetailsDialog from "./components/SeedetailsDialog.vue";
|
import TopicDialog from "./components/TopicDialog.vue";
|
|
export default {
|
name: "FollowupStatistics",
|
components: {
|
SeedetailsDialog,
|
TopicDialog,
|
},
|
props: {
|
// 是否在顶部生成患者表单 / 问题详情分析 tab(仅满意度统计主页开启)
|
showAnalysisTabs: {
|
type: Boolean,
|
default: false,
|
},
|
},
|
data() {
|
return {
|
// 满意度类型:leave=离院满意度(按出院病区统计),hospital=在院满意度(按在院病区统计)
|
// 默认离院满意度
|
satisfactionScope: "leave",
|
|
// 查询参数
|
queryParams: {
|
statisticaltype: 1,
|
leavehospitaldistrictcodes: ["all"],
|
deptcodes: [],
|
// 默认服务类型:6=住院满意度(出入院满意度)
|
serviceType: [6],
|
dateRange: [],
|
pageNum: 1,
|
pageSize: 20,
|
},
|
|
// 统计类型列表
|
Statisticallist: [
|
{ label: "病区统计", value: 1 },
|
{ label: "科室统计", value: 2 },
|
],
|
|
// 病区列表
|
flatArrayhospit: [],
|
|
// 科室列表
|
flatArraydept: [],
|
|
// 服务类型选项
|
options: [
|
{
|
value: 6,
|
label: "出入院满意度",
|
raw: {
|
cssClass: "",
|
listClass: "",
|
},
|
},
|
|
{
|
value: 14,
|
label: "门诊满意度",
|
raw: {
|
cssClass: "",
|
listClass: "",
|
},
|
},
|
],
|
|
// 表格数据
|
userList: [],
|
|
// 总条数
|
total: 0,
|
|
// 加载状态
|
loading: false,
|
orgname: "",
|
// 已完成明细请求序号,避免快速切换筛选时旧请求覆盖新结果
|
completedRequestSeq: 0,
|
// 问题详情请求序号
|
questionRequestSeq: 0,
|
|
// 选中的行
|
ids: [],
|
single: true,
|
multiple: true,
|
|
// 当前操作的行
|
currentRow: null,
|
|
// 对话框显示控制
|
SeedetailsVisible: false,
|
topicVisible: false,
|
|
// 满意度详情数据
|
topiclist: [],
|
topicvalue: {
|
name: "",
|
},
|
|
// 日期选择器选项
|
pickerOptions: {
|
shortcuts: [
|
{
|
text: "最近一周",
|
onClick(picker) {
|
const end = new Date();
|
const start = new Date();
|
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
|
picker.$emit("pick", [start, end]);
|
},
|
},
|
{
|
text: "最近一个月",
|
onClick(picker) {
|
const end = new Date();
|
const start = new Date();
|
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
|
picker.$emit("pick", [start, end]);
|
},
|
},
|
{
|
text: "最近三个月",
|
onClick(picker) {
|
const end = new Date();
|
const start = new Date();
|
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
|
picker.$emit("pick", [start, end]);
|
},
|
},
|
],
|
disabledDate(time) {
|
return time.getTime() > Date.now();
|
},
|
},
|
};
|
},
|
|
computed: {
|
// 是否按「在院满意度」口径统计:在院用 hospitaldistrictcodes / hospitaldeptcodes,
|
// 离院用 leavehospitaldistrictcodes / leaveldeptcodes
|
// 默认在院,所以清空下拉时仍按在院口径处理
|
isInHospitalSatisfaction() {
|
return this.satisfactionScope !== "leave";
|
},
|
// patItem 接口的 serviceType 只接收单个值,多选时取第一个
|
currentServiceType() {
|
const value = this.queryParams.serviceType;
|
const first = Array.isArray(value) ? value[0] : value;
|
return first || 6;
|
},
|
},
|
|
created() {
|
this.orgname = localStorage.getItem("orgname");
|
this.queryParams.serviceType = this.isTongdeOrg ? [6] : [2];
|
this.initData();
|
// 顶部患者表单 / 问题详情分析 tab:默认按当前统计口径加载一次
|
this.maybeRefreshCompletedGroups();
|
this.maybeRefreshQuestionDetails();
|
},
|
|
methods: {
|
// 初始化数据
|
async initData() {
|
await this.getDeptTree();
|
// await this.getList();
|
},
|
|
// 获取科室树
|
getDeptTree() {
|
// 获取服务类型(字典里的「住院满意度」在本页显示为「出入院满意度」)
|
const labelFix = { 住院满意度: "出入院满意度" };
|
this.options = (this.$store.getters.tasktypes || []).map((o) =>
|
labelFix[o.label] ? { ...o, label: labelFix[o.label] } : o
|
);
|
console.log(11, this.options);
|
|
// 获取科室列表
|
this.flatArraydept = (this.$store.getters.belongDepts || []).map(
|
(dept) => {
|
return {
|
label: dept.deptName,
|
value: dept.deptCode,
|
};
|
}
|
);
|
|
// 获取病区列表
|
this.flatArrayhospit = (this.$store.getters.belongWards || []).map(
|
(ward) => {
|
return {
|
label: ward.districtName,
|
value: ward.districtCode,
|
};
|
}
|
);
|
|
// 添加全部选项
|
this.flatArraydept.push({ label: "全部", value: "all" });
|
this.flatArrayhospit.push({ label: "全部", value: "all" });
|
},
|
|
// 获取统计列表
|
async getList() {
|
this.loading = true;
|
try {
|
// 处理查询参数
|
const params = {
|
configKey: "joyCount",
|
...this.queryParams,
|
};
|
|
// 处理日期范围
|
if (
|
this.queryParams.dateRange &&
|
this.queryParams.dateRange.length === 2
|
) {
|
params.startTime = this.queryParams.dateRange[0];
|
params.endTime = this.queryParams.dateRange[1];
|
}
|
|
// 处理病区/科室选择:只保留当前统计类型 + 当前满意度类型对应的字段,
|
// 避免在院/离院两套条件同时拼进 SQL
|
if (params.statisticaltype == 1) {
|
// 病区统计
|
let wardCodes = params.leavehospitaldistrictcodes || [];
|
if (wardCodes.includes("all")) {
|
// 选择"全部"时不加病区限制,查全部病区
|
wardCodes = [];
|
}
|
delete params.leavehospitaldistrictcodes;
|
delete params.hospitaldistrictcodes;
|
delete params.deptcodes;
|
delete params.leaveldeptcodes;
|
delete params.hospitaldeptcodes;
|
if (wardCodes.length) {
|
if (this.isInHospitalSatisfaction) {
|
params.hospitaldistrictcodes = wardCodes;
|
} else {
|
params.leavehospitaldistrictcodes = wardCodes;
|
}
|
}
|
} else if (params.statisticaltype == 2) {
|
// 科室统计
|
let deptCodes = params.deptcodes || [];
|
if (deptCodes.includes("all")) {
|
// 选择"全部"时不加科室限制,查全部科室
|
deptCodes = [];
|
} else {
|
deptCodes = deptCodes.filter((item) => item !== "all");
|
}
|
delete params.deptcodes;
|
delete params.leaveldeptcodes;
|
delete params.hospitaldeptcodes;
|
delete params.leavehospitaldistrictcodes;
|
delete params.hospitaldistrictcodes;
|
if (deptCodes.length) {
|
if (this.isInHospitalSatisfaction) {
|
params.hospitaldeptcodes = deptCodes;
|
} else {
|
params.leaveldeptcodes = deptCodes;
|
}
|
}
|
}
|
|
const response = await getSfStatisticsJoy(params);
|
// 科室统计按接口返回顺序,病区统计保持原有的中文序号排序
|
this.userList =
|
this.queryParams.statisticaltype == 1
|
? this.customSort(response.data) || []
|
: response.data || [];
|
this.total = response.total || 0;
|
} catch (error) {
|
console.error("获取统计列表失败:", error);
|
this.$message.error("获取数据失败");
|
} finally {
|
this.loading = false;
|
}
|
},
|
sortChineseNumber(aRow, bRow) {
|
return this.compareWardNames(
|
this.getWardName(aRow),
|
this.getWardName(bRow)
|
);
|
},
|
sortHospitalWard(aRow, bRow) {
|
return this.compareWardNames(
|
aRow && aRow.hospitaldistrictname,
|
bRow && bRow.hospitaldistrictname
|
);
|
},
|
sortLeaveWard(aRow, bRow) {
|
return this.compareWardNames(
|
aRow && aRow.leavehospitaldistrictname,
|
bRow && bRow.leavehospitaldistrictname
|
);
|
},
|
// 比较两个病区名称的中文序号;无序号时按字符串比较
|
compareWardNames(a, b) {
|
// 中文数字到阿拉伯数字的映射(扩展到45)
|
const chineseNumMap = {
|
一: 1,
|
二: 2,
|
三: 3,
|
四: 4,
|
五: 5,
|
六: 6,
|
七: 7,
|
八: 8,
|
九: 9,
|
十: 10,
|
十一: 11,
|
十二: 12,
|
十三: 13,
|
十四: 14,
|
十五: 15,
|
十六: 16,
|
十七: 17,
|
十八: 18,
|
十九: 19,
|
二十: 20,
|
二十一: 21,
|
二十二: 22,
|
二十三: 23,
|
二十四: 24,
|
二十五: 25,
|
二十六: 26,
|
二十七: 27,
|
二十八: 28,
|
二十九: 29,
|
三十: 30,
|
三十一: 31,
|
三十二: 32,
|
三十三: 33,
|
三十四: 34,
|
三十五: 35,
|
三十六: 36,
|
三十七: 37,
|
三十八: 38,
|
三十九: 39,
|
四十: 40,
|
四十一: 41,
|
四十二: 42,
|
四十三: 43,
|
四十四: 44,
|
四十五: 45,
|
};
|
|
// 提取中文数字
|
const getNumberFromText = (text) => {
|
if (!text || typeof text !== "string") return -1;
|
|
// 匹配中文数字,支持一到四十五
|
const match = text.match(/^([一二三四五六七八九十]+)/);
|
|
if (match && match[1]) {
|
const chineseNum = match[1];
|
return chineseNumMap[chineseNum] !== undefined
|
? chineseNumMap[chineseNum]
|
: -1;
|
}
|
|
// 如果没有匹配到中文数字,尝试匹配阿拉伯数字
|
const arabicMatch = text.match(/^(\d+)/);
|
if (arabicMatch && arabicMatch[1]) {
|
const num = parseInt(arabicMatch[1], 10);
|
return num >= 1 && num <= 45 ? num : -1;
|
}
|
|
return -1;
|
};
|
|
const numA = getNumberFromText(a);
|
const numB = getNumberFromText(b);
|
|
// 处理无法解析的情况
|
if (numA === -1 && numB === -1) {
|
return (a || "").localeCompare(b || "");
|
}
|
if (numA === -1) return 1;
|
if (numB === -1) return -1;
|
|
return numA - numB;
|
},
|
customSort(data) {
|
// 定义您期望的病区顺序(扩展到四十五)
|
const order = [
|
"一",
|
"二",
|
"三",
|
"四",
|
"五",
|
"六",
|
"七",
|
"八",
|
"九",
|
"十",
|
"十一",
|
"十二",
|
"十三",
|
"十四",
|
"十五",
|
"十六",
|
"十七",
|
"十八",
|
"十九",
|
"二十",
|
"二十一",
|
"二十二",
|
"二十三",
|
"二十四",
|
"二十五",
|
"二十六",
|
"二十七",
|
"二十八",
|
"二十九",
|
"三十",
|
"三十一",
|
"三十二",
|
"三十三",
|
"三十四",
|
"三十五",
|
"三十六",
|
"三十七",
|
"三十八",
|
"三十九",
|
"四十",
|
"四十一",
|
"四十二",
|
"四十三",
|
"四十四",
|
"四十五",
|
];
|
|
return data.sort((a, b) => {
|
// 提取病区名称中的中文数字部分
|
const getIndex = (name) => {
|
if (!name || typeof name !== "string") return -1;
|
|
// 匹配中文数字
|
const chineseMatch = name.match(/^([一二三四五六七八九十]+)/);
|
if (chineseMatch && chineseMatch[1]) {
|
return order.indexOf(chineseMatch[1]);
|
}
|
|
// 匹配阿拉伯数字
|
const arabicMatch = name.match(/^(\d+)/);
|
if (arabicMatch && arabicMatch[1]) {
|
const num = parseInt(arabicMatch[1], 10);
|
if (num >= 1 && num <= 45) {
|
return num - 1; // 因为数组索引从0开始
|
}
|
}
|
|
return -1;
|
};
|
|
const indexA = getIndex(this.getWardName(a));
|
const indexB = getIndex(this.getWardName(b));
|
|
// 排序逻辑
|
if (indexA === -1 && indexB === -1) {
|
return this.getWardName(a).localeCompare(this.getWardName(b));
|
}
|
if (indexA === -1) return 1;
|
if (indexB === -1) return -1;
|
return indexA - indexB;
|
});
|
},
|
// 处理统计类型变化
|
handleStatisticalTypeChange(value) {
|
if (value === 1) {
|
this.queryParams.deptcodes = [];
|
} else {
|
this.queryParams.leavehospitaldistrictcodes = [];
|
}
|
this.queryParams.pageNum = 1;
|
this.getList();
|
this.maybeRefreshCompletedGroups();
|
this.maybeRefreshQuestionDetails();
|
},
|
|
// 处理查询
|
handleQuery() {
|
this.queryParams.pageNum = 1;
|
this.getList();
|
this.maybeRefreshCompletedGroups();
|
this.maybeRefreshQuestionDetails();
|
},
|
|
// 切换满意度类型(离院/在院)后重新查询
|
handleSatisfactionScopeChange() {
|
this.queryParams.pageNum = 1;
|
this.getList();
|
this.maybeRefreshCompletedGroups();
|
this.maybeRefreshQuestionDetails();
|
},
|
|
/**
|
* 顶部「已完成患者明细」tab 数据
|
* 维度沿用下方「统计类型」:病区统计 -> 按病区分组;科室统计 -> 按科室分组
|
* 时间沿用下方「随访时间」,状态固定为已完成
|
*/
|
maybeRefreshCompletedGroups() {
|
if (this.showAnalysisTabs) {
|
this.refreshCompletedGroups();
|
}
|
},
|
|
async refreshCompletedGroups() {
|
const seq = ++this.completedRequestSeq;
|
const payload = this.buildCompletedPayload();
|
|
// 没有科室/病区权限时不发起请求,避免把全量数据查出来
|
if (!payload) {
|
this.emitCompletedGroups([], seq);
|
return;
|
}
|
|
this.$emit("completed-rows-change", {
|
...this.buildCompletedMeta(),
|
loading: true,
|
});
|
|
try {
|
const res = await getTaskservelist(payload);
|
if (seq !== this.completedRequestSeq) return;
|
|
const rows = this.pickCompletedRows(res);
|
this.emitCompletedGroups(rows, seq);
|
} catch (error) {
|
if (seq !== this.completedRequestSeq) return;
|
console.error("获取已完成患者明细失败:", error);
|
this.emitCompletedGroups([], seq);
|
}
|
},
|
|
// 组装第一张「满意度状态查询」页同款查询条件
|
buildCompletedPayload() {
|
const scope = this.buildCompletedScope();
|
if (!scope.all && !scope.depts.length && !scope.wards.length) {
|
return null;
|
}
|
|
const payload = {
|
pageNum: null,
|
pageSize: null,
|
sendstate: null,
|
sendstateView: 2, // 2 = 已完成
|
sort: 2,
|
serviceType: this.currentServiceType,
|
searchscope: scope.searchscope,
|
visitCount: 1,
|
scopetype: [],
|
// 与第一张图一致:选了具体科室/病区时只传对应维度;「全部」时两个维度都传
|
deptOrDistrict: scope.deptOrDistrict,
|
};
|
|
// 在院满意度用 hospital*,离院满意度用 leave*
|
if (this.satisfactionScope === "hospital") {
|
if (scope.depts.length) {
|
payload.deptcodes = scope.depts;
|
}
|
if (scope.wards.length) {
|
payload.hospitaldistrictcodes = scope.wards;
|
}
|
} else {
|
if (scope.depts.length) {
|
payload.leaveldeptcodes = scope.depts;
|
}
|
if (scope.wards.length) {
|
payload.leavehospitaldistrictcodes = scope.wards;
|
}
|
}
|
|
const range = this.queryParams.dateRange || [];
|
if (range.length === 2 && range[0] && range[1]) {
|
payload.finishtimeStart = range[0];
|
payload.finishtimeEnd = range[1];
|
}
|
|
return payload;
|
},
|
|
// 组装查询范围:统计类型决定按病区还是科室过滤
|
buildCompletedScope() {
|
const allDepts = (this.$store.getters.belongDepts || [])
|
.map((dept) => dept.deptCode)
|
.filter(Boolean);
|
const allWards = (this.$store.getters.belongWards || [])
|
.map((ward) => ward.districtCode)
|
.filter(Boolean);
|
|
// 病区统计(含统计类型为空的情况)
|
if (this.queryParams.statisticaltype != 2) {
|
const rawSelected = this.queryParams.leavehospitaldistrictcodes || [];
|
const selected = rawSelected.filter((code) => code && code !== "all");
|
const useAll = !rawSelected.length || rawSelected.includes("all");
|
|
if (!useAll && selected.length) {
|
return {
|
depts: [],
|
wards: selected,
|
deptOrDistrict: 1,
|
searchscope: 2,
|
};
|
}
|
// 全部:不传病区/科室编码,由后端返回当前机构全部数据
|
return {
|
depts: [],
|
wards: [],
|
deptOrDistrict: 2,
|
searchscope: 3,
|
all: true,
|
};
|
}
|
|
// 科室统计
|
const rawSelected = this.queryParams.deptcodes || [];
|
const selected = rawSelected.filter((code) => code && code !== "all");
|
const useAll = !rawSelected.length || rawSelected.includes("all");
|
if (!useAll && selected.length) {
|
return {
|
depts: selected,
|
wards: [],
|
deptOrDistrict: 1,
|
searchscope: 1,
|
};
|
}
|
return {
|
depts: [],
|
wards: [],
|
deptOrDistrict: 2,
|
searchscope: 3,
|
all: true,
|
};
|
},
|
|
// 问题详情占比 / 图表 / 饼图 共用数据
|
maybeRefreshQuestionDetails() {
|
if (this.showAnalysisTabs) {
|
this.refreshQuestionDetails();
|
}
|
},
|
|
async refreshQuestionDetails() {
|
const seq = ++this.questionRequestSeq;
|
const payload = this.buildQuestionPayload();
|
|
if (!payload) {
|
this.$emit("question-details-change", { loading: false, list: [] });
|
return;
|
}
|
|
this.$emit("question-details-change", { loading: true });
|
|
try {
|
const res = await getSfStatisticsJoyInfo(payload);
|
if (seq !== this.questionRequestSeq) return;
|
|
this.$emit("question-details-change", {
|
loading: false,
|
list: res.data || [],
|
});
|
} catch (error) {
|
if (seq !== this.questionRequestSeq) return;
|
console.error("获取问题详情占比失败:", error);
|
this.$emit("question-details-change", { loading: false, list: [] });
|
}
|
},
|
|
buildQuestionPayload() {
|
const scope = this.buildCompletedScope();
|
if (!scope.all && !scope.depts.length && !scope.wards.length) {
|
return null;
|
}
|
|
const params = {
|
configKey: "joyCount",
|
statisticaltype: this.queryParams.statisticaltype,
|
serviceType: this.queryParams.serviceType,
|
searchscope: scope.searchscope,
|
pageNum: 1,
|
pageSize: 999,
|
};
|
|
const range = this.queryParams.dateRange || [];
|
if (range.length === 2 && range[0] && range[1]) {
|
params.startTime = range[0];
|
params.endTime = range[1];
|
}
|
|
if (this.queryParams.statisticaltype == 2) {
|
if (scope.depts.length) {
|
if (this.isInHospitalSatisfaction) {
|
params.hospitaldeptcodes = scope.depts;
|
} else {
|
params.leaveldeptcodes = scope.depts;
|
}
|
}
|
} else if (scope.wards.length) {
|
if (this.isInHospitalSatisfaction) {
|
params.hospitaldistrictcodes = scope.wards;
|
} else {
|
params.leavehospitaldistrictcodes = scope.wards;
|
}
|
}
|
|
return params;
|
},
|
|
// 兼容接口不同的返回层级
|
pickCompletedRows(res) {
|
if (!res) return [];
|
const rowsWrap = res.rows || (res.data && res.data.rows);
|
let list = [];
|
|
if (Array.isArray(rowsWrap) && rowsWrap[0]) {
|
list =
|
rowsWrap[0].serviceSubtaskList ||
|
(rowsWrap[0].sendname ? rowsWrap : []);
|
} else if (res.data && Array.isArray(res.data.serviceSubtaskList)) {
|
list = res.data.serviceSubtaskList;
|
} else if (Array.isArray(res.data)) {
|
list = res.data;
|
}
|
|
return (list || [])
|
.filter(Boolean)
|
.map((row) => this.normalizeCompletedRow(row));
|
},
|
|
normalizeCompletedRow(row) {
|
const item = { ...row };
|
if (item.endtime) {
|
item.endDay = this.daysBetween(item.endtime);
|
}
|
return item;
|
},
|
|
emitCompletedGroups(rows, seq) {
|
if (seq !== this.completedRequestSeq) return;
|
this.$emit("completed-rows-change", {
|
...this.buildCompletedMeta(),
|
loading: false,
|
total: rows.length,
|
rows,
|
});
|
},
|
|
buildCompletedMeta() {
|
return {
|
dimension: this.queryParams.statisticaltype == 2 ? "dept" : "ward",
|
scope: this.satisfactionScope,
|
dateRange: [...(this.queryParams.dateRange || [])],
|
};
|
},
|
|
// 按当前统计类型分组:病区统计 -> 病区,科室统计 -> 科室
|
groupCompletedRows(rows) {
|
const dimension = this.queryParams.statisticaltype == 2 ? "dept" : "ward";
|
const groupMap = new Map();
|
|
(rows || []).forEach((row) => {
|
const name =
|
dimension === "ward" ? this.wardNameOf(row) : this.deptNameOf(row);
|
const code =
|
dimension === "ward" ? this.wardCodeOf(row) : this.deptCodeOf(row);
|
|
const key = String(code || name || "unknown");
|
if (!groupMap.has(key)) {
|
groupMap.set(key, {
|
key,
|
label: name || (dimension === "ward" ? "未分病区" : "未分科室"),
|
rows: [],
|
});
|
}
|
groupMap.get(key).rows.push(row);
|
});
|
|
const orderMap = this.buildGroupOrder(dimension);
|
return Array.from(groupMap.values()).sort((a, b) => {
|
const aIndex = orderMap.has(a.key) ? orderMap.get(a.key) : 99999;
|
const bIndex = orderMap.has(b.key) ? orderMap.get(b.key) : 99999;
|
if (aIndex !== bIndex) return aIndex - bIndex;
|
return a.label.localeCompare(b.label, "zh-Hans-CN");
|
});
|
},
|
|
// tab 顺序按页面左侧科室/病区权限列表的顺序,保证「一病区、二病区…」的自然顺序
|
buildGroupOrder(dimension) {
|
const list =
|
dimension === "ward"
|
? this.$store.getters.belongWards
|
: this.$store.getters.belongDepts;
|
const orderMap = new Map();
|
|
(list || []).forEach((item, index) => {
|
const code = dimension === "ward" ? item.districtCode : item.deptCode;
|
if (code) orderMap.set(String(code), index);
|
});
|
|
return orderMap;
|
},
|
|
wardNameOf(row) {
|
if (!row) return "";
|
const name =
|
this.satisfactionScope === "hospital"
|
? row.hospitaldistrictname || row.leavehospitaldistrictname
|
: row.leavehospitaldistrictname || row.hospitaldistrictname;
|
return name === "-" ? "" : name;
|
},
|
|
wardCodeOf(row) {
|
if (!row) return "";
|
return this.satisfactionScope === "hospital"
|
? row.hospitaldistrictcode || row.leavehospitaldistrictcode
|
: row.leavehospitaldistrictcode || row.hospitaldistrictcode;
|
},
|
|
deptNameOf(row) {
|
if (!row) return "";
|
const name =
|
this.satisfactionScope === "hospital"
|
? row.deptname || row.leaveldeptname
|
: row.leaveldeptname || row.deptname;
|
return name === "-" ? "" : name;
|
},
|
|
deptCodeOf(row) {
|
if (!row) return "";
|
return this.satisfactionScope === "hospital"
|
? row.hospitaldeptcode || row.deptcode
|
: row.deptcode || row.hospitaldeptcode;
|
},
|
|
// 重置查询(恢复默认:离院满意度 + 服务类型 6)
|
resetQuery() {
|
this.satisfactionScope = "leave";
|
this.queryParams = {
|
statisticaltype: 1,
|
leavehospitaldistrictcodes: [],
|
deptcodes: [],
|
serviceType: [6],
|
dateRange: [],
|
pageNum: 1,
|
pageSize: 20,
|
};
|
this.getList();
|
this.maybeRefreshCompletedGroups();
|
this.maybeRefreshQuestionDetails();
|
},
|
|
// 处理分页大小变化
|
handleSizeChange(size) {
|
this.queryParams.pageSize = size;
|
this.queryParams.pageNum = 1;
|
this.getList();
|
},
|
|
// 处理页码变化
|
handlePageChange(page) {
|
this.queryParams.pageNum = page;
|
this.getList();
|
},
|
|
// 处理行选择
|
handleSelectionChange(selection) {
|
this.ids = selection.map((item) => item.id);
|
this.single = selection.length !== 1;
|
this.multiple = !selection.length;
|
},
|
|
// 获取行key(按当前统计类型取对应维度编码)
|
getRowKey(row) {
|
return this.queryParams.statisticaltype == 1
|
? row.hospitaldistrictcode || row.leavehospitaldistrictcode
|
: row.hospitaldeptcode || row.deptcode;
|
},
|
|
// 当前满意度类型对应的病区名称(用于排序/导出/详情标题)
|
getWardName(row) {
|
if (!row) return "";
|
return this.isInHospitalSatisfaction
|
? row.hospitaldistrictname || ""
|
: row.leavehospitaldistrictname || "";
|
},
|
|
// 格式化百分比
|
formatPercent(value) {
|
if (value === null || value === undefined) return "-";
|
const num = parseFloat(value);
|
if (isNaN(num)) return "-";
|
return `${(num * 100).toFixed(2)}%`;
|
},
|
|
// 表格合计行
|
getSummaries(param) {
|
const { columns, data } = param;
|
const sums = [];
|
|
columns.forEach((column, index) => {
|
if (index === 0) {
|
sums[index] = "合计";
|
return;
|
}
|
|
const prop = column.property;
|
|
// 数值列求和
|
if (
|
[
|
"dischargeCount",
|
"nonFollowUp",
|
"followUpNeeded",
|
"joyAllCount",
|
"joyCount",
|
].includes(prop)
|
) {
|
sums[index] = data.reduce(
|
(sum, item) => sum + (Number(item[prop]) || 0),
|
0
|
);
|
return;
|
}
|
|
// 完成比率:满意度填报量 / 满意度题目总量
|
if (prop === "joyTotal") {
|
const totalJoyAllCount = data.reduce(
|
(sum, item) => sum + (Number(item.joyAllCount) || 0),
|
0
|
);
|
const totalJoyCount = data.reduce(
|
(sum, item) => sum + (Number(item.joyCount) || 0),
|
0
|
);
|
sums[index] =
|
totalJoyAllCount > 0
|
? ((totalJoyCount / totalJoyAllCount) * 100).toFixed(2) + "%"
|
: "0.00%";
|
return;
|
}
|
|
// 百分比列(随访率/及时率):取平均值(0 值也参与平均)
|
if (prop === "followUpRate" || prop === "rate") {
|
const percentageValues = data
|
.map((item) => {
|
const value = item[prop];
|
if (
|
value === null ||
|
value === undefined ||
|
value === "" ||
|
value === "-"
|
) {
|
return null;
|
}
|
if (typeof value === "string" && value.includes("%")) {
|
const num = parseFloat(value.replace("%", "")) / 100;
|
return isNaN(num) ? null : num;
|
}
|
const num = parseFloat(value);
|
return isNaN(num) ? null : num;
|
})
|
.filter((value) => value !== null);
|
|
sums[index] =
|
percentageValues.length > 0
|
? (
|
(percentageValues.reduce((sum, value) => sum + value, 0) /
|
percentageValues.length) *
|
100
|
).toFixed(2) + "%"
|
: "0.00%";
|
return;
|
}
|
|
sums[index] = "";
|
});
|
|
return sums;
|
},
|
|
// 查看未及时随访详情
|
handleSeedetails(row) {
|
this.currentRow = row;
|
this.SeedetailsVisible = true;
|
},
|
|
// 查看满意度详情
|
async getinfo(row) {
|
this.currentRow = row;
|
|
try {
|
// 处理查询参数
|
const params = {
|
configKey: "joyCount",
|
...this.queryParams,
|
};
|
|
// 处理日期范围
|
if (
|
this.queryParams.dateRange &&
|
this.queryParams.dateRange.length === 2
|
) {
|
params.startTime = this.queryParams.dateRange[0];
|
params.endTime = this.queryParams.dateRange[1];
|
}
|
|
// 详情只按当前分组维度过滤,清掉其余维度,避免在院/离院条件互相干扰
|
delete params.deptcodes;
|
delete params.leaveldeptcodes;
|
delete params.hospitaldeptcodes;
|
delete params.leavehospitaldistrictcodes;
|
delete params.hospitaldistrictcodes;
|
|
if (this.queryParams.statisticaltype == 1) {
|
if (this.isInHospitalSatisfaction) {
|
this.topicvalue.name = row.hospitaldistrictname || "-";
|
params.hospitaldistrictcodes = [row.hospitaldistrictcode].filter(
|
Boolean
|
);
|
} else {
|
this.topicvalue.name = row.leavehospitaldistrictname || "-";
|
params.leavehospitaldistrictcodes = [
|
row.leavehospitaldistrictcode,
|
].filter(Boolean);
|
}
|
} else {
|
if (this.isInHospitalSatisfaction) {
|
this.topicvalue.name = row.hospitaldeptname || "-";
|
params.hospitaldeptcodes = [row.hospitaldeptcode].filter(Boolean);
|
} else {
|
this.topicvalue.name = row.deptname || "-";
|
params.leaveldeptcodes = [row.deptcode].filter(Boolean);
|
}
|
}
|
|
const response = await getSfStatisticsJoyInfo(params);
|
this.topiclist = response.data || [];
|
this.topicVisible = true;
|
} catch (error) {
|
console.error("获取满意度详情失败:", error);
|
this.$message.error("获取详情失败");
|
}
|
},
|
|
// 获取指定病区/科室的题目详情
|
async fetchTopicDetail(row) {
|
const params = {
|
configKey: "joyCount",
|
...this.queryParams,
|
};
|
// 导出需要该病区/科室的全部题目,覆盖主表分页参数
|
params.pageNum = 1;
|
params.pageSize = 1000;
|
if (
|
this.queryParams.dateRange &&
|
this.queryParams.dateRange.length === 2
|
) {
|
params.startTime = this.queryParams.dateRange[0];
|
params.endTime = this.queryParams.dateRange[1];
|
}
|
if (this.queryParams.statisticaltype == 1) {
|
params.leavehospitaldistrictcodes = [row.leavehospitaldistrictcode];
|
} else {
|
params.deptcodes = [row.deptcode];
|
}
|
const response = await getSfStatisticsJoyInfo(params);
|
return response.data || {};
|
},
|
|
// 主表导出行数据
|
getMainExportRow(item) {
|
return [
|
this.queryParams.statisticaltype == 1
|
? item.leavehospitaldistrictname
|
: item.deptname,
|
item.dischargeCount || 0,
|
item.nonFollowUp || 0,
|
item.followUpNeeded || 0,
|
item.followUpRate || "0%",
|
item.rate ? this.formatPercent(item.rate) : "0%",
|
item.joyAllCount || 0,
|
item.joyCount || 0,
|
item.joyTotal ? this.formatPercent(item.joyTotal) : "0%",
|
];
|
},
|
|
// 计算文本显示宽度(中文按 2、英文/数字按 1)
|
textWidth(str) {
|
if (!str) return 0;
|
let width = 0;
|
for (const ch of String(str)) {
|
width += ch.charCodeAt(0) > 255 ? 2 : 1;
|
}
|
return width;
|
},
|
|
// 丽水市中医院:拉取所有科室题目明细,扁平化为「题目选项」列结构
|
async buildLishuiTopicColumns() {
|
const topicMap = new Map();
|
const rowQuantities = [];
|
|
for (const item of this.userList) {
|
const topiclist = await this.fetchTopicDetail(item);
|
const qMap = new Map();
|
|
if (topiclist && Object.keys(topiclist).length) {
|
Object.keys(topiclist).forEach((key) => {
|
const topic = topiclist[key];
|
if (!topic || !topic.scriptContent) return;
|
|
const tKey =
|
topic.scriptid != null && topic.scriptid !== ""
|
? String(topic.scriptid)
|
: topic.scriptContent;
|
|
if (!topicMap.has(tKey)) {
|
topicMap.set(tKey, {
|
key: tKey,
|
scriptContent: topic.scriptContent,
|
options: [],
|
optionSet: new Set(),
|
});
|
}
|
|
const t = topicMap.get(tKey);
|
const topicQ = new Map();
|
(topic.details || []).forEach((detail) => {
|
if (!detail || !detail.optionText) return;
|
if (!t.optionSet.has(detail.optionText)) {
|
t.optionSet.add(detail.optionText);
|
t.options.push(detail.optionText);
|
}
|
topicQ.set(detail.optionText, Number(detail.chosenQuantity) || 0);
|
});
|
qMap.set(tKey, topicQ);
|
});
|
}
|
|
rowQuantities.push(qMap);
|
}
|
|
const topics = Array.from(topicMap.values());
|
// 没有选项的题目默认填充一个空选项列,避免表头/数据错位
|
topics.forEach((t) => {
|
if (!t.options.length) {
|
t.options = [""];
|
}
|
});
|
return { topics, rowQuantities };
|
},
|
|
// 导出数据
|
async handleExport() {
|
if (!this.userList.length) {
|
this.$message.warning("没有数据可导出");
|
return;
|
}
|
|
try {
|
this.loading = true;
|
|
// 构建日期范围字符串
|
let dateRangeString = "";
|
let sheetNameSuffix = "";
|
const isLishuiHospital = this.orgname == "丽水市中医院";
|
|
if (
|
this.queryParams.dateRange &&
|
this.queryParams.dateRange.length === 2
|
) {
|
const startDateStr = this.queryParams.dateRange[0];
|
const endDateStr = this.queryParams.dateRange[1];
|
|
if (isLishuiHospital) {
|
// 丽水市中医院:只显示年月
|
const formatMonthOnly = (dateTimeStr) => {
|
const date = new Date(dateTimeStr);
|
const year = date.getFullYear();
|
const month = date.getMonth() + 1;
|
return `${year}年${month}月`;
|
};
|
const startDateFormatted = formatMonthOnly(startDateStr);
|
const endDateFormatted = formatMonthOnly(endDateStr);
|
dateRangeString = `${startDateFormatted}至${endDateFormatted}`;
|
sheetNameSuffix = `${startDateFormatted}至${endDateFormatted}`;
|
} else {
|
// 其他医院:显示年月日
|
const formatDateForDisplay = (dateTimeStr) => {
|
return dateTimeStr.split(" ")[0];
|
};
|
const startDateFormatted = formatDateForDisplay(startDateStr);
|
const endDateFormatted = formatDateForDisplay(endDateStr);
|
dateRangeString = `${startDateFormatted}至${endDateFormatted}`;
|
sheetNameSuffix = `${startDateFormatted}至${endDateFormatted}`;
|
}
|
} else {
|
const now = new Date();
|
const currentMonth = now.getMonth() + 1;
|
const currentYear = now.getFullYear();
|
|
if (isLishuiHospital) {
|
// 丽水市中医院:显示年月
|
dateRangeString = `${currentYear}年${currentMonth}月`;
|
sheetNameSuffix = `${currentYear}年${currentMonth}月`;
|
} else {
|
// 其他医院:显示月份
|
dateRangeString = `${currentMonth}月`;
|
sheetNameSuffix = `${currentMonth}月`;
|
}
|
}
|
|
const excelName = `满意度随访统计表_${dateRangeString}.xlsx`;
|
const worksheetName = `随访统计_${sheetNameSuffix}`;
|
|
// 创建Excel工作簿
|
const workbook = new ExcelJS.Workbook();
|
const worksheet = workbook.addWorksheet(worksheetName);
|
|
// 定义样式
|
const titleStyle = {
|
font: { name: "微软雅黑", size: 16, bold: true },
|
fill: {
|
type: "pattern",
|
pattern: "solid",
|
fgColor: { argb: "FFE6F3FF" },
|
},
|
alignment: { vertical: "middle", horizontal: "center" },
|
border: {
|
top: { style: "thin", color: { argb: "FFD0D0D0" } },
|
left: { style: "thin", color: { argb: "FFD0D0D0" } },
|
bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
|
right: { style: "thin", color: { argb: "FFD0D0D0" } },
|
},
|
};
|
|
const headerStyle = {
|
font: { name: "微软雅黑", size: 11, bold: true },
|
fill: {
|
type: "pattern",
|
pattern: "solid",
|
fgColor: { argb: "FFF5F7FA" },
|
},
|
alignment: {
|
vertical: "middle",
|
horizontal: "center",
|
wrapText: true,
|
},
|
border: {
|
top: { style: "thin", color: { argb: "FFD0D0D0" } },
|
left: { style: "thin", color: { argb: "FFD0D0D0" } },
|
bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
|
right: { style: "thin", color: { argb: "FFD0D0D0" } },
|
},
|
};
|
|
const cellStyle = {
|
font: { name: "宋体", size: 10 },
|
alignment: { vertical: "middle", horizontal: "center" },
|
border: {
|
top: { style: "thin", color: { argb: "FFD0D0D0" } },
|
left: { style: "thin", color: { argb: "FFD0D0D0" } },
|
bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
|
right: { style: "thin", color: { argb: "FFD0D0D0" } },
|
},
|
};
|
|
// 丽水市中医院:预先拉取题目明细,扁平化为「题目选项」列
|
let lishuiTopics = [];
|
let lishuiRowQuantities = [];
|
if (isLishuiHospital) {
|
const built = await this.buildLishuiTopicColumns();
|
lishuiTopics = built.topics;
|
lishuiRowQuantities = built.rowQuantities;
|
}
|
|
// 主表列
|
const mainHeaders = [
|
this.queryParams.statisticaltype == 1 ? "出院病区" : "科室",
|
"出院人次",
|
"无需随访人次",
|
"应随访人次",
|
"随访率",
|
"及时率",
|
"满意度题目总量",
|
"满意度填报量",
|
"完成比率",
|
];
|
const mainColumnCount = mainHeaders.length;
|
|
// 总列数(丽水市中医院在末尾追加题目选项列)
|
const topicColumnCount = lishuiTopics.reduce(
|
(sum, t) => sum + (t.options.length || 1),
|
0
|
);
|
const totalColumns = mainColumnCount + topicColumnCount;
|
|
// 添加总标题
|
worksheet.mergeCells(1, 1, 1, totalColumns);
|
const titleCell = worksheet.getCell(1, 1);
|
titleCell.value = `满意度随访统计表(${sheetNameSuffix})`;
|
titleCell.style = titleStyle;
|
worksheet.getRow(1).height = 35;
|
|
if (isLishuiHospital && lishuiTopics.length) {
|
// 计算每个题目的选项列宽(选项文字自适应,题干过长时摊宽列、必要时换行)
|
const topicColumns = lishuiTopics.map((topic) => {
|
const n = topic.options.length || 1;
|
const baseWidths = topic.options.map((opt) =>
|
opt === "" ? 10 : Math.max(10, this.textWidth(opt) + 4)
|
);
|
const sumWidth = baseWidths.reduce((s, w) => s + w, 0);
|
const topicWidth = this.textWidth(topic.scriptContent) + 4;
|
const groupWidth = Math.max(sumWidth, topicWidth);
|
const extra = (groupWidth - sumWidth) / n;
|
const widths = baseWidths.map((w) => w + extra);
|
const lines = Math.max(1, Math.ceil(topicWidth / groupWidth));
|
return { widths, lines };
|
});
|
const maxHeaderLines = topicColumns.reduce(
|
(m, t) => Math.max(m, t.lines),
|
1
|
);
|
|
// 两行表头:第一行 = 主表列 + 题目名;第二行 = 选项名
|
const headerRow1 = worksheet.addRow([]);
|
const headerRow2 = worksheet.addRow([]);
|
|
mainHeaders.forEach((h, i) => {
|
const col = i + 1;
|
headerRow1.getCell(col).value = h;
|
headerRow1.getCell(col).style = headerStyle;
|
headerRow2.getCell(col).style = headerStyle;
|
worksheet.mergeCells(
|
headerRow1.number,
|
col,
|
headerRow2.number,
|
col
|
);
|
});
|
|
let colIndex = mainColumnCount + 1;
|
lishuiTopics.forEach((topic) => {
|
const optCount = topic.options.length || 1;
|
for (let k = 0; k < optCount; k++) {
|
headerRow1.getCell(colIndex + k).style = headerStyle;
|
}
|
headerRow1.getCell(colIndex).value = topic.scriptContent;
|
if (optCount > 1) {
|
worksheet.mergeCells(
|
headerRow1.number,
|
colIndex,
|
headerRow1.number,
|
colIndex + optCount - 1
|
);
|
}
|
topic.options.forEach((opt, k) => {
|
headerRow2.getCell(colIndex + k).value = opt;
|
headerRow2.getCell(colIndex + k).style = headerStyle;
|
});
|
colIndex += optCount;
|
});
|
|
// 表头高度:题干换行时按最长题目预留
|
headerRow1.height = Math.max(30, 15 * maxHeaderLines + 8);
|
headerRow2.height = 25;
|
|
// 数据行
|
this.userList.forEach((item, idx) => {
|
const row = worksheet.addRow([]);
|
const main = this.getMainExportRow(item);
|
main.forEach((v, i) => {
|
const cell = row.getCell(i + 1);
|
cell.value = v;
|
cell.style = cellStyle;
|
});
|
|
const qMap = lishuiRowQuantities[idx] || new Map();
|
let cIndex = mainColumnCount + 1;
|
for (const topic of lishuiTopics) {
|
const topicQ = qMap.get(topic.key);
|
topic.options.forEach((opt) => {
|
const cell = row.getCell(cIndex);
|
if (opt === "") {
|
// 空选项占位列不填数字
|
cell.value = "";
|
} else {
|
const val = topicQ ? topicQ.get(opt) : undefined;
|
cell.value = val === undefined || val === null ? 0 : val;
|
}
|
cell.style = cellStyle;
|
cIndex++;
|
});
|
}
|
row.height = 22;
|
});
|
|
// 列宽:主表列 + 题目选项列
|
const columns = [
|
{ width: 20 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 15 },
|
{ width: 15 },
|
{ width: 12 },
|
];
|
lishuiTopics.forEach((topic, ti) => {
|
topicColumns[ti].widths.forEach((w) => {
|
columns.push({ width: w });
|
});
|
});
|
worksheet.columns = columns;
|
} else {
|
// 其他医院:单行表头
|
const headerRow = worksheet.addRow(mainHeaders);
|
headerRow.eachCell((cell) => {
|
cell.style = headerStyle;
|
});
|
headerRow.height = 25;
|
|
this.userList.forEach((item) => {
|
const dataRow = worksheet.addRow(this.getMainExportRow(item));
|
dataRow.eachCell((cell) => {
|
cell.style = cellStyle;
|
});
|
dataRow.height = 22;
|
});
|
|
worksheet.columns = [
|
{ width: 20 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 15 },
|
{ width: 15 },
|
{ width: 12 },
|
];
|
}
|
|
// 生成并下载文件
|
const buffer = await workbook.xlsx.writeBuffer();
|
const blob = new Blob([buffer], {
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
});
|
|
saveAs(blob, excelName);
|
this.$message.success("导出成功");
|
} catch (error) {
|
console.error("导出失败:", error);
|
this.$message.error(`导出失败: ${error.message}`);
|
} finally {
|
this.loading = false;
|
}
|
},
|
},
|
};
|
</script>
|
|
<style lang="scss" scoped>
|
.followup-statistics {
|
.query-section {
|
background: #fff;
|
padding: 20px;
|
border-radius: 4px;
|
margin-bottom: 20px;
|
|
.query-form {
|
display: flex;
|
flex-wrap: wrap;
|
|
::v-deep .el-form-item {
|
margin-bottom: 20px;
|
|
&:not(:last-child) {
|
margin-right: 20px;
|
}
|
}
|
}
|
}
|
|
.table-section {
|
background: #fff;
|
padding: 20px;
|
border-radius: 4px;
|
margin-bottom: 20px;
|
|
::v-deep .el-table {
|
th {
|
background-color: #f8f9fa;
|
font-weight: 600;
|
color: #333;
|
}
|
}
|
}
|
|
.pagination-section {
|
display: flex;
|
justify-content: flex-end;
|
background: #fff;
|
padding: 20px;
|
border-radius: 4px;
|
}
|
}
|
</style>
|