<template>
|
<div class="second-follow-up">
|
<div class="your-table-container">
|
<el-table
|
ref="exportTableSecond"
|
id="exportTableidSecond"
|
v-loading="loading"
|
:data="tableData"
|
:border="true"
|
@selection-change="handleSelectionChange"
|
@expand-change="handleRowClick"
|
:row-key="getRowKey"
|
show-summary
|
:summary-method="getSummaries"
|
:expand-row-keys="expands"
|
>
|
<!-- 展开行箭头列 -->
|
<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="right">
|
<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="科室"
|
width="120"
|
prop="deptname"
|
align="center"
|
/>
|
<el-table-column
|
label="出院人次"
|
prop="dischargeCount"
|
align="center"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
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="无需随访人次"
|
align="center"
|
width="100"
|
key="nonFollowUp"
|
prop="nonFollowUp"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
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="应随访人次"
|
align="center"
|
width="100"
|
key="followUpNeeded"
|
prop="followUpNeeded"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
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 align="center" label="再次出院随访">
|
<el-table-column
|
label="需随访"
|
align="center"
|
key="needFollowUpAgain"
|
prop="needFollowUpAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
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="待随访"
|
align="center"
|
key="pendingFollowUpAgain"
|
prop="pendingFollowUpAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
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="随访成功"
|
align="center"
|
key="followUpSuccessAgain"
|
prop="followUpSuccessAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
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="随访失败"
|
align="center"
|
key="followUpFailAgain"
|
prop="followUpFailAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
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
|
label="成功率"
|
align="center"
|
width="120"
|
key="successRateAgain"
|
prop="successRateAgain"
|
>
|
<template slot-scope="scope">
|
<span class="success-rate">{{
|
calculateSuccessRate(
|
scope.row.followUpSuccessAgain,
|
scope.row.needFollowUpAgain,
|
scope.row.pendingFollowUpAgain
|
)
|
}}</span>
|
</template>
|
</el-table-column>
|
<el-table-column
|
label="随访率"
|
align="center"
|
width="120"
|
key="followUpRateAgain"
|
prop="followUpRateAgain"
|
/>
|
<el-table-column
|
label="人工"
|
align="center"
|
key="manualAgain"
|
prop="manualAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
type="text"
|
@click="
|
handleViewDetails(
|
scope.row,
|
'manualAgainInfo',
|
'人工随访列表',
|
'1'
|
)
|
"
|
>
|
<span class="button-zx">{{ scope.row.manualAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column
|
label="语音"
|
align="center"
|
key="voiceAgain"
|
prop="voiceAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
type="text"
|
@click="
|
handleViewDetails(
|
scope.row,
|
'voiceAgainInfo',
|
'语音随访列表',
|
'1'
|
)
|
"
|
>
|
<span class="button-zx">{{ scope.row.voiceAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column
|
label="短信"
|
align="center"
|
key="smsAgain"
|
prop="smsAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
type="text"
|
@click="
|
handleViewDetails(
|
scope.row,
|
'smsAgainInfo',
|
'短信随访列表',
|
'1'
|
)
|
"
|
>
|
<span class="button-zx">{{ scope.row.smsAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column
|
label="微信"
|
align="center"
|
key="weChatAgain"
|
prop="weChatAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
type="text"
|
@click="
|
handleViewDetails(
|
scope.row,
|
'weChatAgainInfo',
|
'微信随访列表',
|
'1'
|
)
|
"
|
>
|
<span class="button-zx">{{ scope.row.weChatAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table-column>
|
</el-table>
|
</template>
|
</el-table-column>
|
|
<!-- 表格列定义 -->
|
<el-table-column
|
v-if="queryParams.statisticaltype == 1"
|
label="出院病区"
|
align="center"
|
sortable
|
key="leavehospitaldistrictname"
|
prop="leavehospitaldistrictname"
|
width="150"
|
:show-overflow-tooltip="true"
|
:sort-method="sortChineseNumber"
|
/>
|
<el-table-column
|
v-if="queryParams.statisticaltype == 2"
|
label="科室"
|
align="center"
|
key="deptname"
|
prop="deptname"
|
:show-overflow-tooltip="true"
|
/>
|
<el-table-column
|
label="出院人次"
|
align="center"
|
key="dischargeCount"
|
prop="dischargeCount"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
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"
|
width="100"
|
key="nonFollowUp"
|
prop="nonFollowUp"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
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"
|
width="100"
|
key="followUpNeeded"
|
prop="followUpNeeded"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
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"
|
key="needFollowUpAgain"
|
prop="needFollowUpAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
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"
|
key="pendingFollowUpAgain"
|
prop="pendingFollowUpAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
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"
|
key="followUpSuccessAgain"
|
prop="followUpSuccessAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
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"
|
key="followUpFailAgain"
|
prop="followUpFailAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
type="text"
|
@click="
|
handleViewDetails(
|
scope.row,
|
'followUpFailAgainInfo',
|
'再次随访随访失败列表'
|
)
|
"
|
>
|
<span class="button-zx">{{ scope.row.followUpFailAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column
|
label="成功率"
|
align="center"
|
width="120"
|
key="successRateAgain"
|
prop="successRateAgain"
|
>
|
<template slot-scope="scope">
|
<span class="success-rate">{{
|
calculateSuccessRate(
|
scope.row.followUpSuccessAgain,
|
scope.row.needFollowUpAgain,
|
scope.row.pendingFollowUpAgain
|
)
|
}}</span>
|
</template>
|
</el-table-column>
|
<el-table-column
|
label="随访率"
|
align="center"
|
width="120"
|
key="followUpRateAgain"
|
prop="followUpRateAgain"
|
/>
|
<el-table-column
|
label="人工"
|
align="center"
|
key="manualAgain"
|
prop="manualAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
type="text"
|
@click="
|
handleViewDetails(
|
scope.row,
|
'manualAgainInfo',
|
'再次随访人工随访列表'
|
)
|
"
|
>
|
<span class="button-zx">{{ scope.row.manualAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column
|
label="语音"
|
align="center"
|
key="voiceAgain"
|
prop="voiceAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
type="text"
|
@click="
|
handleViewDetails(scope.row, 'voiceAgainInfo', '语音随访列表')
|
"
|
>
|
<span class="button-zx">{{ scope.row.voiceAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column
|
label="短信"
|
align="center"
|
key="smsAgain"
|
prop="smsAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
type="text"
|
@click="
|
handleViewDetails(
|
scope.row,
|
'smsAgainInfo',
|
'再次随访短信随访列表'
|
)
|
"
|
>
|
<span class="button-zx">{{ scope.row.smsAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column
|
label="微信"
|
align="center"
|
key="weChatAgain"
|
prop="weChatAgain"
|
>
|
<template slot-scope="scope">
|
<el-button
|
size="medium"
|
type="text"
|
@click="
|
handleViewDetails(
|
scope.row,
|
'weChatAgainInfo',
|
'再次随访微信随访列表'
|
)
|
"
|
>
|
<span class="button-zx">{{ scope.row.weChatAgain }}</span>
|
</el-button>
|
</template>
|
</el-table-column>
|
</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: "SecondFollowUp",
|
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: [],
|
ids: [],
|
tasktypes: store.getters.tasktypes,
|
};
|
},
|
methods: {
|
loadData() {
|
this.loading = true;
|
const params = {
|
...this.queryParams,
|
visitCount: 2,
|
leavehospitaldistrictcodes:
|
this.queryParams.leavehospitaldistrictcodes.includes("all")
|
? this.getAllWardCodes()
|
: this.queryParams.leavehospitaldistrictcodes,
|
deptcodes: this.queryParams.deptcodes.includes("all")
|
? this.getAllDeptCodes()
|
: this.queryParams.deptcodes,
|
};
|
|
delete params.leavehospitaldistrictcodes.all;
|
delete params.deptcodes.all;
|
params.rateDay = 7;
|
|
getSfStatistics(params)
|
.then((response) => {
|
this.tableData = this.customSort(response.data);
|
})
|
.catch((error) => {
|
console.error("获取再次随访数据失败:", error);
|
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) {
|
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: 2,
|
};
|
|
delete params.leavehospitaldistrictcodes.all;
|
delete params.deptcodes.all;
|
|
if (!row.doctorStats) {
|
this.loading = true;
|
params.rateDay = 7;
|
|
getSfStatistics(params).then((res) => {
|
this.$set(row, "doctorStats", res.data);
|
this.expands = [this.getRowKey(row)];
|
this.loading = false;
|
});
|
} else {
|
this.expands = [this.getRowKey(row)];
|
}
|
},
|
|
// 在统计汇总方法中处理成功率
|
getSummaries(param) {
|
const { columns, data } = param;
|
const sums = [];
|
|
columns.forEach((column, index) => {
|
if (index === 0) {
|
sums[index] = "合计";
|
return;
|
}
|
if (index === 1) {
|
sums[index] = "/";
|
return;
|
}
|
|
if (column.property === "successRateAgain") {
|
// 成功率需要重新计算总的成功率,而不是平均值
|
const totalSuccess = data.reduce((sum, item) => {
|
return sum + (Number(item.followUpSuccessAgain) || 0);
|
}, 0);
|
|
const totalNeed = data.reduce((sum, item) => {
|
return sum + (Number(item.needFollowUpAgain) || 0);
|
}, 0);
|
|
const totalPending = data.reduce((sum, item) => {
|
return sum + (Number(item.pendingFollowUpAgain) || 0);
|
}, 0);
|
|
const denominator = totalNeed - totalPending;
|
|
if (denominator > 0) {
|
sums[index] = ((totalSuccess / denominator) * 100).toFixed(2) + "%";
|
} else {
|
sums[index] = "0.00%";
|
}
|
} else if (column.property === "followUpRateAgain") {
|
const percentageValues = data
|
.map((item) => {
|
const value = item[column.property];
|
if (!value || value === "-" || value === "0%") return null;
|
if (typeof value === "string" && value.includes("%")) {
|
const numValue = parseFloat(value.replace("%", "")) / 100;
|
return isNaN(numValue) ? null : numValue;
|
} else {
|
const numValue = parseFloat(value);
|
return isNaN(numValue) ? null : numValue;
|
}
|
})
|
.filter((value) => value !== null && value !== 0);
|
|
if (percentageValues.length > 0) {
|
const average =
|
percentageValues.reduce((sum, value) => sum + value, 0) /
|
percentageValues.length;
|
sums[index] = (average * 100).toFixed(2) + "%";
|
} else {
|
sums[index] = "0.00%";
|
}
|
} else {
|
const values = data.map((item) => {
|
const value = item[column.property];
|
if (value === "-" || value === "" || value === null) return 0;
|
return Number(value) || 0;
|
});
|
|
if (!values.every((value) => isNaN(value))) {
|
sums[index] = values.reduce((prev, curr) => prev + curr, 0);
|
sums[index] = this.formatNumber(sums[index]);
|
} else {
|
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;
|
}
|
|
if (column.property === "successRateAgain") {
|
// 成功率需要重新计算总的成功率,而不是平均值
|
const totalSuccess = data.reduce((sum, item) => {
|
return sum + (Number(item.followUpSuccessAgain) || 0);
|
}, 0);
|
|
const totalNeed = data.reduce((sum, item) => {
|
return sum + (Number(item.needFollowUpAgain) || 0);
|
}, 0);
|
|
const totalPending = data.reduce((sum, item) => {
|
return sum + (Number(item.pendingFollowUpAgain) || 0);
|
}, 0);
|
|
const denominator = totalNeed - totalPending;
|
|
if (denominator > 0) {
|
sums[index] = ((totalSuccess / denominator) * 100).toFixed(2) + "%";
|
} else {
|
sums[index] = "0.00%";
|
}
|
} else if (column.property === "followUpRateAgain") {
|
const percentageValues = data
|
.map((item) => {
|
const value = item[column.property];
|
if (!value || value === "-" || value === "0%") return null;
|
if (typeof value === "string" && value.includes("%")) {
|
const numValue = parseFloat(value.replace("%", "")) / 100;
|
return isNaN(numValue) ? null : numValue;
|
} else {
|
const numValue = parseFloat(value);
|
return isNaN(numValue) ? null : numValue;
|
}
|
})
|
.filter((value) => value !== null && value !== 0);
|
|
if (percentageValues.length > 0) {
|
const average =
|
percentageValues.reduce((sum, value) => sum + value, 0) /
|
percentageValues.length;
|
sums[index] = (average * 100).toFixed(2) + "%";
|
} else {
|
sums[index] = "0.00%";
|
}
|
} else {
|
const values = data.map((item) => {
|
const value = item[column.property];
|
if (value === "-" || value === "" || value === null) return 0;
|
return Number(value) || 0;
|
});
|
|
if (!values.every((value) => isNaN(value))) {
|
sums[index] = values.reduce((prev, curr) => prev + curr, 0);
|
sums[index] = this.formatNumber(sums[index]);
|
} else {
|
sums[index] = "-";
|
}
|
}
|
});
|
|
return sums;
|
},
|
|
formatNumber(num) {
|
if (isNaN(num)) return "-";
|
return Number.isInteger(num) ? num.toString() : num.toFixed(0);
|
},
|
|
handleSelectionChange(selection) {
|
this.ids = selection.map((item) => item.tagid);
|
},
|
|
handleViewDetails(row, infoKey, titleSuffix, type) {
|
const title = `${
|
row.leavehospitaldistrictname || row.deptname
|
}${titleSuffix}`;
|
this.$emit("view-details", row, infoKey, title, type);
|
},
|
// 计算成功率的方法
|
calculateSuccessRate(followUpSuccess, needFollowUp, pendingFollowUp) {
|
const success = Number(followUpSuccess) || 0;
|
const need = Number(needFollowUp) || 0;
|
const pending = Number(pendingFollowUp) || 0;
|
|
// 分母 = 需随访 - 待随访
|
const denominator = need - pending;
|
|
if (denominator <= 0) {
|
return "0.00%";
|
}
|
|
const rate = (success / denominator) * 100;
|
return rate.toFixed(2) + "%";
|
},
|
async exportTable() {
|
try {
|
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}月`;
|
}
|
}
|
|
// 根据 serviceType 生成随访类型名称
|
let serviceTypeName = "出院随访"; // 文件名使用的名称
|
let sheetTypeName = "再次随访"; // 工作表使用的名称(简化版)
|
console.log(this.queryParams.serviceType);
|
|
if (
|
this.queryParams.serviceType &&
|
Array.isArray(this.queryParams.serviceType) &&
|
this.queryParams.serviceType.length > 0
|
) {
|
if (this.tasktypes && Array.isArray(this.tasktypes)) {
|
// 过滤出匹配的随访类型
|
const matchedTypes = this.tasktypes.filter((task) =>
|
this.queryParams.serviceType.includes(task.value)
|
);
|
|
if (matchedTypes.length === 1) {
|
// 单个类型
|
const label = matchedTypes[0].label;
|
serviceTypeName = label;
|
sheetTypeName = label;
|
} else if (matchedTypes.length > 1) {
|
// 多个类型
|
const typeNames = matchedTypes.map((task) => task.label);
|
|
// 文件名:用斜杠分隔
|
serviceTypeName = typeNames.join("/");
|
|
// 工作表名:使用第一个类型或简化名称
|
if (matchedTypes.length <= 2) {
|
// 如果只有2个类型,都显示
|
sheetTypeName = `${typeNames[0]}等`;
|
} else {
|
// 如果超过2个类型,只显示第一个
|
sheetTypeName = `${typeNames[0]}等`;
|
}
|
} else if (this.queryParams.serviceType.length > 0) {
|
// 如果没有匹配的,使用原始值
|
const typeStr = this.queryParams.serviceType.join("/");
|
serviceTypeName = typeStr;
|
sheetTypeName = "再次随访";
|
}
|
} else if (this.queryParams.serviceType.length > 0) {
|
// 如果没有 tasktypes,使用原始值
|
const typeStr = this.queryParams.serviceType.join("/");
|
serviceTypeName = typeStr;
|
sheetTypeName = "再次随访";
|
}
|
}
|
|
const excelName = `再次${serviceTypeName}统计表_${dateRangeString}.xlsx`;
|
|
// 清理工作表名称,移除非法字符
|
const cleanSheetName = (name) => {
|
// Excel工作表名不能包含的字符: * ? : \ / [ ]
|
return name.replace(/[*?:\\/[\]]/g, ' ');
|
};
|
|
const worksheetName = cleanSheetName(`再次${sheetTypeName}统计_${sheetNameSuffix}`);
|
|
if (!this.tableData || this.tableData.length === 0) {
|
this.$message.warning(`暂无再次${serviceTypeName}数据可导出`);
|
return false;
|
}
|
|
const workbook = new ExcelJS.Workbook();
|
const worksheet = workbook.addWorksheet(worksheetName);
|
|
// 构建表格
|
this.buildExportSheet(worksheet, sheetNameSuffix);
|
|
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(`导出失败: ${error.message}`);
|
return false;
|
}
|
},
|
/** 导出医生子表(再次随访) */
|
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" },
|
},
|
};
|
|
// 标题
|
worksheet.mergeCells(1, 1, 1, 10);
|
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.needFollowUpAgain,
|
item.pendingFollowUpAgain,
|
item.followUpSuccessAgain,
|
item.followUpFailAgain,
|
this.calculateSuccessRate(
|
item.followUpSuccessAgain,
|
item.needFollowUpAgain,
|
item.pendingFollowUpAgain
|
),
|
item.followUpRateAgain,
|
]);
|
row.eachCell((cell) => {
|
cell.style = cellStyle;
|
});
|
});
|
|
// 小计行
|
const summaryRow = worksheet.addRow(
|
this.getDoctorAgainExportSummary(data)
|
);
|
summaryRow.eachCell((cell) => {
|
cell.font = { bold: true };
|
cell.fill = {
|
type: "pattern",
|
pattern: "solid",
|
fgColor: { argb: "FFF5F7FA" },
|
};
|
});
|
|
// 列宽
|
worksheet.columns = [
|
{ width: 15 },
|
{ width: 15 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 12 }, // 成功率
|
{ width: 12 }, // 随访率
|
];
|
},
|
/** 再次随访 - 医生子表导出小计 */
|
getDoctorAgainExportSummary(data) {
|
const sums = ["小计"];
|
|
const keys = [
|
"dischargeCount",
|
"nonFollowUp",
|
"followUpNeeded",
|
"needFollowUpAgain",
|
"pendingFollowUpAgain",
|
"followUpSuccessAgain",
|
"followUpFailAgain",
|
];
|
|
keys.forEach((key) => {
|
sums.push(data.reduce((t, r) => t + (Number(r[key]) || 0), 0));
|
});
|
|
// 成功率(平均值)
|
const successRates = data
|
.map((item) => {
|
const success = Number(item.followUpSuccessAgain) || 0;
|
const need = Number(item.needFollowUpAgain) || 0;
|
const pending = Number(item.pendingFollowUpAgain) || 0;
|
const denominator = need - pending;
|
if (denominator <= 0) return 0;
|
return success / denominator;
|
})
|
.filter((rate) => !isNaN(rate));
|
|
sums.push(
|
successRates.length
|
? (
|
(successRates.reduce((a, b) => a + b, 0) / successRates.length) *
|
100
|
).toFixed(2) + "%"
|
: "0.00%"
|
);
|
|
// 随访率(平均值)
|
const followUpRates = data
|
.map((i) => this.extractPercentageValue(i.followUpRateAgain))
|
.filter(Boolean);
|
|
sums.push(
|
followUpRates.length
|
? (
|
(followUpRates.reduce((a, b) => a + b, 0) /
|
followUpRates.length) *
|
100
|
).toFixed(2) + "%"
|
: "0.00%"
|
);
|
|
return sums;
|
},
|
buildExportSheet(worksheet, sheetNameSuffix) {
|
const titleStyle = {
|
font: {
|
name: "微软雅黑",
|
size: 16,
|
bold: true,
|
color: { argb: "FF000000" },
|
},
|
fill: {
|
type: "pattern",
|
pattern: "solid",
|
fgColor: { argb: "FFE6F3FF" },
|
},
|
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 headerStyle = {
|
font: {
|
name: "微软雅黑",
|
size: 11,
|
bold: true,
|
color: { argb: "FF000000" },
|
},
|
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, color: { argb: "FF000000" } },
|
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 summaryStyle = {
|
font: {
|
name: "宋体",
|
size: 10,
|
bold: true,
|
color: { argb: "FF409EFF" },
|
},
|
fill: {
|
type: "pattern",
|
pattern: "solid",
|
fgColor: { argb: "FFF5F7FA" },
|
},
|
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" } },
|
},
|
};
|
|
// 添加标题行
|
worksheet.mergeCells(1, 1, 1, 15);
|
const titleCell = worksheet.getCell(1, 1);
|
titleCell.value = `再次出院随访统计表_${sheetNameSuffix}`;
|
titleCell.style = titleStyle;
|
worksheet.getRow(1).height = 35;
|
|
// 表头
|
// 表头需要增加成功率列
|
const secondRowHeaders = [
|
"",
|
"出院病区",
|
"科室",
|
"出院人次",
|
"无需随访人次",
|
"应随访人次",
|
"需随访",
|
"待随访",
|
"随访成功",
|
"随访失败",
|
"成功率", // 成功率应该在随访失败后面
|
"随访率", // 随访率在成功率后面
|
"人工",
|
"语音", // 修正:应该是语音,不是短信
|
"短信", // 短信
|
"微信", // 微信
|
];
|
|
secondRowHeaders.forEach((header, index) => {
|
const cell = worksheet.getCell(3, index + 1);
|
cell.value = header;
|
cell.style = headerStyle;
|
});
|
|
// 更新合并单元格的范围
|
for (let i = 1; i <= 6; i++) {
|
worksheet.mergeCells(2, i, 3, i);
|
const cell = worksheet.getCell(2, i);
|
cell.style = headerStyle;
|
}
|
|
worksheet.getCell(2, 1).value = "";
|
worksheet.getCell(2, 2).value = "出院病区";
|
worksheet.getCell(2, 3).value = "科室";
|
worksheet.getCell(2, 4).value = "出院人次";
|
worksheet.getCell(2, 5).value = "无需随访人次";
|
worksheet.getCell(2, 6).value = "应随访人次";
|
|
// 注意:由于增加了成功率列,合并列数要增加
|
worksheet.mergeCells(2, 7, 2, 16); // 从7合并到16(原来是7-14)
|
worksheet.getCell(2, 7).value = "再次出院随访";
|
worksheet.getCell(2, 7).style = headerStyle;
|
|
worksheet.getRow(2).height = 28;
|
worksheet.getRow(3).height = 25;
|
|
// 数据行
|
this.tableData.forEach((item, rowIndex) => {
|
const dataRow = worksheet.addRow(
|
[
|
"",
|
item.leavehospitaldistrictname || "",
|
item.deptname || "",
|
item.dischargeCount || 0,
|
item.nonFollowUp || 0,
|
item.followUpNeeded || 0,
|
item.needFollowUpAgain || 0,
|
item.pendingFollowUpAgain || 0,
|
item.followUpSuccessAgain || 0,
|
item.followUpFailAgain || 0,
|
// 成功率 - 需要动态计算
|
this.calculateSuccessRate(
|
item.followUpSuccessAgain,
|
item.needFollowUpAgain,
|
item.pendingFollowUpAgain
|
),
|
item.followUpRateAgain || "0%", // 随访率
|
item.manualAgain || 0,
|
item.voiceAgain || 0,
|
item.smsAgain || 0,
|
item.weChatAgain || 0,
|
],
|
rowIndex + 4
|
);
|
|
dataRow.eachCell((cell) => {
|
cell.style = cellStyle;
|
});
|
dataRow.height = 24;
|
});
|
|
// 合计行
|
const summaries = this.getExportSummaries();
|
const summaryRow = worksheet.addRow(summaries);
|
summaryRow.eachCell((cell, colNumber) => {
|
cell.style = summaryStyle;
|
if (colNumber === 1) {
|
cell.value = "合计";
|
}
|
});
|
summaryRow.height = 28;
|
|
// 列宽
|
// 修正列宽
|
worksheet.columns = [
|
{ width: 8 },
|
{ width: 20 },
|
{ width: 15 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 12 },
|
{ width: 10 },
|
{ width: 10 },
|
{ width: 10 },
|
{ width: 10 },
|
{ width: 12 }, // 成功率
|
{ width: 12 }, // 随访率
|
{ width: 8 }, // 人工
|
{ width: 8 }, // 语音
|
{ width: 8 }, // 短信
|
{ width: 8 }, // 微信
|
];
|
},
|
|
getExportSummaries() {
|
const summaries = [
|
"合计",
|
"/",
|
"/",
|
0, // 3: dischargeCount
|
0, // 4: nonFollowUp
|
0, // 5: followUpNeeded
|
0, // 6: needFollowUpAgain
|
0, // 7: pendingFollowUpAgain
|
0, // 8: followUpSuccessAgain
|
0, // 9: followUpFailAgain
|
"0%", // 10: 成功率
|
"0%", // 11: 随访率
|
0, // 12: manualAgain
|
0, // 13: voiceAgain
|
0, // 14: smsAgain
|
0, // 15: weChatAgain
|
];
|
|
this.tableData.forEach((item) => {
|
summaries[3] += Number(item.dischargeCount) || 0;
|
summaries[4] += Number(item.nonFollowUp) || 0;
|
summaries[5] += Number(item.followUpNeeded) || 0;
|
summaries[6] += Number(item.needFollowUpAgain) || 0;
|
summaries[7] += Number(item.pendingFollowUpAgain) || 0;
|
summaries[8] += Number(item.followUpSuccessAgain) || 0;
|
summaries[9] += Number(item.followUpFailAgain) || 0;
|
summaries[12] += Number(item.manualAgain) || 0;
|
summaries[13] += Number(item.voiceAgain) || 0;
|
summaries[14] += Number(item.smsAgain) || 0;
|
summaries[15] += Number(item.weChatAgain) || 0;
|
});
|
|
// 成功率计算
|
const totalSuccess = summaries[8]; // followUpSuccessAgain的总和
|
const totalNeed = summaries[6]; // needFollowUpAgain的总和
|
const totalPending = summaries[7]; // pendingFollowUpAgain的总和
|
const denominator = totalNeed - totalPending;
|
|
if (denominator > 0) {
|
summaries[10] = ((totalSuccess / denominator) * 100).toFixed(2) + "%";
|
} else {
|
summaries[10] = "0.00%";
|
}
|
|
// 随访率计算
|
const followUpRateAgainValues = this.tableData
|
.map((item) => this.extractPercentageValue(item.followUpRateAgain))
|
.filter((value) => value !== null);
|
|
if (followUpRateAgainValues.length > 0) {
|
const avgFollowUpRateAgain =
|
followUpRateAgainValues.reduce((sum, val) => sum + val, 0) /
|
followUpRateAgainValues.length;
|
summaries[11] = (avgFollowUpRateAgain * 100).toFixed(2) + "%";
|
}
|
|
// 格式化数字
|
[3, 4, 5, 6, 7, 8, 9, 12, 13, 14, 15].forEach((index) => {
|
summaries[index] = this.formatNumber(summaries[index]);
|
});
|
|
return summaries;
|
},
|
|
extractPercentageValue(value) {
|
if (!value) return null;
|
if (typeof value === "string" && value.includes("%")) {
|
const num = parseFloat(value.replace("%", ""));
|
return isNaN(num) ? null : num / 100;
|
}
|
const num = parseFloat(value);
|
return isNaN(num) ? null : num;
|
},
|
},
|
};
|
</script>
|
|
<style lang="scss" scoped>
|
.second-follow-up {
|
.your-table-container {
|
margin-top: 10px;
|
}
|
|
.button-zx {
|
color: rgb(70, 204, 238);
|
}
|
}
|
</style>
|