| | |
| | | |
| | | // 加载状态 |
| | | loading: false, |
| | | orgname: "", |
| | | |
| | | // 选中的行 |
| | | ids: [], |
| | |
| | | }, |
| | | |
| | | created() { |
| | | this.orgname = localStorage.getItem("orgname"); |
| | | this.initData(); |
| | | }, |
| | | |
| | |
| | | } |
| | | }, |
| | | |
| | | // 获取指定病区/科室的题目详情 |
| | | async fetchTopicDetail(row) { |
| | | const params = { |
| | | configKey: "returnVisitCount", |
| | | ...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) { |
| | |
| | | pattern: "solid", |
| | | fgColor: { argb: "FFF5F7FA" }, |
| | | }, |
| | | alignment: { vertical: "middle", horizontal: "center" }, |
| | | alignment: { |
| | | vertical: "middle", |
| | | horizontal: "center", |
| | | wrapText: true, |
| | | }, |
| | | border: { |
| | | top: { style: "thin", color: { argb: "FFD0D0D0" } }, |
| | | left: { style: "thin", color: { argb: "FFD0D0D0" } }, |
| | |
| | | }, |
| | | }; |
| | | |
| | | // 添加总标题 |
| | | worksheet.mergeCells(1, 1, 1, 10); |
| | | const titleCell = worksheet.getCell(1, 1); |
| | | titleCell.value = `复诊通知统计表(${sheetNameSuffix})`; |
| | | titleCell.style = titleStyle; |
| | | worksheet.getRow(1).height = 35; |
| | | // 丽水市中医院:预先拉取题目明细,扁平化为「题目选项」列 |
| | | let lishuiTopics = []; |
| | | let lishuiRowQuantities = []; |
| | | if (isLishuiHospital) { |
| | | const built = await this.buildLishuiTopicColumns(); |
| | | lishuiTopics = built.topics; |
| | | lishuiRowQuantities = built.rowQuantities; |
| | | } |
| | | |
| | | // 添加表头 |
| | | const headers = [ |
| | | // 主表列 |
| | | const mainHeaders = [ |
| | | this.queryParams.statisticaltype == 1 ? "出院病区" : "科室", |
| | | "出院人次", |
| | | "无需随访人次", |
| | |
| | | "复诊通知填报量", |
| | | "完成比率", |
| | | ]; |
| | | const mainColumnCount = mainHeaders.length; |
| | | |
| | | const headerRow = worksheet.addRow(headers); |
| | | headerRow.eachCell((cell) => { |
| | | cell.style = headerStyle; |
| | | }); |
| | | headerRow.height = 25; |
| | | // 总列数(丽水市中医院在末尾追加题目选项列) |
| | | const topicColumnCount = lishuiTopics.reduce( |
| | | (sum, t) => sum + (t.options.length || 1), |
| | | 0 |
| | | ); |
| | | const totalColumns = mainColumnCount + topicColumnCount; |
| | | |
| | | // 添加数据行 |
| | | this.userList.forEach((item) => { |
| | | const dataRow = worksheet.addRow([ |
| | | 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%", |
| | | ]); |
| | | // 添加总标题 |
| | | worksheet.mergeCells(1, 1, 1, totalColumns); |
| | | const titleCell = worksheet.getCell(1, 1); |
| | | titleCell.value = `复诊通知统计表(${sheetNameSuffix})`; |
| | | titleCell.style = titleStyle; |
| | | worksheet.getRow(1).height = 35; |
| | | |
| | | dataRow.eachCell((cell) => { |
| | | cell.style = cellStyle; |
| | | 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 }; |
| | | }); |
| | | dataRow.height = 22; |
| | | }); |
| | | const maxHeaderLines = topicColumns.reduce( |
| | | (m, t) => Math.max(m, t.lines), |
| | | 1 |
| | | ); |
| | | |
| | | // 设置列宽 |
| | | worksheet.columns = [ |
| | | { width: 20 }, |
| | | { width: 12 }, |
| | | { width: 12 }, |
| | | { width: 12 }, |
| | | { width: 12 }, |
| | | { width: 12 }, |
| | | { width: 15 }, |
| | | { width: 15 }, |
| | | { width: 12 }, |
| | | ]; |
| | | // 两行表头:第一行 = 主表列 + 题目名;第二行 = 选项名 |
| | | 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(); |