328 lines
12 KiB
Vue
328 lines
12 KiB
Vue
<template>
|
||
<div class="document-page">
|
||
<el-card class="mb-md" shadow="never">
|
||
<ty-search-form :schema="searchSchema" :model-value="query" @search="handleSearch" @reset="handleReset" />
|
||
</el-card>
|
||
|
||
<el-card class="document-page__table" shadow="never">
|
||
<ty-table
|
||
:columns="columns"
|
||
:data="list"
|
||
:loading="loading"
|
||
:total="total"
|
||
v-model:page="page"
|
||
v-model:page-size="pageSize"
|
||
:actions-width="100"
|
||
>
|
||
<template #actions="{ row }">
|
||
<el-button link type="primary" @click="openDetail(row)">查看</el-button>
|
||
</template>
|
||
</ty-table>
|
||
</el-card>
|
||
|
||
<el-dialog v-model="detailVisible" :title="`一人一档(${currentName})`" width="920px" top="6vh">
|
||
<div v-loading="detailLoading" class="document-page__detail">
|
||
<template v-if="detail">
|
||
<el-alert
|
||
v-if="detail.watermark"
|
||
:title="`${detail.watermark.orgName} · 导出时间 ${formatDateTime(detail.watermark.exportTime)} · ${detail.watermark.notice}`"
|
||
type="info"
|
||
:closable="false"
|
||
class="mb-md"
|
||
/>
|
||
|
||
<el-divider content-position="left">基本信息</el-divider>
|
||
<el-descriptions :column="3" border size="small">
|
||
<el-descriptions-item label="姓名">{{ detail.elder.name }}</el-descriptions-item>
|
||
<el-descriptions-item label="性别">{{ labelOf(GENDER_OPTIONS, detail.elder.gender) }}</el-descriptions-item>
|
||
<el-descriptions-item label="身份证号">{{ detail.elder.idCardNo }}</el-descriptions-item>
|
||
<el-descriptions-item label="出生日期">{{ detail.elder.birthDate ?? '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="联系电话">{{ detail.elder.phone ?? '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="居住地址">{{ detail.elder.address ?? '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="参保号">{{ detail.elder.insurNo ?? '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="护理等级">{{ labelOf(CARE_LEVEL_OPTIONS, detail.elder.careLevel) }}</el-descriptions-item>
|
||
<el-descriptions-item label="待遇状态">
|
||
<el-tag :type="TREAT_STATUS_TAG_MAP[detail.elder.treatStatus]?.type ?? 'info'" size="small">
|
||
{{ TREAT_STATUS_TAG_MAP[detail.elder.treatStatus]?.label ?? detail.elder.treatStatus }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="待遇起">{{ detail.elder.benefitStart ?? '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="待遇止">{{ detail.elder.benefitEnd ?? '—' }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
|
||
<el-divider content-position="left">服务计划</el-divider>
|
||
<div v-if="detail.plans.length > 0">
|
||
<div v-for="plan in detail.plans" :key="plan.id" class="mb-md">
|
||
<div class="mb-xs text-sm text-text-regular">
|
||
计划 {{ plan.id }} · {{ plan.planPeriodStart ?? '—' }} ~ {{ plan.planPeriodEnd ?? '—' }}
|
||
· {{ labelOf(CARE_MODE_OPTIONS, plan.careMode) }} · {{ planStatusLabel(plan.planStatus) }}
|
||
</div>
|
||
<el-table :data="plan.items" border size="small">
|
||
<el-table-column prop="svcName" label="项目" min-width="160" />
|
||
<el-table-column label="频次" min-width="120">
|
||
<template #default="{ row }">{{ freqLabel(row) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="时长(分)" width="90" align="center">
|
||
<template #default="{ row }">{{ row.durationMin ?? '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="拍照分档" width="100" align="center">
|
||
<template #default="{ row }">{{ photoTierLabel(row.photoTier) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
<el-empty v-else description="暂无服务计划" :image-size="60" />
|
||
|
||
<el-divider content-position="left">留痕记录</el-divider>
|
||
<el-table v-if="detail.records.length > 0" :data="detail.records" border size="small">
|
||
<el-table-column label="签到时间" width="160">
|
||
<template #default="{ row }">{{ formatDateTime(row.checkinTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="签退时间" width="160">
|
||
<template #default="{ row }">{{ formatDateTime(row.checkoutTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="时长(分)" width="90" align="center">
|
||
<template #default="{ row }">{{ row.durationMin ?? '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="90" align="center">
|
||
<template #default="{ row }">{{ recordStatusLabel(row.recordStatus) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="项目执行" min-width="200">
|
||
<template #default="{ row }">
|
||
<div v-for="it in row.items" :key="it.svcId" class="text-xs">
|
||
{{ it.svcName }}:{{ execResultLabel(it.execResult) }}{{ it.undoReason ? `(${it.undoReason})` : '' }}
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-empty v-else description="暂无留痕记录" :image-size="60" />
|
||
|
||
<el-divider content-position="left">异常上报</el-divider>
|
||
<el-table v-if="detail.abnormals.length > 0" :data="detail.abnormals" border size="small">
|
||
<el-table-column label="分类" width="130">
|
||
<template #default="{ row }">{{ abnormalTypeLabel(row.type) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="描述" min-width="180">
|
||
<template #default="{ row }">{{ row.desc ?? '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="处理状态" width="100" align="center">
|
||
<template #default="{ row }">{{ handleStatusLabel(row.handleStatus) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-empty v-else description="暂无异常上报" :image-size="60" />
|
||
</template>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<div class="flex w-full items-center gap-sm">
|
||
<el-date-picker
|
||
v-model="dateRange"
|
||
type="daterange"
|
||
value-format="YYYY-MM-DD"
|
||
start-placeholder="留痕起始"
|
||
end-placeholder="留痕结束"
|
||
range-separator="至"
|
||
class="flex-1"
|
||
/>
|
||
<el-button v-permission="'export:export'" type="primary" :loading="exporting" @click="download">
|
||
导出 JSON
|
||
</el-button>
|
||
<el-button @click="detailVisible = false">关闭</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { onMounted, reactive, ref, watch } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { ApiError } from '@/api/http'
|
||
import type { JsonSchema, TableColumn } from '@/types/crud'
|
||
import { useRequest } from '@/composables/useRequest'
|
||
import { listElders, type ElderListQuery } from '@/api/elder'
|
||
import {
|
||
exportDocument,
|
||
type DocumentExportData,
|
||
} from '@/api/document'
|
||
import { ABNORMAL_TYPE_LABEL_MAP, HANDLE_STATUS_TAG_MAP } from '@/api/abnormal'
|
||
import { EXEC_RESULT_TAG_MAP } from '@/api/record'
|
||
import {
|
||
CARE_LEVEL_OPTIONS,
|
||
CARE_MODE_OPTIONS,
|
||
GENDER_OPTIONS,
|
||
TREAT_STATUS_TAG_MAP,
|
||
formatDateTime,
|
||
labelOf,
|
||
} from '@/pages/ltc/elder/constants'
|
||
|
||
defineOptions({ name: 'document' })
|
||
|
||
const columns: TableColumn[] = [
|
||
{ prop: 'name', label: '姓名', minWidth: 100 },
|
||
{ prop: 'gender', label: '性别', width: 70, align: 'center', formatter: (v) => labelOf(GENDER_OPTIONS, v) },
|
||
{ prop: 'careLevel', label: '护理等级', minWidth: 140, formatter: (v) => labelOf(CARE_LEVEL_OPTIONS, v) },
|
||
{ prop: 'treatStatus', label: '待遇状态', width: 120, align: 'center', type: 'tag', tagMap: TREAT_STATUS_TAG_MAP },
|
||
{ prop: 'phone', label: '联系电话', width: 130, formatter: (v) => (v == null || v === '' ? '—' : String(v)) },
|
||
]
|
||
|
||
const searchSchema: JsonSchema = {
|
||
type: 'object',
|
||
properties: {
|
||
name: { type: 'string', title: '姓名', placeholder: '请输入姓名' },
|
||
},
|
||
}
|
||
|
||
const query = reactive<Record<string, any>>({ name: '' })
|
||
const list = ref<Record<string, any>[]>([])
|
||
const total = ref(0)
|
||
const page = ref(1)
|
||
const pageSize = ref(10)
|
||
|
||
const str = (v: unknown): string | undefined => (typeof v === 'string' && v !== '' ? v : undefined)
|
||
|
||
function toListQuery(): ElderListQuery {
|
||
return { page: page.value, pageSize: pageSize.value, name: str(query.name) }
|
||
}
|
||
|
||
const { loading, run: fetchList } = useRequest((signal: AbortSignal) => listElders(toListQuery(), signal), {
|
||
manual: true,
|
||
onSuccess: (result) => {
|
||
list.value = result.list.map((e) => ({ ...e }))
|
||
total.value = result.total
|
||
},
|
||
})
|
||
|
||
watch([page, pageSize], () => fetchList())
|
||
|
||
function handleSearch(): void {
|
||
if (page.value === 1) fetchList()
|
||
else page.value = 1
|
||
}
|
||
|
||
function handleReset(): void {
|
||
if (page.value === 1) fetchList()
|
||
else page.value = 1
|
||
}
|
||
|
||
// ---- 一人一档详情 ----
|
||
const detailVisible = ref(false)
|
||
const detailLoading = ref(false)
|
||
const detail = ref<DocumentExportData | null>(null)
|
||
const currentElderId = ref('')
|
||
const currentName = ref('')
|
||
const dateRange = ref<[string, string] | null>(null)
|
||
const exporting = ref(false)
|
||
|
||
const PHOTO_TIER_LABELS: Record<string, string> = {
|
||
must: '必拍',
|
||
free: '不拍',
|
||
optional: '选拍',
|
||
}
|
||
const PLAN_STATUS_LABELS: Record<string, string> = {
|
||
active: '生效中',
|
||
changed: '已变更',
|
||
}
|
||
|
||
function photoTierLabel(v: string | null): string {
|
||
return v ? PHOTO_TIER_LABELS[v] ?? v : '—'
|
||
}
|
||
|
||
function planStatusLabel(v: string): string {
|
||
return PLAN_STATUS_LABELS[v] ?? v
|
||
}
|
||
|
||
function freqLabel(item: { freqType?: string | null; freqValue?: number | null }): string {
|
||
const v = item.freqValue
|
||
switch (item.freqType) {
|
||
case 'daily':
|
||
return '每日'
|
||
case 'weekly_n':
|
||
return `每周 ${v ?? '—'} 次`
|
||
case 'monthly_n':
|
||
return `每月 ${v ?? '—'} 次`
|
||
case 'as_needed':
|
||
return '按需'
|
||
default:
|
||
return item.freqType ?? '—'
|
||
}
|
||
}
|
||
|
||
function recordStatusLabel(v: string): string {
|
||
return v === 'submitted' ? '已提交' : v === 'serving' ? '服务中' : v
|
||
}
|
||
|
||
function execResultLabel(v: string): string {
|
||
return EXEC_RESULT_TAG_MAP[v]?.label ?? v
|
||
}
|
||
|
||
function abnormalTypeLabel(v: string): string {
|
||
return ABNORMAL_TYPE_LABEL_MAP[v] ?? v
|
||
}
|
||
|
||
function handleStatusLabel(v: string): string {
|
||
return HANDLE_STATUS_TAG_MAP[v]?.label ?? v
|
||
}
|
||
|
||
async function openDetail(row: Record<string, any>): Promise<void> {
|
||
currentElderId.value = String(row.id)
|
||
currentName.value = String(row.name ?? '')
|
||
dateRange.value = null
|
||
detailVisible.value = true
|
||
detail.value = null
|
||
detailLoading.value = true
|
||
try {
|
||
detail.value = await exportDocument({ elderId: currentElderId.value })
|
||
} catch (e) {
|
||
if (e instanceof ApiError) ElMessage.error(e.message)
|
||
} finally {
|
||
detailLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 按当前时间范围导出并下载 JSON。 */
|
||
async function download(): Promise<void> {
|
||
if (!currentElderId.value) return
|
||
exporting.value = true
|
||
try {
|
||
const data = await exportDocument({
|
||
elderId: currentElderId.value,
|
||
startDate: dateRange.value?.[0],
|
||
endDate: dateRange.value?.[1],
|
||
})
|
||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||
const url = URL.createObjectURL(blob)
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
a.download = `文书档案_${currentName.value}_${new Date().toISOString().slice(0, 10)}.json`
|
||
a.click()
|
||
URL.revokeObjectURL(url)
|
||
ElMessage.success('已导出')
|
||
} catch (e) {
|
||
if (e instanceof ApiError) ElMessage.error(e.message)
|
||
} finally {
|
||
exporting.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => fetchList())
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.document-page {
|
||
@apply flex h-full flex-col;
|
||
}
|
||
|
||
.document-page__table {
|
||
@apply flex min-h-0 flex-1 flex-col;
|
||
|
||
:deep(.el-card__body) {
|
||
@apply flex min-h-0 flex-1 flex-col;
|
||
}
|
||
}
|
||
|
||
.document-page__detail {
|
||
@apply max-h-[62vh] overflow-auto;
|
||
}
|
||
</style>
|