|
|
@@ -1,9 +1,5 @@
|
|
|
<template>
|
|
|
- <div
|
|
|
- v-loading="loading"
|
|
|
- class="oa-page"
|
|
|
- element-loading-text="正在加载在院长者档案,数据较多请稍候…"
|
|
|
- >
|
|
|
+ <div v-loading="loading" class="oa-page" element-loading-text="正在加载长者结构统计…">
|
|
|
<ContentWrap>
|
|
|
<div class="page-header">
|
|
|
<div class="header-title">
|
|
|
@@ -42,12 +38,7 @@
|
|
|
<el-form-item>
|
|
|
<el-button type="primary" :icon="Search" @click="loadAll">查询</el-button>
|
|
|
<el-button :icon="RefreshLeft" @click="handleReset">重置</el-button>
|
|
|
- <el-button
|
|
|
- type="success"
|
|
|
- :icon="Download"
|
|
|
- :disabled="!elders.length"
|
|
|
- @click="handleExport"
|
|
|
- >
|
|
|
+ <el-button type="success" :icon="Download" :disabled="!hasData" @click="handleExport">
|
|
|
导出汇总
|
|
|
</el-button>
|
|
|
</el-form-item>
|
|
|
@@ -58,7 +49,7 @@
|
|
|
type="warning"
|
|
|
:closable="false"
|
|
|
show-icon
|
|
|
- title="长者档案数据加载失败,请稍后刷新重试"
|
|
|
+ title="长者结构数据加载失败,请稍后刷新重试"
|
|
|
class="api-tip"
|
|
|
/>
|
|
|
|
|
|
@@ -83,7 +74,7 @@
|
|
|
<ContentWrap>
|
|
|
<div class="panel-title">性别结构</div>
|
|
|
<div class="donut-wrap">
|
|
|
- <Echart v-if="elders.length" :options="sexOption" height="340px" />
|
|
|
+ <Echart v-if="hasData" :options="sexOption" height="340px" />
|
|
|
<el-empty v-else description="暂无数据" :image-size="80" class="chart-empty" />
|
|
|
</div>
|
|
|
</ContentWrap>
|
|
|
@@ -91,7 +82,7 @@
|
|
|
<el-col :xs="24" :lg="15">
|
|
|
<ContentWrap>
|
|
|
<div class="panel-title">年龄段分布</div>
|
|
|
- <Echart v-if="elders.length" :options="ageOption" height="340px" />
|
|
|
+ <Echart v-if="hasData" :options="ageOption" height="340px" />
|
|
|
<el-empty v-else description="暂无数据" :image-size="80" class="chart-empty" />
|
|
|
</ContentWrap>
|
|
|
</el-col>
|
|
|
@@ -104,14 +95,14 @@
|
|
|
护理等级分布 TOP10
|
|
|
<span class="title-tip">按在院长者当前护理等级名称统计</span>
|
|
|
</div>
|
|
|
- <Echart v-if="elders.length" :options="nurseLevelOption" height="380px" />
|
|
|
+ <Echart v-if="hasData" :options="nurseLevelOption" height="380px" />
|
|
|
<el-empty v-else description="暂无数据" :image-size="80" class="chart-empty" />
|
|
|
</ContentWrap>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :lg="10">
|
|
|
<ContentWrap>
|
|
|
<div class="panel-title">入住时长分布</div>
|
|
|
- <Echart v-if="elders.length" :options="stayOption" height="380px" />
|
|
|
+ <Echart v-if="hasData" :options="stayOption" height="380px" />
|
|
|
<el-empty v-else description="暂无数据" :image-size="80" class="chart-empty" />
|
|
|
</ContentWrap>
|
|
|
</el-col>
|
|
|
@@ -121,14 +112,14 @@
|
|
|
<el-col :xs="24" :lg="12">
|
|
|
<ContentWrap>
|
|
|
<div class="panel-title">文化程度分布</div>
|
|
|
- <Echart v-if="elders.length" :options="educationOption" height="340px" />
|
|
|
+ <Echart v-if="hasData" :options="educationOption" height="340px" />
|
|
|
<el-empty v-else description="暂无数据" :image-size="80" class="chart-empty" />
|
|
|
</ContentWrap>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :lg="12">
|
|
|
<ContentWrap>
|
|
|
<div class="panel-title">缴费方式分布</div>
|
|
|
- <Echart v-if="elders.length" :options="paymentOption" height="340px" />
|
|
|
+ <Echart v-if="hasData" :options="paymentOption" height="340px" />
|
|
|
<el-empty v-else description="暂无数据" :image-size="80" class="chart-empty" />
|
|
|
</ContentWrap>
|
|
|
</el-col>
|
|
|
@@ -181,9 +172,14 @@
|
|
|
import { Download, Refresh, RefreshLeft, Search } from '@element-plus/icons-vue'
|
|
|
import Echart from '@/components/Echart/src/Echart.vue'
|
|
|
import { exportWithCustomHeaders } from '@/utils/excel-export'
|
|
|
-import { getStrDictOptions, getDictLabel, DICT_TYPE } from '@/utils/dict'
|
|
|
+import { getDictLabel, DICT_TYPE } from '@/utils/dict'
|
|
|
import { getCampusOptions } from '../../overview/campus-data'
|
|
|
-import { fetchInHospitalElders, type InHospitalElderVO } from '../occupancy-api'
|
|
|
+import {
|
|
|
+ fetchElderStructure,
|
|
|
+ type DictBucket,
|
|
|
+ type ElderStructureData,
|
|
|
+ type StructureBucket
|
|
|
+} from '../occupancy-api'
|
|
|
|
|
|
defineOptions({ name: 'OaOccupancyElderStructure' })
|
|
|
|
|
|
@@ -194,122 +190,92 @@ const loading = ref(false)
|
|
|
const loadFailed = ref(false)
|
|
|
const updateTime = ref('')
|
|
|
const selectedCampusIds = ref<number[]>(campusOptions.map((item) => item.value))
|
|
|
-const elders = ref<InHospitalElderVO[]>([])
|
|
|
-
|
|
|
-const educationDict = getStrDictOptions(DICT_TYPE.DEGREE_EDUCATION_ARR)
|
|
|
-const paymentDict = getStrDictOptions(DICT_TYPE.PAYMENT_METHOD_ARR)
|
|
|
-
|
|
|
-const dictLabel = (dict: any[], value: number | null) =>
|
|
|
- value === null || value === undefined ? '未设置' : getDictLabel(dict, String(value)) || '未设置'
|
|
|
-
|
|
|
-const pad2 = (n: number) => String(n).padStart(2, '0')
|
|
|
-
|
|
|
-/* ---------------- 聚合 ---------------- */
|
|
|
-
|
|
|
-const countBy = (fn: (e: InHospitalElderVO) => string) => {
|
|
|
- const map = new Map<string, number>()
|
|
|
- elders.value.forEach((e) => {
|
|
|
- const key = fn(e)
|
|
|
- map.set(key, (map.get(key) || 0) + 1)
|
|
|
- })
|
|
|
- return map
|
|
|
+const data = ref<ElderStructureData | null>(null)
|
|
|
+const hasData = computed(() => (data.value?.total ?? 0) > 0)
|
|
|
+
|
|
|
+/** 后端分段短标签 → 页面展示文案映射(未匹配则原样展示) */
|
|
|
+const AGE_LABEL_MAP: Record<string, string> = {
|
|
|
+ '<60': '60岁以下',
|
|
|
+ '60-69': '60-69岁',
|
|
|
+ '70-79': '70-79岁',
|
|
|
+ '80-89': '80-89岁',
|
|
|
+ '90-99': '90-99岁',
|
|
|
+ '≥100': '100岁及以上',
|
|
|
+ 未设置: '未设置'
|
|
|
+}
|
|
|
+const STAY_LABEL_MAP: Record<string, string> = {
|
|
|
+ '<1年': '1年以内',
|
|
|
+ '1-3年': '1-3年',
|
|
|
+ '3-5年': '3-5年',
|
|
|
+ '5-10年': '5-10年',
|
|
|
+ '≥10年': '10年及以上',
|
|
|
+ 未设置: '未设置'
|
|
|
}
|
|
|
|
|
|
-const average = (nums: number[]) =>
|
|
|
- nums.length ? Math.round((nums.reduce((a, b) => a + b, 0) / nums.length) * 10) / 10 : null
|
|
|
-
|
|
|
-const stats = computed(() => {
|
|
|
- const list = elders.value
|
|
|
- const total = list.length
|
|
|
- const male = list.filter((e) => Number(e.elderSex) === 1).length
|
|
|
- const ages = list.map((e) => Number(e.elderAge)).filter((v) => !Number.isNaN(v) && v > 0)
|
|
|
- const now = Date.now()
|
|
|
- const stayMonthsList = list
|
|
|
- .filter((e) => e.checkInTime && e.checkInTime > 0)
|
|
|
- .map((e) => (now - (e.checkInTime as number)) / 1000 / 60 / 60 / 24 / 30.4375)
|
|
|
- return {
|
|
|
- total,
|
|
|
- male,
|
|
|
- female: list.filter((e) => Number(e.elderSex) === 2).length,
|
|
|
- sexUnknown: total - male - list.filter((e) => Number(e.elderSex) === 2).length,
|
|
|
- avgAge: average(ages),
|
|
|
- over90: list.filter((e) => Number(e.elderAge) >= 90).length,
|
|
|
- avgStayMonths: average(stayMonthsList)
|
|
|
- }
|
|
|
+/** 分段标签颜色(按顺序循环) */
|
|
|
+const AGE_COLORS = ['#95d4ff', '#67b8ff', '#409eff', '#36a866', '#e6a23c', '#f56c6c', '#909399']
|
|
|
+
|
|
|
+/* ---------------- KPI ---------------- */
|
|
|
+
|
|
|
+const kpiCards = computed(() => {
|
|
|
+ const d = data.value
|
|
|
+ const total = d?.total ?? 0
|
|
|
+ const male = d?.maleCount ?? 0
|
|
|
+ const female = d?.femaleCount ?? 0
|
|
|
+ return [
|
|
|
+ { label: '在院长者', value: total, unit: '人', icon: 'ep:user', theme: 'blue' },
|
|
|
+ {
|
|
|
+ label: '平均年龄',
|
|
|
+ value: d?.avgAge ?? '-',
|
|
|
+ unit: '岁',
|
|
|
+ icon: 'ep:calendar',
|
|
|
+ theme: 'cyan'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '男性',
|
|
|
+ value: male,
|
|
|
+ unit: '人',
|
|
|
+ sub: total ? `占比 ${((male / total) * 100).toFixed(1)}%` : '',
|
|
|
+ icon: 'ep:male',
|
|
|
+ theme: 'green'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '女性',
|
|
|
+ value: female,
|
|
|
+ unit: '人',
|
|
|
+ sub: total ? `占比 ${((female / total) * 100).toFixed(1)}%` : '',
|
|
|
+ icon: 'ep:female',
|
|
|
+ theme: 'pink'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '90岁及以上',
|
|
|
+ value: d?.over90Count ?? 0,
|
|
|
+ unit: '人',
|
|
|
+ icon: 'ep:histogram',
|
|
|
+ theme: 'orange'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '平均入住时长',
|
|
|
+ value: d?.avgStayMonths ?? '-',
|
|
|
+ unit: '月',
|
|
|
+ icon: 'ep:timer',
|
|
|
+ theme: 'purple'
|
|
|
+ }
|
|
|
+ ]
|
|
|
})
|
|
|
|
|
|
-const kpiCards = computed(() => [
|
|
|
- { label: '在院长者', value: stats.value.total, unit: '人', icon: 'ep:user', theme: 'blue' },
|
|
|
- {
|
|
|
- label: '平均年龄',
|
|
|
- value: stats.value.avgAge ?? '-',
|
|
|
- unit: '岁',
|
|
|
- icon: 'ep:calendar',
|
|
|
- theme: 'cyan'
|
|
|
- },
|
|
|
- {
|
|
|
- label: '男性',
|
|
|
- value: stats.value.male,
|
|
|
- unit: '人',
|
|
|
- sub: stats.value.total
|
|
|
- ? `占比 ${((stats.value.male / stats.value.total) * 100).toFixed(1)}%`
|
|
|
- : '',
|
|
|
- icon: 'ep:male',
|
|
|
- theme: 'green'
|
|
|
- },
|
|
|
- {
|
|
|
- label: '女性',
|
|
|
- value: stats.value.female,
|
|
|
- unit: '人',
|
|
|
- sub: stats.value.total
|
|
|
- ? `占比 ${((stats.value.female / stats.value.total) * 100).toFixed(1)}%`
|
|
|
- : '',
|
|
|
- icon: 'ep:female',
|
|
|
- theme: 'pink'
|
|
|
- },
|
|
|
- {
|
|
|
- label: '90岁及以上',
|
|
|
- value: stats.value.over90,
|
|
|
- unit: '人',
|
|
|
- icon: 'ep:histogram',
|
|
|
- theme: 'orange'
|
|
|
- },
|
|
|
- {
|
|
|
- label: '平均入住时长',
|
|
|
- value: stats.value.avgStayMonths ?? '-',
|
|
|
- unit: '月',
|
|
|
- icon: 'ep:timer',
|
|
|
- theme: 'purple'
|
|
|
- }
|
|
|
-])
|
|
|
-
|
|
|
-/** 年龄段(养老行业通用分段) */
|
|
|
-const AGE_BUCKETS = [
|
|
|
- { label: '60岁以下', test: (a: number) => a < 60 },
|
|
|
- { label: '60-69岁', test: (a: number) => a >= 60 && a < 70 },
|
|
|
- { label: '70-79岁', test: (a: number) => a >= 70 && a < 80 },
|
|
|
- { label: '80-89岁', test: (a: number) => a >= 80 && a < 90 },
|
|
|
- { label: '90-99岁', test: (a: number) => a >= 90 && a < 100 },
|
|
|
- { label: '100岁及以上', test: (a: number) => a >= 100 }
|
|
|
-]
|
|
|
-
|
|
|
-/** 入住时长分段(年) */
|
|
|
-const STAY_BUCKETS = [
|
|
|
- { label: '1年以内', test: (m: number) => m < 12 },
|
|
|
- { label: '1-3年', test: (m: number) => m >= 12 && m < 36 },
|
|
|
- { label: '3-5年', test: (m: number) => m >= 36 && m < 60 },
|
|
|
- { label: '5-10年', test: (m: number) => m >= 60 && m < 120 },
|
|
|
- { label: '10年及以上', test: (m: number) => m >= 120 }
|
|
|
-]
|
|
|
-
|
|
|
/* ---------------- 图表 ---------------- */
|
|
|
|
|
|
const sexOption = computed(() => {
|
|
|
- const s = stats.value
|
|
|
- const data = [
|
|
|
- { name: '男', value: s.male, itemStyle: { color: '#409eff' } },
|
|
|
- { name: '女', value: s.female, itemStyle: { color: '#ec5f86' } }
|
|
|
- ].filter((item) => item.value > 0)
|
|
|
+ const buckets = data.value?.sexBuckets ?? []
|
|
|
+ const colorMap: Record<string, string> = {
|
|
|
+ 男: '#409eff',
|
|
|
+ 女: '#ec5f86',
|
|
|
+ 未设置: '#909399'
|
|
|
+ }
|
|
|
+ const chartData = buckets
|
|
|
+ .filter((b) => b.count > 0)
|
|
|
+ .map((b) => ({ name: b.label, value: b.count, itemStyle: { color: colorMap[b.label] } }))
|
|
|
return {
|
|
|
tooltip: { trigger: 'item', formatter: '{b}:{c} 人({d}%)' },
|
|
|
legend: { bottom: 0, icon: 'circle' },
|
|
|
@@ -319,50 +285,52 @@ const sexOption = computed(() => {
|
|
|
radius: ['50%', '70%'],
|
|
|
center: ['50%', '46%'],
|
|
|
label: { formatter: '{b}\n{c} 人' },
|
|
|
- data
|
|
|
+ data: chartData
|
|
|
}
|
|
|
]
|
|
|
}
|
|
|
})
|
|
|
|
|
|
-const ageOption = computed(() => {
|
|
|
- const counts = AGE_BUCKETS.map(
|
|
|
- (b) => elders.value.filter((e) => Number(e.elderAge) > 0 && b.test(Number(e.elderAge))).length
|
|
|
- )
|
|
|
- return {
|
|
|
- tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: '{b}:{c} 人' },
|
|
|
- grid: { left: 48, right: 24, top: 24, bottom: 30 },
|
|
|
- xAxis: { type: 'category', data: AGE_BUCKETS.map((b) => b.label) },
|
|
|
- yAxis: { type: 'value', name: '人', minInterval: 1 },
|
|
|
- series: [
|
|
|
- {
|
|
|
- type: 'bar',
|
|
|
- barWidth: 28,
|
|
|
- data: counts,
|
|
|
- itemStyle: {
|
|
|
- borderRadius: [4, 4, 0, 0],
|
|
|
- color: (p: any) =>
|
|
|
- ['#95d4ff', '#67b8ff', '#409eff', '#36a866', '#e6a23c', '#f56c6c'][p.dataIndex]
|
|
|
- },
|
|
|
- label: { show: true, position: 'top' }
|
|
|
- }
|
|
|
- ]
|
|
|
- }
|
|
|
+const barOption = (
|
|
|
+ buckets: StructureBucket[],
|
|
|
+ colors: string[],
|
|
|
+ labelMap?: Record<string, string>
|
|
|
+) => ({
|
|
|
+ tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: '{b}:{c} 人' },
|
|
|
+ grid: { left: 48, right: 24, top: 24, bottom: 30 },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: buckets.map((b) => (labelMap ? (labelMap[b.label] ?? b.label) : b.label))
|
|
|
+ },
|
|
|
+ yAxis: { type: 'value', name: '人', minInterval: 1 },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'bar',
|
|
|
+ barWidth: 28,
|
|
|
+ data: buckets.map((b, i) => ({
|
|
|
+ value: b.count,
|
|
|
+ itemStyle: { borderRadius: [4, 4, 0, 0], color: colors[i % colors.length] }
|
|
|
+ })),
|
|
|
+ label: { show: true, position: 'top' }
|
|
|
+ }
|
|
|
+ ]
|
|
|
})
|
|
|
|
|
|
+const ageOption = computed(() => barOption(data.value?.ageBuckets ?? [], AGE_COLORS, AGE_LABEL_MAP))
|
|
|
+const stayOption = computed(() =>
|
|
|
+ barOption(data.value?.stayBuckets ?? [], ['#1fa9bf'], STAY_LABEL_MAP)
|
|
|
+)
|
|
|
+
|
|
|
const nurseLevelOption = computed(() => {
|
|
|
- const map = countBy((e) => e.nurseLevelName || '未设置')
|
|
|
- const list = Array.from(map.entries())
|
|
|
- .map(([name, count]) => ({ name, count }))
|
|
|
- .sort((a, b) => a.count - b.count)
|
|
|
- .slice(-10)
|
|
|
+ // 后端按人数降序返回;echart 类目轴第一项在底部,反转让人数最多的显示在顶部
|
|
|
+ const list = [...(data.value?.nurseLevelBuckets ?? [])].slice(0, 10).reverse()
|
|
|
return {
|
|
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: '{b}:{c} 人' },
|
|
|
grid: { left: 130, right: 48, top: 10, bottom: 10, containLabel: true },
|
|
|
xAxis: { type: 'value', minInterval: 1 },
|
|
|
yAxis: {
|
|
|
type: 'category',
|
|
|
- data: list.map((i) => i.name),
|
|
|
+ data: list.map((i) => i.nurseLevelName || '未设置'),
|
|
|
axisLabel: { width: 120, overflow: 'truncate' }
|
|
|
},
|
|
|
series: [
|
|
|
@@ -377,42 +345,16 @@ const nurseLevelOption = computed(() => {
|
|
|
}
|
|
|
})
|
|
|
|
|
|
-const stayOption = computed(() => {
|
|
|
- const now = Date.now()
|
|
|
- const monthsOf = (e: InHospitalElderVO) =>
|
|
|
- e.checkInTime ? (now - e.checkInTime) / 1000 / 60 / 60 / 24 / 30.4375 : null
|
|
|
- const counts = STAY_BUCKETS.map(
|
|
|
- (b) =>
|
|
|
- elders.value.filter((e) => {
|
|
|
- const m = monthsOf(e)
|
|
|
- return m !== null && b.test(m)
|
|
|
- }).length
|
|
|
- )
|
|
|
- return {
|
|
|
- tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: '{b}:{c} 人' },
|
|
|
- grid: { left: 48, right: 24, top: 24, bottom: 30 },
|
|
|
- xAxis: { type: 'category', data: STAY_BUCKETS.map((b) => b.label) },
|
|
|
- yAxis: { type: 'value', name: '人', minInterval: 1 },
|
|
|
- series: [
|
|
|
- {
|
|
|
- type: 'bar',
|
|
|
- barWidth: 24,
|
|
|
- data: counts,
|
|
|
- itemStyle: { borderRadius: [4, 4, 0, 0], color: '#1fa9bf' },
|
|
|
- label: { show: true, position: 'top' }
|
|
|
- }
|
|
|
- ]
|
|
|
- }
|
|
|
-})
|
|
|
-
|
|
|
-const distributionOption = (
|
|
|
- dict: any[],
|
|
|
- valueOf: (e: InHospitalElderVO) => number | null,
|
|
|
- color: string
|
|
|
-) => {
|
|
|
- const map = countBy((e) => dictLabel(dict, valueOf(e)))
|
|
|
- const list = Array.from(map.entries())
|
|
|
- .map(([name, count]) => ({ name, count }))
|
|
|
+/** 字典类分布:后端返回字典原值,前端按字典类型实时翻译展示 */
|
|
|
+const dictDistributionOption = (buckets: DictBucket[], dictType: string, color: string) => {
|
|
|
+ const list = buckets
|
|
|
+ .map((b) => ({
|
|
|
+ name:
|
|
|
+ b.value === null || b.value === undefined || b.value === ''
|
|
|
+ ? '未设置'
|
|
|
+ : getDictLabel(dictType, b.value) || '未设置',
|
|
|
+ count: b.count
|
|
|
+ }))
|
|
|
.sort((a, b) => b.count - a.count)
|
|
|
return {
|
|
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: '{b}:{c} 人' },
|
|
|
@@ -436,77 +378,54 @@ const distributionOption = (
|
|
|
}
|
|
|
|
|
|
const educationOption = computed(() =>
|
|
|
- distributionOption(educationDict, (e) => e.degreeEducation, '#36a866')
|
|
|
+ dictDistributionOption(
|
|
|
+ data.value?.educationBuckets ?? [],
|
|
|
+ DICT_TYPE.DEGREE_EDUCATION_ARR,
|
|
|
+ '#36a866'
|
|
|
+ )
|
|
|
)
|
|
|
const paymentOption = computed(() =>
|
|
|
- distributionOption(paymentDict, (e) => e.paymentMethod, '#f5851d')
|
|
|
+ dictDistributionOption(data.value?.paymentBuckets ?? [], DICT_TYPE.PAYMENT_METHOD_ARR, '#f5851d')
|
|
|
)
|
|
|
|
|
|
/* ---------------- 院区明细表 ---------------- */
|
|
|
|
|
|
-const campusNameOf = (e: InHospitalElderVO) => {
|
|
|
- const opt = campusOptions.find((item) => item.value === Number(e.tenantId))
|
|
|
- return opt?.label || e.tenantName || `院区${e.tenantId ?? '-'}`
|
|
|
-}
|
|
|
-
|
|
|
-const campusRows = computed(() => {
|
|
|
- const map = new Map<string, InHospitalElderVO[]>()
|
|
|
- elders.value.forEach((e) => {
|
|
|
- const name = campusNameOf(e)
|
|
|
- const arr = map.get(name) || []
|
|
|
- arr.push(e)
|
|
|
- map.set(name, arr)
|
|
|
- })
|
|
|
- return Array.from(map.entries())
|
|
|
- .map(([campusName, list]) => {
|
|
|
- const male = list.filter((e) => Number(e.elderSex) === 1).length
|
|
|
- const ages = list.map((e) => Number(e.elderAge)).filter((v) => v > 0)
|
|
|
- const now = Date.now()
|
|
|
- const stays = list
|
|
|
- .filter((e) => e.checkInTime)
|
|
|
- .map((e) => (now - (e.checkInTime as number)) / 1000 / 60 / 60 / 24 / 30.4375)
|
|
|
- const nurseMap = new Map<string, number>()
|
|
|
- list.forEach((e) => {
|
|
|
- const name = e.nurseLevelName || '未设置'
|
|
|
- nurseMap.set(name, (nurseMap.get(name) || 0) + 1)
|
|
|
- })
|
|
|
- const main = Array.from(nurseMap.entries()).sort((a, b) => b[1] - a[1])[0]
|
|
|
- return {
|
|
|
- campusName,
|
|
|
- total: list.length,
|
|
|
- male,
|
|
|
- female: list.length - male,
|
|
|
- maleRate: list.length ? ((male / list.length) * 100).toFixed(1) + '%' : '-',
|
|
|
- avgAge: average(ages),
|
|
|
- over80: list.filter((e) => Number(e.elderAge) >= 80).length,
|
|
|
- avgStayMonths: average(stays),
|
|
|
- mainNurseLevel: main ? `${main[0]}(${main[1]}人)` : ''
|
|
|
- }
|
|
|
- })
|
|
|
+const campusRows = computed(() =>
|
|
|
+ [...(data.value?.campusStats ?? [])]
|
|
|
+ .map((c) => ({
|
|
|
+ campusName: c.tenantName || `院区${c.tenantId}`,
|
|
|
+ total: c.total,
|
|
|
+ male: c.maleCount,
|
|
|
+ female: c.femaleCount,
|
|
|
+ maleRate: c.total ? ((c.maleCount / c.total) * 100).toFixed(1) + '%' : '-',
|
|
|
+ avgAge: c.avgAge,
|
|
|
+ over80: c.over80Count,
|
|
|
+ avgStayMonths: c.avgStayMonths,
|
|
|
+ mainNurseLevel: c.mainNurseLevelName || ''
|
|
|
+ }))
|
|
|
.sort((a, b) => b.total - a.total)
|
|
|
-})
|
|
|
+)
|
|
|
|
|
|
/* ---------------- 加载与导出 ---------------- */
|
|
|
|
|
|
+const pad2 = (n: number) => String(n).padStart(2, '0')
|
|
|
+
|
|
|
const loadAll = async () => {
|
|
|
+ // 空选 = 下拉范围内全部院区
|
|
|
const ids = selectedCampusIds.value.length
|
|
|
? selectedCampusIds.value
|
|
|
: campusOptions.map((item) => item.value)
|
|
|
- if (!ids.length) {
|
|
|
- elders.value = []
|
|
|
- return
|
|
|
- }
|
|
|
loading.value = true
|
|
|
loadFailed.value = false
|
|
|
try {
|
|
|
- elders.value = await fetchInHospitalElders(ids)
|
|
|
+ data.value = await fetchElderStructure(ids)
|
|
|
const now = new Date()
|
|
|
updateTime.value = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())} ${pad2(
|
|
|
now.getHours()
|
|
|
)}:${pad2(now.getMinutes())}`
|
|
|
} catch {
|
|
|
loadFailed.value = true
|
|
|
- elders.value = []
|
|
|
+ data.value = null
|
|
|
} finally {
|
|
|
loading.value = false
|
|
|
}
|