liuyun 532a8cc76c feat(c5): 后台 7 个配置/文书页
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-15 07:46:48 +08:00

328 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>