| | |
| | | v-if="queryParams.statisticaltype == 1" |
| | | label="出院病区" |
| | | align="center" |
| | | sortable |
| | | key="leavehospitaldistrictname" |
| | | prop="leavehospitaldistrictname" |
| | | :show-overflow-tooltip="true" |
| | | :sort-method="sortChineseNumber" |
| | | min-width="120" |
| | | /> |
| | | |
| | |
| | | min-width="100" |
| | | > |
| | | <template slot-scope="scope"> |
| | | <span v-if="scope.row.followUpRate !== null && scope.row.followUpRate !== undefined"> |
| | | {{ formatPercent(scope.row.followUpRate) }} |
| | | <span |
| | | v-if=" |
| | | scope.row.followUpRate !== null && |
| | | scope.row.followUpRate !== undefined |
| | | " |
| | | > |
| | | {{ scope.row.followUpRate }} |
| | | </span> |
| | | <span v-else>-</span> |
| | | </template> |
| | |
| | | min-width="100" |
| | | > |
| | | <template slot-scope="scope"> |
| | | <span v-if="scope.row.joyTotal !== null && scope.row.joyTotal !== undefined"> |
| | | <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" |
| | | > |
| | | <el-table-column label="操作" align="center" fixed="right" width="120"> |
| | | <template slot-scope="scope"> |
| | | <el-button |
| | | type="text" |
| | | @click="getinfo(scope.row)" |
| | | > |
| | | <el-button type="text" @click="getinfo(scope.row)"> |
| | | <i class="el-icon-s-order" style="margin-right: 4px"></i> |
| | | 查看详情 |
| | | </el-button> |
| | |
| | | :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> |
| | | <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> |
| | | <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" |
| | | @close="topicVisible = false" |
| | | /> |
| | |
| | | </template> |
| | | |
| | | <script> |
| | | import { getSfStatisticsJoy, getSfStatisticsJoyInfo, selectTimelyRate } from "@/api/system/user"; |
| | | import { |
| | | getSfStatisticsJoy, |
| | | getSfStatisticsJoyInfo, |
| | | selectTimelyRate, |
| | | } from "@/api/system/user"; |
| | | import ExcelJS from "exceljs"; |
| | | import { saveAs } from "file-saver"; |
| | | import SeedetailsDialog from './components/SeedetailsDialog.vue'; |
| | | import TopicDialog from './components/TopicDialog.vue'; |
| | | import SeedetailsDialog from "./components/SeedetailsDialog.vue"; |
| | | import TopicDialog from "./components/TopicDialog.vue"; |
| | | |
| | | export default { |
| | | name: 'FollowupStatistics', |
| | | name: "FollowupStatistics", |
| | | components: { |
| | | SeedetailsDialog, |
| | | TopicDialog |
| | | TopicDialog, |
| | | }, |
| | | data() { |
| | | return { |
| | | // 查询参数 |
| | | queryParams: { |
| | | statisticaltype: 1, |
| | | leavehospitaldistrictcodes: [], |
| | | leavehospitaldistrictcodes: ["all"], |
| | | deptcodes: [], |
| | | serviceType: [2], |
| | | dateRange: [], |
| | | pageNum: 1, |
| | | pageSize: 20 |
| | | pageSize: 20, |
| | | }, |
| | | |
| | | // 统计类型列表 |
| | | Statisticallist: [ |
| | | { label: "病区统计", value: 1 }, |
| | | { label: "科室统计", value: 2 } |
| | | { label: "科室统计", value: 2 }, |
| | | ], |
| | | |
| | | // 病区列表 |
| | |
| | | // 满意度详情数据 |
| | | topiclist: [], |
| | | topicvalue: { |
| | | name: '' |
| | | name: "", |
| | | }, |
| | | |
| | | // 日期选择器选项 |
| | | pickerOptions: { |
| | | shortcuts: [ |
| | | { |
| | | text: '最近一周', |
| | | text: "最近一周", |
| | | onClick(picker) { |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setTime(start.getTime() - 3600 * 1000 * 24 * 7); |
| | | picker.$emit('pick', [start, end]); |
| | | } |
| | | picker.$emit("pick", [start, end]); |
| | | }, |
| | | }, |
| | | { |
| | | text: '最近一个月', |
| | | text: "最近一个月", |
| | | onClick(picker) { |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setTime(start.getTime() - 3600 * 1000 * 24 * 30); |
| | | picker.$emit('pick', [start, end]); |
| | | } |
| | | picker.$emit("pick", [start, end]); |
| | | }, |
| | | }, |
| | | { |
| | | text: '最近三个月', |
| | | text: "最近三个月", |
| | | onClick(picker) { |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setTime(start.getTime() - 3600 * 1000 * 24 * 90); |
| | | picker.$emit('pick', [start, end]); |
| | | } |
| | | } |
| | | picker.$emit("pick", [start, end]); |
| | | }, |
| | | }, |
| | | ], |
| | | disabledDate(time) { |
| | | return time.getTime() > Date.now(); |
| | | } |
| | | } |
| | | }, |
| | | }, |
| | | }; |
| | | }, |
| | | |
| | |
| | | this.options = this.$store.getters.tasktypes || []; |
| | | |
| | | // 获取科室列表 |
| | | this.flatArraydept = (this.$store.getters.belongDepts || []).map((dept) => { |
| | | return { |
| | | label: dept.deptName, |
| | | value: dept.deptCode |
| | | }; |
| | | }); |
| | | 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.flatArrayhospit = (this.$store.getters.belongWards || []).map( |
| | | (ward) => { |
| | | return { |
| | | label: ward.districtName, |
| | | value: ward.districtCode, |
| | | }; |
| | | } |
| | | ); |
| | | |
| | | // 添加全部选项 |
| | | this.flatArraydept.push({ label: "全部", value: "all" }); |
| | |
| | | // 处理查询参数 |
| | | const params = { |
| | | configKey: "joyCount", |
| | | ...this.queryParams |
| | | ...this.queryParams, |
| | | }; |
| | | |
| | | // 处理日期范围 |
| | | if (this.queryParams.dateRange && this.queryParams.dateRange.length === 2) { |
| | | if ( |
| | | this.queryParams.dateRange && |
| | | this.queryParams.dateRange.length === 2 |
| | | ) { |
| | | params.startTime = this.queryParams.dateRange[0]; |
| | | params.endTime = this.queryParams.dateRange[1]; |
| | | } |
| | |
| | | // 病区统计 |
| | | if (params.leavehospitaldistrictcodes.includes("all")) { |
| | | // 如果选择了"全部",则移除"all"值 |
| | | params.leavehospitaldistrictcodes = params.leavehospitaldistrictcodes.filter(item => item !== "all"); |
| | | params.leavehospitaldistrictcodes = |
| | | params.leavehospitaldistrictcodes.filter( |
| | | (item) => item !== "all" |
| | | ); |
| | | // 如果需要传所有病区代码,可以从store中获取 |
| | | params.leavehospitaldistrictcodes = (this.$store.getters.belongWards || []).map(ward => ward.districtCode); |
| | | params.leavehospitaldistrictcodes = ( |
| | | this.$store.getters.belongWards || [] |
| | | ).map((ward) => ward.districtCode); |
| | | } |
| | | } else if (params.statisticaltype == 2) { |
| | | // 科室统计 |
| | | if (params.deptcodes.includes("all")) { |
| | | // 如果选择了"全部",则移除"all"值 |
| | | params.deptcodes = params.deptcodes.filter(item => item !== "all"); |
| | | params.deptcodes = params.deptcodes.filter( |
| | | (item) => item !== "all" |
| | | ); |
| | | // 如果需要传所有科室代码,可以从store中获取 |
| | | params.deptcodes = (this.$store.getters.belongDepts || []).map(dept => dept.deptCode); |
| | | params.deptcodes = (this.$store.getters.belongDepts || []).map( |
| | | (dept) => dept.deptCode |
| | | ); |
| | | } |
| | | } |
| | | |
| | | const response = await getSfStatisticsJoy(params); |
| | | this.userList = response.data || []; |
| | | this.userList = this.customSort(response.data) || []; |
| | | this.total = response.total || 0; |
| | | } catch (error) { |
| | | console.error('获取统计列表失败:', error); |
| | | this.$message.error('获取数据失败'); |
| | | console.error("获取统计列表失败:", error); |
| | | this.$message.error("获取数据失败"); |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | }, |
| | | sortChineseNumber(aRow, bRow) { |
| | | const a = aRow.leavehospitaldistrictname; |
| | | const b = bRow.leavehospitaldistrictname; |
| | | |
| | | // 中文数字到阿拉伯数字的映射(扩展到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(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; |
| | | }); |
| | | }, |
| | | // 处理统计类型变化 |
| | | handleStatisticalTypeChange(value) { |
| | | if (value === 1) { |
| | |
| | | serviceType: [2], |
| | | dateRange: [], |
| | | pageNum: 1, |
| | | pageSize: 20 |
| | | pageSize: 20, |
| | | }; |
| | | this.getList(); |
| | | }, |
| | |
| | | |
| | | // 处理行选择 |
| | | handleSelectionChange(selection) { |
| | | this.ids = selection.map(item => item.id); |
| | | this.ids = selection.map((item) => item.id); |
| | | this.single = selection.length !== 1; |
| | | this.multiple = !selection.length; |
| | | }, |
| | |
| | | |
| | | // 格式化百分比 |
| | | formatPercent(value) { |
| | | if (value === null || value === undefined) return '-'; |
| | | if (value === null || value === undefined) return "-"; |
| | | const num = parseFloat(value); |
| | | if (isNaN(num)) return '-'; |
| | | if (isNaN(num)) return "-"; |
| | | return `${(num * 100).toFixed(2)}%`; |
| | | }, |
| | | |
| | |
| | | // 查看满意度详情 |
| | | async getinfo(row) { |
| | | this.currentRow = row; |
| | | this.topicVisible = true; |
| | | |
| | | try { |
| | | // 处理查询参数 |
| | | const params = { |
| | | configKey: "joyCount", |
| | | ...this.queryParams |
| | | ...this.queryParams, |
| | | }; |
| | | |
| | | // 处理日期范围 |
| | | if (this.queryParams.dateRange && this.queryParams.dateRange.length === 2) { |
| | | if ( |
| | | this.queryParams.dateRange && |
| | | this.queryParams.dateRange.length === 2 |
| | | ) { |
| | | params.startTime = this.queryParams.dateRange[0]; |
| | | params.endTime = this.queryParams.dateRange[1]; |
| | | } |
| | |
| | | |
| | | const response = await getSfStatisticsJoyInfo(params); |
| | | this.topiclist = response.data || []; |
| | | this.topicVisible = true; |
| | | |
| | | } catch (error) { |
| | | console.error('获取满意度详情失败:', error); |
| | | this.$message.error('获取详情失败'); |
| | | console.error("获取满意度详情失败:", error); |
| | | this.$message.error("获取详情失败"); |
| | | } |
| | | }, |
| | | |
| | | // 导出数据 |
| | | async handleExport() { |
| | | if (!this.userList.length) { |
| | | this.$message.warning('没有数据可导出'); |
| | | this.$message.warning("没有数据可导出"); |
| | | return; |
| | | } |
| | | |
| | |
| | | let dateRangeString = ""; |
| | | let sheetNameSuffix = ""; |
| | | |
| | | if (this.queryParams.dateRange && this.queryParams.dateRange.length === 2) { |
| | | if ( |
| | | this.queryParams.dateRange && |
| | | this.queryParams.dateRange.length === 2 |
| | | ) { |
| | | const startDateFormatted = this.queryParams.dateRange[0]; |
| | | const endDateFormatted = this.queryParams.dateRange[1]; |
| | | dateRangeString = `${startDateFormatted}至${endDateFormatted}`; |
| | |
| | | // 定义样式 |
| | | const titleStyle = { |
| | | font: { name: "微软雅黑", size: 16, bold: true }, |
| | | fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFE6F3FF" } }, |
| | | 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" } } |
| | | } |
| | | right: { style: "thin", color: { argb: "FFD0D0D0" } }, |
| | | }, |
| | | }; |
| | | |
| | | const headerStyle = { |
| | | font: { name: "微软雅黑", size: 11, bold: true }, |
| | | fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFF5F7FA" } }, |
| | | 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" } } |
| | | } |
| | | right: { style: "thin", color: { argb: "FFD0D0D0" } }, |
| | | }, |
| | | }; |
| | | |
| | | const cellStyle = { |
| | |
| | | top: { style: "thin", color: { argb: "FFD0D0D0" } }, |
| | | left: { style: "thin", color: { argb: "FFD0D0D0" } }, |
| | | bottom: { style: "thin", color: { argb: "FFD0D0D0" } }, |
| | | right: { style: "thin", color: { argb: "FFD0D0D0" } } |
| | | } |
| | | right: { style: "thin", color: { argb: "FFD0D0D0" } }, |
| | | }, |
| | | }; |
| | | |
| | | // 添加总标题 |
| | |
| | | "及时率", |
| | | "满意度题目总量", |
| | | "满意度填报量", |
| | | "完成比率" |
| | | "完成比率", |
| | | ]; |
| | | |
| | | const headerRow = worksheet.addRow(headers); |
| | |
| | | // 添加数据行 |
| | | this.userList.forEach((item) => { |
| | | const dataRow = worksheet.addRow([ |
| | | this.queryParams.statisticaltype == 1 ? item.leavehospitaldistrictname : item.deptname, |
| | | this.queryParams.statisticaltype == 1 |
| | | ? item.leavehospitaldistrictname |
| | | : item.deptname, |
| | | item.dischargeCount || 0, |
| | | item.nonFollowUp || 0, |
| | | item.followUpNeeded || 0, |
| | |
| | | item.rate ? this.formatPercent(item.rate) : "0%", |
| | | item.joyAllCount || 0, |
| | | item.joyCount || 0, |
| | | item.joyTotal ? this.formatPercent(item.joyTotal) : "0%" |
| | | item.joyTotal ? this.formatPercent(item.joyTotal) : "0%", |
| | | ]); |
| | | |
| | | dataRow.eachCell((cell) => { |
| | |
| | | { width: 12 }, |
| | | { width: 15 }, |
| | | { width: 15 }, |
| | | { width: 12 } |
| | | { width: 12 }, |
| | | ]; |
| | | |
| | | // 生成并下载文件 |
| | | const buffer = await workbook.xlsx.writeBuffer(); |
| | | const blob = new Blob([buffer], { |
| | | type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" |
| | | type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", |
| | | }); |
| | | |
| | | saveAs(blob, excelName); |
| | |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | }, |
| | | }; |
| | | </script> |
| | | |