<template>
|
<div class="combined-follow-up">
|
<div class="your-table-container">
|
<el-table
|
ref="exportTable"
|
id="exportTableCombined"
|
v-loading="loading"
|
:data="tableData"
|
border
|
@expand-change="handleRowClick"
|
:row-key="getRowKey"
|
:expand-row-keys="expands"
|
show-summary
|
:summary-method="getSummaries"
|
>
|
<!-- ========== 展开行(医生子表) ========== -->
|
<el-table-column type="expand">
|
<template slot-scope="props">
|
<el-table
|
:data="props.row.doctorStats"
|
border
|
style="width: 95%; margin: 0 auto"
|
class="inner-table"
|
show-summary
|
:summary-method="getInnerSummaries"
|
>
|
<el-table-column label="" align="center" width="96" fixed="left">
|
<template slot="header">
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
<el-button type="primary" size="mini" icon="el-icon-download" @click="exportDoctorTable(props.row)">
|
导出
|
</el-button>
|
</div>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="医生姓名" prop="drname" align="center" />
|
<el-table-column label="科室" prop="deptname" align="center" width="120" />
|
|
<!-- 基础列 -->
|
<el-table-column label="出院人次" prop="dischargeCount" align="center">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'dischargeCountInfo', '出院患者列表', '1')">
|
<span class="button-zx">{{ scope.row.dischargeCount }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
|
<!-- 首次随访(医生) -->
|
<el-table-column label="首次无需随访" prop="nonFollowUp" align="center">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'nonFollowUpInfo', '首次无需随访列表', '1')">
|
<span class="button-zx">{{ scope.row.nonFollowUp }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="首次应随访" prop="followUpNeeded" align="center">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'followUpNeededInfo', '首次应随访列表', '1')">
|
<span class="button-zx">{{ scope.row.followUpNeeded }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="首次需随访" prop="needFollowUp" align="center">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'needFollowUpInfo', '首次需随访列表', '1')">
|
<span class="button-zx">{{ scope.row.needFollowUp }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="首次待随访" prop="pendingFollowUp" align="center">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'pendingFollowUpInfo', '首次待随访列表', '1')">
|
<span class="button-zx">{{ scope.row.pendingFollowUp }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="首次完成" prop="followUpSuccess" align="center">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'followUpSuccessInfo', '首次完成列表', '1')">
|
<span class="button-zx">{{ scope.row.followUpSuccess }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="首次失败" prop="followUpFail" align="center">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'followUpFailInfo', '首次失败列表', '1')">
|
<span class="button-zx">{{ scope.row.followUpFail }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column v-if="orgname != '景宁畲族自治县人民医院'" label="首次成功率" prop="successRate" align="center" :render-header="Tooltipcgl">
|
<template slot-scope="scope">
|
<span>{{ calculateSuccessRate(scope.row.followUpSuccess, scope.row.needFollowUp, scope.row.pendingFollowUp) }}</span>
|
</template>
|
</el-table-column>
|
|
<!-- 再次随访(医生) -->
|
<el-table-column label="再次需随访" prop="needFollowUpAgain" align="center">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'needFollowUpAgainInfo', '再次需随访列表', '1')">
|
<span class="button-zx">{{ scope.row.needFollowUpAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="再次待随访" prop="pendingFollowUpAgain" align="center">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'pendingFollowUpAgainInfo', '再次待随访列表', '1')">
|
<span class="button-zx">{{ scope.row.pendingFollowUpAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="再次完成" prop="followUpSuccessAgain" align="center">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'followUpSuccessAgainInfo', '再次完成列表', '1')">
|
<span class="button-zx">{{ scope.row.followUpSuccessAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="再次失败" prop="followUpFailAgain" align="center">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'followUpFailAgainInfo', '再次失败列表', '1')">
|
<span class="button-zx">{{ scope.row.followUpFailAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column v-if="orgname != '景宁畲族自治县人民医院'" label="再次成功率" prop="successRateAgain" align="center" :render-header="Tooltipcgl">
|
<template slot-scope="scope">
|
<span>{{ calculateSuccessRate(scope.row.followUpSuccessAgain, scope.row.needFollowUpAgain, scope.row.pendingFollowUpAgain) }}</span>
|
</template>
|
</el-table-column>
|
|
<!-- 新增计算指标(医生) -->
|
<el-table-column label="随访率" align="center" prop="followUpRate" :render-header="TooltipFollowUpRate">
|
<template slot-scope="scope">
|
<span>{{ calculateFollowUpRate(scope.row) }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="推送率" align="center" prop="pushRate" :render-header="TooltipPushRate">
|
<template slot-scope="scope">
|
<span>{{ calculatePushRate(scope.row) }}</span>
|
</template>
|
</el-table-column>
|
</el-table>
|
</template>
|
</el-table-column>
|
|
<!-- ========== 主表格列 ========== -->
|
<!-- 病区 / 科室 -->
|
<el-table-column
|
v-if="queryParams.statisticaltype == 1"
|
label="出院病区"
|
align="center"
|
sortable
|
prop="leavehospitaldistrictname"
|
width="150"
|
:show-overflow-tooltip="true"
|
:sort-method="sortChineseNumber"
|
/>
|
<el-table-column
|
v-if="queryParams.statisticaltype == 2"
|
label="科室"
|
align="center"
|
prop="deptname"
|
:show-overflow-tooltip="true"
|
/>
|
|
<!-- 出院人次 -->
|
<el-table-column label="出院人次" align="center" prop="dischargeCount">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'dischargeCountInfo', '出院患者列表')">
|
<span class="button-zx">{{ scope.row.dischargeCount }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="无需随访" align="center" prop="nonFollowUp">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'nonFollowUpInfo', '首次无需随访列表')">
|
<span class="button-zx">{{ scope.row.nonFollowUp }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="应随访" align="center" prop="followUpNeeded">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'followUpNeededInfo', '首次应随访列表')">
|
<span class="button-zx">{{ scope.row.followUpNeeded }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
|
<!-- ===== 首次随访 ===== -->
|
<el-table-column align="center" label="首次随访">
|
|
<el-table-column label="需随访" align="center" prop="needFollowUp">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'needFollowUpInfo', '首次需随访列表')">
|
<span class="button-zx">{{ scope.row.needFollowUp }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="待随访" align="center" prop="pendingFollowUp">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'pendingFollowUpInfo', '首次待随访列表')">
|
<span class="button-zx">{{ scope.row.pendingFollowUp }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="完成" align="center" prop="followUpSuccess">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'followUpSuccessInfo', '首次完成列表')">
|
<span class="button-zx">{{ scope.row.followUpSuccess }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="失败" align="center" prop="followUpFail">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'followUpFailInfo', '首次失败列表')">
|
<span class="button-zx">{{ scope.row.followUpFail }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column v-if="orgname != '景宁畲族自治县人民医院'" label="成功率" align="center" prop="successRate" :render-header="Tooltipcgl">
|
<template slot-scope="scope">
|
<span>{{ calculateSuccessRate(scope.row.followUpSuccess, scope.row.needFollowUp, scope.row.pendingFollowUp) }}</span>
|
</template>
|
</el-table-column>
|
</el-table-column>
|
|
<!-- ===== 再次随访 ===== -->
|
<el-table-column align="center" label="再次随访">
|
<el-table-column label="需随访" align="center" prop="needFollowUpAgain">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'needFollowUpAgainInfo', '再次需随访列表')">
|
<span class="button-zx">{{ scope.row.needFollowUpAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="待随访" align="center" prop="pendingFollowUpAgain">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'pendingFollowUpAgainInfo', '再次待随访列表')">
|
<span class="button-zx">{{ scope.row.pendingFollowUpAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="完成" align="center" prop="followUpSuccessAgain">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'followUpSuccessAgainInfo', '再次完成列表')">
|
<span class="button-zx">{{ scope.row.followUpSuccessAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="失败" align="center" prop="followUpFailAgain">
|
<template slot-scope="scope">
|
<el-button type="text" @click="handleViewDetails(scope.row, 'followUpFailAgainInfo', '再次失败列表')">
|
<span class="button-zx">{{ scope.row.followUpFailAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column v-if="orgname != '景宁畲族自治县人民医院'" label="成功率" align="center" prop="successRateAgain" :render-header="Tooltipcgl">
|
<template slot-scope="scope">
|
<span>{{ calculateSuccessRate(scope.row.followUpSuccessAgain, scope.row.needFollowUpAgain, scope.row.pendingFollowUpAgain) }}</span>
|
</template>
|
</el-table-column>
|
</el-table-column>
|
|
<!-- ===== 新增计算指标 ===== -->
|
<el-table-column label="随访率" align="center" prop="followUpRate" :render-header="TooltipFollowUpRate">
|
<template slot-scope="scope">
|
<span>{{ calculateFollowUpRate(scope.row) }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="推送率" align="center" prop="pushRate" :render-header="TooltipPushRate">
|
<template slot-scope="scope">
|
<span>{{ calculatePushRate(scope.row) }}</span>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import { getSfStatistics } from "@/api/system/user";
|
import ExcelJS from "exceljs";
|
import { saveAs } from "file-saver";
|
import store from "@/store";
|
|
export default {
|
name: "CombinedFollowUp",
|
props: {
|
queryParams: {
|
type: Object,
|
required: true,
|
},
|
flatArrayhospit: {
|
type: Array,
|
default: () => [],
|
},
|
flatArraydept: {
|
type: Array,
|
default: () => [],
|
},
|
options: {
|
type: Array,
|
default: () => [],
|
},
|
orgname: {
|
type: String,
|
default: "",
|
},
|
},
|
data() {
|
return {
|
tableData: [],
|
loading: false,
|
expands: [],
|
tasktypes: store.getters.tasktypes,
|
};
|
},
|
|
methods: {
|
// ===================== 数据加载 =====================
|
loadData() {
|
this.loading = true;
|
const params = {
|
...this.queryParams,
|
// 不传 visitCount,后端返回包含首次和再次的数据
|
leavehospitaldistrictcodes: this.queryParams.leavehospitaldistrictcodes.includes("all")
|
? this.getAllWardCodes()
|
: this.queryParams.leavehospitaldistrictcodes,
|
deptcodes: this.queryParams.deptcodes.includes("all")
|
? this.getAllDeptCodes()
|
: this.queryParams.deptcodes,
|
rateDay: this.orgname == "龙泉市人民医院" ? 21 : 7,
|
};
|
|
delete params.leavehospitaldistrictcodes.all;
|
delete params.deptcodes.all;
|
|
getSfStatistics(params)
|
.then((res) => {
|
this.tableData = this.customSort(res.data);
|
})
|
.catch((err) => {
|
console.error("获取综合统计数据失败:", err);
|
this.$message.error("获取综合统计数据失败");
|
})
|
.finally(() => {
|
this.loading = false;
|
});
|
},
|
|
getAllWardCodes() {
|
return this.flatArrayhospit.filter((item) => item.value !== "all").map((item) => item.value);
|
},
|
getAllDeptCodes() {
|
return this.flatArraydept.filter((item) => item.value !== "all").map((item) => item.value);
|
},
|
|
// ===================== 排序 =====================
|
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;
|
}
|
return -1;
|
};
|
const indexA = getIndex(a.leavehospitaldistrictname);
|
const indexB = getIndex(b.leavehospitaldistrictname);
|
if (indexA === -1 && indexB === -1) {
|
return (a.leavehospitaldistrictname || "").localeCompare(b.leavehospitaldistrictname || "");
|
}
|
if (indexA === -1) return 1;
|
if (indexB === -1) return -1;
|
return indexA - indexB;
|
});
|
},
|
|
sortChineseNumber(aRow, bRow) {
|
// 与 customSort 逻辑相同,但用于 el-table 的 sort-method
|
const a = aRow.leavehospitaldistrictname;
|
const b = bRow.leavehospitaldistrictname;
|
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;
|
},
|
|
// ===================== 行展开 =====================
|
getRowKey(row) {
|
return row.statisticaltype === 1 ? row.leavehospitaldistrictcode : row.deptcode;
|
},
|
|
handleRowClick(row) {
|
if (this.expands.includes(this.getRowKey(row))) {
|
this.expands = [];
|
return;
|
}
|
|
const params = {
|
...this.queryParams,
|
deptcodes: this.queryParams.deptcodes.includes("all")
|
? this.getAllDeptCodes()
|
: this.queryParams.deptcodes,
|
leavehospitaldistrictcodes: [row.leavehospitaldistrictcode],
|
drcode: "1",
|
// 不传 visitCount
|
rateDay: this.orgname == "龙泉市人民医院" ? 21 : 7,
|
};
|
delete params.leavehospitaldistrictcodes.all;
|
delete params.deptcodes.all;
|
|
if (!row.doctorStats) {
|
this.loading = true;
|
getSfStatistics(params)
|
.then((res) => {
|
this.$set(row, "doctorStats", res.data);
|
this.expands = [this.getRowKey(row)];
|
})
|
.catch(() => {
|
this.$message.error("获取医生数据失败");
|
})
|
.finally(() => {
|
this.loading = false;
|
});
|
} else {
|
this.expands = [this.getRowKey(row)];
|
}
|
},
|
|
// ===================== 计算指标 =====================
|
calculateSuccessRate(success, need, pending) {
|
const s = Number(success) || 0;
|
const n = Number(need) || 0;
|
const p = Number(pending) || 0;
|
const denominator = n - p;
|
if (denominator <= 0) return "0.00%";
|
return ((s / denominator) * 100).toFixed(2) + "%";
|
},
|
|
// 随访率 = (首次完成 + 首次无需随访) / (首次应随访 + 首次无需随访)
|
calculateFollowUpRate(row) {
|
const success = Number(row.followUpSuccess) || 0;
|
const nonFollowUp = Number(row.nonFollowUp) || 0;
|
const followUpNeeded = Number(row.followUpNeeded) || 0;
|
const denominator = followUpNeeded + nonFollowUp;
|
if (denominator <= 0) return "0.00%";
|
return (((success + nonFollowUp) / denominator) * 100).toFixed(2) + "%";
|
},
|
|
// 推送率 = 再次完成 / (首次应随访 + 首次无需随访)
|
calculatePushRate(row) {
|
const successAgain = Number(row.followUpSuccessAgain) || 0;
|
const nonFollowUp = Number(row.nonFollowUp) || 0;
|
const followUpNeeded = Number(row.followUpNeeded) || 0;
|
const denominator = followUpNeeded + nonFollowUp;
|
console.log(successAgain,nonFollowUp,followUpNeeded);
|
|
if (denominator <= 0) return "0.00%";
|
return ((successAgain / denominator) * 100).toFixed(2) + "%";
|
},
|
|
// ===================== 汇总 =====================
|
getSummaries(param) {
|
const { columns, data } = param;
|
const sums = [];
|
columns.forEach((column, index) => {
|
if (index === 0) {
|
sums[index] = "合计";
|
return;
|
}
|
if (index === 1) {
|
sums[index] = "/";
|
return;
|
}
|
|
const prop = column.property;
|
// 对于百分比列,使用整体法或平均法(这里用平均值)
|
if (prop === "successRate" || prop === "successRateAgain") {
|
// 因为计算复杂,这里简化为 "-"
|
sums[index] = "-";
|
} else if (prop === "followUpRate") {
|
const values = data.map((row) => parseFloat(this.calculateFollowUpRate(row).replace("%", ""))).filter(v => !isNaN(v));
|
if (values.length === 0) sums[index] = "0.00%";
|
else sums[index] = (values.reduce((a, b) => a + b, 0) / values.length).toFixed(2) + "%";
|
} else if (prop === "pushRate") {
|
const values = data.map((row) => parseFloat(this.calculatePushRate(row).replace("%", ""))).filter(v => !isNaN(v));
|
if (values.length === 0) sums[index] = "0.00%";
|
else sums[index] = (values.reduce((a, b) => a + b, 0) / values.length).toFixed(2) + "%";
|
} else {
|
// 数字列求和
|
const values = data.map((item) => Number(item[prop]) || 0);
|
sums[index] = values.reduce((a, b) => a + b, 0);
|
sums[index] = this.formatNumber(sums[index]);
|
}
|
});
|
return sums;
|
},
|
|
// 内嵌表格汇总(医生)
|
getInnerSummaries(param) {
|
const { columns, data } = param;
|
const sums = [];
|
columns.forEach((column, index) => {
|
if (index === 0) {
|
sums[index] = "小计";
|
return;
|
}
|
if (column.property === "drname" || column.property === "deptname") {
|
sums[index] = "-";
|
return;
|
}
|
const prop = column.property;
|
if (prop === "successRate" || prop === "successRateAgain") {
|
sums[index] = "-";
|
} else if (prop === "followUpRate") {
|
const values = data.map((row) => parseFloat(this.calculateFollowUpRate(row).replace("%", ""))).filter(v => !isNaN(v));
|
sums[index] = values.length ? (values.reduce((a, b) => a + b, 0) / values.length).toFixed(2) + "%" : "0.00%";
|
} else if (prop === "pushRate") {
|
const values = data.map((row) => parseFloat(this.calculatePushRate(row).replace("%", ""))).filter(v => !isNaN(v));
|
sums[index] = values.length ? (values.reduce((a, b) => a + b, 0) / values.length).toFixed(2) + "%" : "0.00%";
|
} else {
|
const values = data.map((item) => Number(item[prop]) || 0);
|
sums[index] = values.reduce((a, b) => a + b, 0);
|
sums[index] = this.formatNumber(sums[index]);
|
}
|
});
|
return sums;
|
},
|
|
formatNumber(num) {
|
if (isNaN(num)) return "-";
|
return Number.isInteger(num) ? num.toString() : num.toFixed(0);
|
},
|
|
// ===================== 点击钻取 =====================
|
handleViewDetails(row, infoKey, titleSuffix, type) {
|
const title = `${row.leavehospitaldistrictname || row.deptname}${titleSuffix}`;
|
this.$emit("view-details", row, infoKey, title, type);
|
},
|
|
// ===================== 导出 =====================
|
async exportTable() {
|
try {
|
let dateRangeString = "";
|
const isLishuiHospital = this.orgname === "丽水市中医院";
|
if (this.queryParams.dateRange && this.queryParams.dateRange.length === 2) {
|
const start = this.queryParams.dateRange[0];
|
const end = this.queryParams.dateRange[1];
|
if (isLishuiHospital) {
|
const formatMonth = (d) => {
|
const date = new Date(d);
|
return `${date.getFullYear()}年${date.getMonth() + 1}月`;
|
};
|
dateRangeString = `${formatMonth(start)}至${formatMonth(end)}`;
|
} else {
|
dateRangeString = `${start.split(" ")[0]}至${end.split(" ")[0]}`;
|
}
|
} else {
|
const now = new Date();
|
dateRangeString = isLishuiHospital ? `${now.getFullYear()}年${now.getMonth() + 1}月` : `${now.getMonth() + 1}月`;
|
}
|
|
const excelName = `综合随访统计表_${dateRangeString}.xlsx`;
|
if (!this.tableData || this.tableData.length === 0) {
|
this.$message.warning("暂无数据可导出");
|
return false;
|
}
|
|
const workbook = new ExcelJS.Workbook();
|
const worksheet = workbook.addWorksheet("综合统计");
|
this.buildExportSheet(worksheet, dateRangeString);
|
const buffer = await workbook.xlsx.writeBuffer();
|
const blob = new Blob([buffer], {
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
});
|
saveAs(blob, excelName);
|
this.$message.success("导出成功");
|
return true;
|
} catch (error) {
|
console.error(error);
|
this.$message.error("导出失败");
|
return false;
|
}
|
},
|
|
buildExportSheet(worksheet, dateRangeString) {
|
// 定义样式
|
const titleStyle = {
|
font: { name: "微软雅黑", size: 16, bold: true },
|
alignment: { horizontal: "center", vertical: "middle" },
|
fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFE6F3FF" } },
|
border: { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" } },
|
};
|
const headerStyle = {
|
font: { name: "微软雅黑", size: 11, bold: true },
|
fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFF5F7FA" } },
|
alignment: { horizontal: "center", vertical: "middle", wrapText: true },
|
border: { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" } },
|
};
|
const cellStyle = {
|
font: { name: "宋体", size: 10 },
|
alignment: { horizontal: "center", vertical: "middle" },
|
border: { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" } },
|
};
|
const summaryStyle = {
|
...cellStyle,
|
font: { ...cellStyle.font, bold: true, color: { argb: "FF409EFF" } },
|
fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFF5F7FA" } },
|
};
|
|
// 标题行
|
worksheet.mergeCells(1, 1, 1, 20);
|
worksheet.getCell(1, 1).value = `综合随访统计表_${dateRangeString}`;
|
worksheet.getCell(1, 1).style = titleStyle;
|
worksheet.getRow(1).height = 35;
|
|
// 第二行和第三行合并构建表头
|
// 第一列留空
|
worksheet.mergeCells(2, 1, 3, 1);
|
worksheet.getCell(2, 1).value = "";
|
worksheet.getCell(2, 1).style = headerStyle;
|
|
// 病区/科室
|
worksheet.mergeCells(2, 2, 3, 2);
|
worksheet.getCell(2, 2).value = this.queryParams.statisticaltype == 1 ? "出院病区" : "科室";
|
worksheet.getCell(2, 2).style = headerStyle;
|
|
// 出院人次
|
worksheet.mergeCells(2, 3, 3, 3);
|
worksheet.getCell(2, 3).value = "出院人次";
|
worksheet.getCell(2, 3).style = headerStyle;
|
|
// 首次随访大列
|
worksheet.mergeCells(2, 4, 2, 10);
|
worksheet.getCell(2, 4).value = "首次随访";
|
worksheet.getCell(2, 4).style = headerStyle;
|
const firstCols = ["无需随访", "应随访", "需随访", "待随访", "完成", "失败", "成功率"];
|
firstCols.forEach((label, idx) => {
|
const col = 4 + idx;
|
worksheet.getCell(3, col).value = label;
|
worksheet.getCell(3, col).style = headerStyle;
|
});
|
|
// 再次随访大列
|
worksheet.mergeCells(2, 11, 2, 15);
|
worksheet.getCell(2, 11).value = "再次随访";
|
worksheet.getCell(2, 11).style = headerStyle;
|
const secondCols = ["需随访", "待随访", "完成", "失败", "成功率"];
|
secondCols.forEach((label, idx) => {
|
const col = 11 + idx;
|
worksheet.getCell(3, col).value = label;
|
worksheet.getCell(3, col).style = headerStyle;
|
});
|
|
// 新增计算指标
|
worksheet.mergeCells(2, 16, 3, 16);
|
worksheet.getCell(2, 16).value = "随访率";
|
worksheet.getCell(2, 16).style = headerStyle;
|
worksheet.mergeCells(2, 17, 3, 17);
|
worksheet.getCell(2, 17).value = "推送率";
|
worksheet.getCell(2, 17).style = headerStyle;
|
|
// 数据行
|
this.tableData.forEach((item) => {
|
const rowData = [
|
"",
|
item.leavehospitaldistrictname || item.deptname || "",
|
item.dischargeCount || 0,
|
item.nonFollowUp || 0,
|
item.followUpNeeded || 0,
|
item.needFollowUp || 0,
|
item.pendingFollowUp || 0,
|
item.followUpSuccess || 0,
|
item.followUpFail || 0,
|
this.calculateSuccessRate(item.followUpSuccess, item.needFollowUp, item.pendingFollowUp),
|
item.needFollowUpAgain || 0,
|
item.pendingFollowUpAgain || 0,
|
item.followUpSuccessAgain || 0,
|
item.followUpFailAgain || 0,
|
this.calculateSuccessRate(item.followUpSuccessAgain, item.needFollowUpAgain, item.pendingFollowUpAgain),
|
this.calculateFollowUpRate(item),
|
this.calculatePushRate(item),
|
];
|
const row = worksheet.addRow(rowData);
|
row.eachCell((cell) => { cell.style = cellStyle; });
|
row.height = 24;
|
});
|
|
// 合计行
|
const summaries = this.getExportSummaries();
|
const summaryRow = worksheet.addRow(summaries);
|
summaryRow.eachCell((cell) => { cell.style = summaryStyle; });
|
|
// 列宽
|
worksheet.columns = [
|
{ width: 8 },
|
{ width: 20 },
|
{ width: 12 },
|
{ width: 12 }, { width: 12 }, { width: 12 }, { width: 12 }, { width: 12 }, { width: 12 }, { width: 12 },
|
{ width: 12 }, { width: 12 }, { width: 12 }, { width: 12 }, { width: 12 },
|
{ width: 12 }, { width: 12 },
|
];
|
},
|
|
getExportSummaries() {
|
// 提取数据,计算合计行
|
const data = this.tableData;
|
const sums = ["合计", "/", 0, 0, 0, 0, 0, 0, 0, "0%", 0, 0, 0, 0, "0%", "0%", "0%"];
|
// 数字列索引
|
const numIndices = [2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13];
|
data.forEach(row => {
|
numIndices.forEach(idx => {
|
const val = Number(row[this.getFieldByIndex(idx)]) || 0;
|
sums[idx] += val;
|
});
|
});
|
// 格式化数字
|
numIndices.forEach(idx => { sums[idx] = this.formatNumber(sums[idx]); });
|
// 计算两个百分比列的平均值
|
const followUpRates = data.map(row => parseFloat(this.calculateFollowUpRate(row).replace("%", ""))).filter(v => !isNaN(v));
|
const pushRates = data.map(row => parseFloat(this.calculatePushRate(row).replace("%", ""))).filter(v => !isNaN(v));
|
sums[15] = followUpRates.length ? (followUpRates.reduce((a,b)=>a+b,0)/followUpRates.length).toFixed(2)+"%" : "0.00%";
|
sums[16] = pushRates.length ? (pushRates.reduce((a,b)=>a+b,0)/pushRates.length).toFixed(2)+"%" : "0.00%";
|
// 成功率的合计用整体法计算
|
const totalSuccess = data.reduce((s, row) => s + (Number(row.followUpSuccess)||0), 0);
|
const totalNeed = data.reduce((s, row) => s + (Number(row.needFollowUp)||0), 0);
|
const totalPending = data.reduce((s, row) => s + (Number(row.pendingFollowUp)||0), 0);
|
const denom = totalNeed - totalPending;
|
sums[9] = denom > 0 ? ((totalSuccess/denom)*100).toFixed(2)+"%" : "0.00%";
|
const totalSuccessAgain = data.reduce((s, row) => s + (Number(row.followUpSuccessAgain)||0), 0);
|
const totalNeedAgain = data.reduce((s, row) => s + (Number(row.needFollowUpAgain)||0), 0);
|
const totalPendingAgain = data.reduce((s, row) => s + (Number(row.pendingFollowUpAgain)||0), 0);
|
const denomAgain = totalNeedAgain - totalPendingAgain;
|
sums[14] = denomAgain > 0 ? ((totalSuccessAgain/denomAgain)*100).toFixed(2)+"%" : "0.00%";
|
return sums;
|
},
|
|
getFieldByIndex(idx) {
|
// 根据导出列索引映射字段名
|
const map = {
|
2: "dischargeCount",
|
3: "nonFollowUp",
|
4: "followUpNeeded",
|
5: "needFollowUp",
|
6: "pendingFollowUp",
|
7: "followUpSuccess",
|
8: "followUpFail",
|
10: "needFollowUpAgain",
|
11: "pendingFollowUpAgain",
|
12: "followUpSuccessAgain",
|
13: "followUpFailAgain",
|
};
|
return map[idx];
|
},
|
|
// ===================== 导出医生子表 =====================
|
async exportDoctorTable(row) {
|
try {
|
const areaName = row.leavehospitaldistrictname || row.deptname || "未知病区";
|
let dateRangeString = "";
|
if (this.queryParams.dateRange && this.queryParams.dateRange.length === 2) {
|
const start = this.queryParams.dateRange[0].split(" ")[0];
|
const end = this.queryParams.dateRange[1].split(" ")[0];
|
dateRangeString = `${start}至${end}`;
|
} else {
|
dateRangeString = `${new Date().getMonth() + 1}月`;
|
}
|
const fileName = `${areaName}医生综合随访列表_${dateRangeString}.xlsx`;
|
const sheetName = `${areaName}医生综合随访`;
|
if (!row.doctorStats || row.doctorStats.length === 0) {
|
this.$message.warning("当前病区暂无医生数据");
|
return;
|
}
|
const workbook = new ExcelJS.Workbook();
|
const worksheet = workbook.addWorksheet(sheetName);
|
this.buildDoctorExportSheet(worksheet, row.doctorStats, areaName);
|
const buffer = await workbook.xlsx.writeBuffer();
|
saveAs(new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }), fileName);
|
this.$message.success("医生综合随访列表导出成功");
|
} catch (err) {
|
console.error(err);
|
this.$message.error("导出失败");
|
}
|
},
|
|
buildDoctorExportSheet(worksheet, data, areaName) {
|
const titleStyle = { font: { name: "微软雅黑", size: 16, bold: true }, alignment: { horizontal: "center", vertical: "middle" } };
|
const headerStyle = {
|
font: { name: "微软雅黑", size: 11, bold: true },
|
fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFF5F7FA" } },
|
alignment: { horizontal: "center", vertical: "middle", wrapText: true },
|
border: { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" } },
|
};
|
const cellStyle = {
|
font: { name: "宋体", size: 10 },
|
alignment: { horizontal: "center", vertical: "middle" },
|
border: { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" } },
|
};
|
const summaryStyle = { ...cellStyle, font: { ...cellStyle.font, bold: true } };
|
|
worksheet.mergeCells(1, 1, 1, 17);
|
worksheet.getCell(1, 1).value = `${areaName}医生综合随访列表`;
|
worksheet.getCell(1, 1).style = titleStyle;
|
worksheet.getRow(1).height = 30;
|
|
const headers = [
|
"医生姓名", "科室", "出院人次",
|
"首次无需随访", "首次应随访", "首次需随访", "首次待随访", "首次完成", "首次失败", "首次成功率",
|
"再次需随访", "再次待随访", "再次完成", "再次失败", "再次成功率",
|
"随访率", "推送率"
|
];
|
const headerRow = worksheet.addRow(headers);
|
headerRow.eachCell((cell) => { cell.style = headerStyle; });
|
worksheet.getRow(2).height = 25;
|
|
data.forEach(item => {
|
const row = worksheet.addRow([
|
item.drname,
|
item.deptname,
|
item.dischargeCount,
|
item.nonFollowUp,
|
item.followUpNeeded,
|
item.needFollowUp,
|
item.pendingFollowUp,
|
item.followUpSuccess,
|
item.followUpFail,
|
this.calculateSuccessRate(item.followUpSuccess, item.needFollowUp, item.pendingFollowUp),
|
item.needFollowUpAgain,
|
item.pendingFollowUpAgain,
|
item.followUpSuccessAgain,
|
item.followUpFailAgain,
|
this.calculateSuccessRate(item.followUpSuccessAgain, item.needFollowUpAgain, item.pendingFollowUpAgain),
|
this.calculateFollowUpRate(item),
|
this.calculatePushRate(item),
|
]);
|
row.eachCell((cell) => { cell.style = cellStyle; });
|
row.height = 24;
|
});
|
|
// 小计
|
const summaryRow = worksheet.addRow(this.getDoctorExportSummary(data));
|
summaryRow.eachCell((cell) => { cell.style = summaryStyle; });
|
|
worksheet.columns = Array(17).fill({ width: 12 });
|
worksheet.columns[0].width = 15;
|
worksheet.columns[1].width = 15;
|
},
|
|
getDoctorExportSummary(data) {
|
const sums = ["小计", "-", 0, 0, 0, 0, 0, 0, 0, "0%", 0, 0, 0, 0, "0%", "0%", "0%"];
|
const numIndices = [2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13];
|
data.forEach(row => {
|
numIndices.forEach(idx => {
|
const val = Number(row[this.getFieldByIndex(idx)]) || 0;
|
sums[idx] += val;
|
});
|
});
|
numIndices.forEach(idx => { sums[idx] = this.formatNumber(sums[idx]); });
|
// 平均值
|
const followUpRates = data.map(row => parseFloat(this.calculateFollowUpRate(row).replace("%", ""))).filter(v => !isNaN(v));
|
const pushRates = data.map(row => parseFloat(this.calculatePushRate(row).replace("%", ""))).filter(v => !isNaN(v));
|
sums[15] = followUpRates.length ? (followUpRates.reduce((a,b)=>a+b,0)/followUpRates.length).toFixed(2)+"%" : "0.00%";
|
sums[16] = pushRates.length ? (pushRates.reduce((a,b)=>a+b,0)/pushRates.length).toFixed(2)+"%" : "0.00%";
|
// 成功率整体法
|
const totalSuccess = data.reduce((s, row) => s + (Number(row.followUpSuccess)||0), 0);
|
const totalNeed = data.reduce((s, row) => s + (Number(row.needFollowUp)||0), 0);
|
const totalPending = data.reduce((s, row) => s + (Number(row.pendingFollowUp)||0), 0);
|
const denom = totalNeed - totalPending;
|
sums[9] = denom > 0 ? ((totalSuccess/denom)*100).toFixed(2)+"%" : "0.00%";
|
const totalSuccessAgain = data.reduce((s, row) => s + (Number(row.followUpSuccessAgain)||0), 0);
|
const totalNeedAgain = data.reduce((s, row) => s + (Number(row.needFollowUpAgain)||0), 0);
|
const totalPendingAgain = data.reduce((s, row) => s + (Number(row.pendingFollowUpAgain)||0), 0);
|
const denomAgain = totalNeedAgain - totalPendingAgain;
|
sums[14] = denomAgain > 0 ? ((totalSuccessAgain/denomAgain)*100).toFixed(2)+"%" : "0.00%";
|
return sums;
|
},
|
|
// ===================== Tooltips =====================
|
Tooltipcgl(h, { column }) {
|
return h("el-tooltip", {
|
props: { content: "随访成功/(需随访-待随访)", placement: "top", effect: "dark" },
|
}, [h("span", column.label)]);
|
},
|
TooltipFollowUpRate(h, { column }) {
|
return h("el-tooltip", {
|
props: { content: "(首次完成+首次无需随访)/(首次应随访+首次无需随访)", placement: "top", effect: "dark" },
|
}, [h("span", column.label)]);
|
},
|
TooltipPushRate(h, { column }) {
|
return h("el-tooltip", {
|
props: { content: "再次完成/(首次应随访+首次无需随访)", placement: "top", effect: "dark" },
|
}, [h("span", column.label)]);
|
},
|
},
|
};
|
</script>
|
|
<style lang="scss" scoped>
|
.combined-follow-up {
|
.your-table-container {
|
margin-top: 10px;
|
}
|
.button-zx {
|
color: rgb(70, 204, 238);
|
}
|
}
|
</style>
|