feat(c1): 服务对象列表/建档/详情/状态变更
- elder api 封装(RESTful /elder 六接口)+ 十态常量与日期格式化 - 列表页用 ty-crud(姓名/性别/护理等级/待遇状态/出生日期 + 姓名·状态筛选) - 建档表单(身份证 18 位校验+出生日期带出、等级绑定评估结论副本联动校验) - 详情弹窗(完整信息 + 十态状态变更 + 状态日志时间线) - ty-crud 增 showCreate、ty-dialog 注入 dialog-id 以支持自定义建档与自关闭详情 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
56056ec3d0
commit
8975a7dd63
14
components.d.ts
vendored
14
components.d.ts
vendored
@ -22,7 +22,11 @@ declare module 'vue' {
|
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
@ -49,8 +53,11 @@ declare module 'vue' {
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
Icon: typeof import('./src/components/icon/icon.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
@ -88,7 +95,11 @@ declare global {
|
||||
const ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
const ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||
const ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
const ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
const ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
const ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
const ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
const ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
const ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
const ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
const ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
@ -115,8 +126,11 @@ declare global {
|
||||
const ElTable: typeof import('element-plus/es')['ElTable']
|
||||
const ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
const ElTag: typeof import('element-plus/es')['ElTag']
|
||||
const ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||
const ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||
const ElTree: typeof import('element-plus/es')['ElTree']
|
||||
const ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
const ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
const Icon: typeof import('./src/components/icon/icon.vue')['default']
|
||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||
const RouterView: typeof import('vue-router')['RouterView']
|
||||
|
||||
125
src/api/elder.ts
Normal file
125
src/api/elder.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { http } from '@/api/http'
|
||||
import type { PageResult } from '@/types/crud'
|
||||
|
||||
/**
|
||||
* 老人档案(elder)实体。字段字典锚点见 shared/data-model spec(ltc_elder)。
|
||||
* - 日期字段(birthDate/benefitStart 等)后端经 TransformInterceptor 序列化为毫秒时间戳;
|
||||
* - idCardNo 列表返回掩码、详情(admin/qc)返回明文;
|
||||
* - assessEvidenceFile 为评估结论副本的 MinIO key。
|
||||
*/
|
||||
export interface Elder {
|
||||
id: string
|
||||
locCode?: string
|
||||
name: string
|
||||
gender: string
|
||||
idCardNo: string
|
||||
birthDate?: number
|
||||
phone?: string | null
|
||||
address?: string
|
||||
guardianName?: string | null
|
||||
guardianPhone?: string | null
|
||||
insurNo?: string
|
||||
insurType?: string
|
||||
insurYears?: number | null
|
||||
insurVerifyStatus?: string | null
|
||||
insurVerifyDate?: number | null
|
||||
assessLevel?: string | null
|
||||
careLevel?: string | null
|
||||
assessDate?: number | null
|
||||
assessOrg?: string | null
|
||||
assessEvidenceFile?: string | null
|
||||
benefitStart?: number | null
|
||||
benefitEnd?: number | null
|
||||
treatStatus: string
|
||||
statusSource?: string | null
|
||||
statusChangeDate?: number | null
|
||||
hospStart?: number | null
|
||||
hospEnd?: number | null
|
||||
note?: string | null
|
||||
createdAt?: number
|
||||
updatedAt?: number
|
||||
}
|
||||
|
||||
export interface ElderListQuery {
|
||||
page: number
|
||||
pageSize: number
|
||||
name?: string
|
||||
treatStatus?: string
|
||||
}
|
||||
|
||||
/** 建档入参(日期字段为 YYYY-MM-DD 字符串,与 staff 建档入参一致)。 */
|
||||
export interface ElderCreateData {
|
||||
locCode: string
|
||||
name: string
|
||||
gender: string
|
||||
idCardNo: string
|
||||
phone?: string
|
||||
address: string
|
||||
guardianName?: string
|
||||
guardianPhone?: string
|
||||
insurNo: string
|
||||
insurType: string
|
||||
insurYears?: number
|
||||
insurVerifyStatus?: string
|
||||
insurVerifyDate?: string
|
||||
assessLevel?: string
|
||||
careLevel?: string
|
||||
assessDate?: string
|
||||
assessOrg?: string
|
||||
assessEvidenceFile?: string
|
||||
benefitStart?: string
|
||||
benefitEnd?: string
|
||||
hospStart?: string
|
||||
hospEnd?: string
|
||||
note?: string
|
||||
}
|
||||
|
||||
export interface ElderStatusLog {
|
||||
id: string
|
||||
elderId: string
|
||||
fromStatus: string
|
||||
toStatus: string
|
||||
changeReason: string
|
||||
source: string
|
||||
operatorId: string
|
||||
evidenceFile: string | null
|
||||
changedAt: number
|
||||
}
|
||||
|
||||
/** 老人分页列表(身份证服务端掩码)。 */
|
||||
export function listElders(query: ElderListQuery, signal?: AbortSignal): Promise<PageResult<Elder>> {
|
||||
const params: Record<string, unknown> = { page: query.page, pageSize: query.pageSize }
|
||||
if (query.name) params.name = query.name
|
||||
if (query.treatStatus) params.treatStatus = query.treatStatus
|
||||
return http.get<PageResult<Elder>>('/elder', { params, signal })
|
||||
}
|
||||
|
||||
/** 建档。 */
|
||||
export function createElder(data: ElderCreateData): Promise<Elder> {
|
||||
return http.post<Elder>('/elder', data)
|
||||
}
|
||||
|
||||
/** 详情(admin/qc 身份证明文并留痕,其余掩码)。 */
|
||||
export function getElder(id: string): Promise<Elder> {
|
||||
return http.get<Elder>(`/elder/${id}`)
|
||||
}
|
||||
|
||||
/** 变更待遇状态(changeReason 必填)。 */
|
||||
export function changeElderStatus(
|
||||
id: string,
|
||||
data: { toStatus: string; changeReason: string },
|
||||
): Promise<{ id: string; fromStatus: string; toStatus: string }> {
|
||||
return http.post(`/elder/${id}/status`, data)
|
||||
}
|
||||
|
||||
/** 状态变更历史(按时间倒序)。 */
|
||||
export function getStatusLog(id: string): Promise<ElderStatusLog[]> {
|
||||
return http.get<ElderStatusLog[]>(`/elder/${id}/status/log`)
|
||||
}
|
||||
|
||||
/** 上传评估结论副本到 MinIO,返回 file key。 */
|
||||
export function uploadEvidence(file: File): Promise<{ key: string }> {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
return http.post<{ key: string }>('/elder/assess-evidence', fd)
|
||||
}
|
||||
@ -12,7 +12,7 @@
|
||||
<el-card class="ty-crud__table" shadow="never">
|
||||
<div class="ty-crud__toolbar">
|
||||
<div class="ty-crud__toolbar-left">
|
||||
<el-button v-if="!createPermission || permissionStore.has(createPermission)" type="primary" @click="openCreate">
|
||||
<el-button v-if="showCreate && (!createPermission || permissionStore.has(createPermission))" type="primary" @click="openCreate">
|
||||
新增{{ entityName }}
|
||||
</el-button>
|
||||
<slot name="toolbar" />
|
||||
@ -122,6 +122,8 @@ const props = withDefaults(
|
||||
selection?: boolean
|
||||
/** 新增按钮所需权限码(无该权限且非超管时隐藏新增按钮)。 */
|
||||
createPermission?: string
|
||||
/** 是否显示内置「新增」按钮(false 时可改用 #toolbar 插槽自定义建档入口)。 */
|
||||
showCreate?: boolean
|
||||
actionsWidth?: number | string
|
||||
}>(),
|
||||
{
|
||||
@ -132,6 +134,7 @@ const props = withDefaults(
|
||||
actions: () => ['edit', 'delete'],
|
||||
collapseCount: 2,
|
||||
selection: false,
|
||||
showCreate: true,
|
||||
actionsWidth: 140,
|
||||
},
|
||||
)
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
<component
|
||||
:is="dialog.options.component"
|
||||
v-bind="dialog.options.props ?? {}"
|
||||
:dialog-id="dialog.id"
|
||||
:ref="setRef(dialog.id)"
|
||||
/>
|
||||
<template v-if="dialog.options.showFooter !== false" #footer>
|
||||
|
||||
126
src/pages/ltc/elder/components/elder-detail.vue
Normal file
126
src/pages/ltc/elder/components/elder-detail.vue
Normal file
@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="elder-detail">
|
||||
<template v-if="elder">
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="姓名">{{ elder.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ labelOf(GENDER_OPTIONS, elder.gender) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号">{{ elder.idCardNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出生日期">{{ formatDate(elder.birthDate) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ elder.phone ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="居住地址">{{ elder.address ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="参保地">{{ elder.locCode ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="医保参保号">{{ elder.insurNo ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="参保类型">{{ labelOf(INSURE_TYPE_OPTIONS, elder.insurType) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="参保核验">{{ labelOf(INSURE_VERIFY_STATUS_OPTIONS, elder.insurVerifyStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="失能等级">{{ labelOf(ASSESS_LEVEL_OPTIONS, elder.assessLevel) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="护理等级">{{ labelOf(CARE_LEVEL_OPTIONS, elder.careLevel) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="评估机构">{{ elder.assessOrg ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="评估日期">{{ formatDate(elder.assessDate) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="待遇有效期起">{{ formatDate(elder.benefitStart) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="待遇有效期止">{{ formatDate(elder.benefitEnd) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="待遇状态" :span="2">
|
||||
<el-tag :type="TREAT_STATUS_TAG_MAP[elder.treatStatus]?.type ?? 'info'">
|
||||
{{ TREAT_STATUS_TAG_MAP[elder.treatStatus]?.label ?? elder.treatStatus }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ elder.note ?? '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider content-position="left">状态变更</el-divider>
|
||||
<el-form inline>
|
||||
<el-form-item label="目标状态">
|
||||
<el-select v-model="statusForm.toStatus" placeholder="请选择" style="width: 200px">
|
||||
<el-option v-for="o in TREAT_STATUS_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="变更原因">
|
||||
<el-input v-model="statusForm.changeReason" placeholder="必填" style="width: 240px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="changing" @click="submitStatusChange">变更</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-divider content-position="left">状态日志</el-divider>
|
||||
<el-timeline v-if="statusLog.length > 0">
|
||||
<el-timeline-item v-for="log in statusLog" :key="log.id" :timestamp="formatDateTime(log.changedAt)">
|
||||
<span>{{ labelOf(TREAT_STATUS_OPTIONS, log.fromStatus) }} → {{ labelOf(TREAT_STATUS_OPTIONS, log.toStatus) }}</span>
|
||||
<div class="text-xs text-text-regular">{{ log.changeReason }}</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-else description="暂无状态变更记录" :image-size="60" />
|
||||
</template>
|
||||
|
||||
<div class="mt-md flex justify-end">
|
||||
<el-button @click="close">关闭</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { changeElderStatus, getElder, getStatusLog, type Elder, type ElderStatusLog } from '@/api/elder'
|
||||
import { closeDialog } from '@/utils/dialog'
|
||||
import {
|
||||
ASSESS_LEVEL_OPTIONS,
|
||||
CARE_LEVEL_OPTIONS,
|
||||
GENDER_OPTIONS,
|
||||
INSURE_TYPE_OPTIONS,
|
||||
INSURE_VERIFY_STATUS_OPTIONS,
|
||||
TREAT_STATUS_OPTIONS,
|
||||
TREAT_STATUS_TAG_MAP,
|
||||
formatDate,
|
||||
formatDateTime,
|
||||
labelOf,
|
||||
} from '../constants'
|
||||
|
||||
defineOptions({ name: 'ElderDetail' })
|
||||
|
||||
const props = defineProps<{ elderId: string; dialogId?: number }>()
|
||||
|
||||
const elder = ref<Elder>()
|
||||
const statusLog = ref<ElderStatusLog[]>([])
|
||||
const loading = ref(false)
|
||||
const changing = ref(false)
|
||||
|
||||
const statusForm = reactive({ toStatus: '', changeReason: '' })
|
||||
|
||||
async function load(): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
const [detail, log] = await Promise.all([getElder(props.elderId), getStatusLog(props.elderId)])
|
||||
elder.value = detail
|
||||
statusLog.value = log
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitStatusChange(): Promise<void> {
|
||||
if (!statusForm.toStatus) {
|
||||
ElMessage.warning('请选择目标状态')
|
||||
return
|
||||
}
|
||||
if (!statusForm.changeReason.trim()) {
|
||||
ElMessage.warning('请填写变更原因')
|
||||
return
|
||||
}
|
||||
changing.value = true
|
||||
try {
|
||||
await changeElderStatus(props.elderId, { toStatus: statusForm.toStatus, changeReason: statusForm.changeReason.trim() })
|
||||
ElMessage.success('状态已变更')
|
||||
statusForm.toStatus = ''
|
||||
statusForm.changeReason = ''
|
||||
await load()
|
||||
} finally {
|
||||
changing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
if (props.dialogId != null) closeDialog(props.dialogId)
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
256
src/pages/ltc/elder/components/elder-form.vue
Normal file
256
src/pages/ltc/elder/components/elder-form.vue
Normal file
@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="性别" prop="gender">
|
||||
<el-select v-model="form.gender" placeholder="请选择">
|
||||
<el-option v-for="o in GENDER_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="身份证号" prop="idCardNo">
|
||||
<el-input v-model="form.idCardNo" placeholder="18 位身份证号,末位可为 X" maxlength="18" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="出生日期">
|
||||
<el-input :model-value="birthDate" disabled placeholder="由身份证自动带出" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系电话" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="居住地址" prop="address">
|
||||
<el-input v-model="form.address" placeholder="请输入居住地址" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="参保地" prop="locCode">
|
||||
<el-input v-model="form.locCode" placeholder="参保地/统筹区 code(默认取机构属地)" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="医保参保号" prop="insurNo">
|
||||
<el-input v-model="form.insurNo" placeholder="请输入医保参保号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="参保类型" prop="insurType">
|
||||
<el-select v-model="form.insurType" placeholder="请选择">
|
||||
<el-option v-for="o in INSURE_TYPE_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="参保核验" prop="insurVerifyStatus">
|
||||
<el-select v-model="form.insurVerifyStatus" placeholder="请选择" clearable>
|
||||
<el-option v-for="o in INSURE_VERIFY_STATUS_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="待遇有效期起" prop="benefitStart">
|
||||
<el-date-picker v-model="form.benefitStart" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="待遇有效期止" prop="benefitEnd">
|
||||
<el-date-picker v-model="form.benefitEnd" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="note">
|
||||
<el-input v-model="form.note" type="textarea" :rows="2" placeholder="备注(含注意事项)" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">评估等级</el-divider>
|
||||
|
||||
<el-form-item label="评估结论副本">
|
||||
<div class="w-full">
|
||||
<el-upload
|
||||
v-model:file-list="fileList"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept="image/*,.pdf"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleFileRemove"
|
||||
>
|
||||
<el-button type="primary" plain>选择文件</el-button>
|
||||
</el-upload>
|
||||
<div class="mt-xs text-xs text-text-placeholder">填写失能/护理等级前,须先上传评估结论副本</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="评估机构" prop="assessOrg">
|
||||
<el-input v-model="form.assessOrg" placeholder="评估机构名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="评估日期" prop="assessDate">
|
||||
<el-date-picker v-model="form.assessDate" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="失能等级" prop="assessLevel">
|
||||
<el-select v-model="form.assessLevel" placeholder="请选择" clearable>
|
||||
<el-option v-for="o in ASSESS_LEVEL_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="护理等级" prop="careLevel">
|
||||
<el-select v-model="form.careLevel" placeholder="请选择" clearable>
|
||||
<el-option v-for="o in CARE_LEVEL_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules, UploadFile, UploadUserFile } from 'element-plus'
|
||||
import { uploadEvidence, type ElderCreateData } from '@/api/elder'
|
||||
import { getAgency } from '@/api/agency'
|
||||
import {
|
||||
ASSESS_LEVEL_OPTIONS,
|
||||
CARE_LEVEL_OPTIONS,
|
||||
GENDER_OPTIONS,
|
||||
INSURE_TYPE_OPTIONS,
|
||||
INSURE_VERIFY_STATUS_OPTIONS,
|
||||
} from '../constants'
|
||||
|
||||
defineOptions({ name: 'ElderForm' })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const form = reactive<ElderCreateData>({
|
||||
locCode: '',
|
||||
name: '',
|
||||
gender: '',
|
||||
idCardNo: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
insurNo: '',
|
||||
insurType: '',
|
||||
insurVerifyStatus: '',
|
||||
assessOrg: '',
|
||||
assessDate: '',
|
||||
assessLevel: '',
|
||||
careLevel: '',
|
||||
benefitStart: '',
|
||||
benefitEnd: '',
|
||||
note: '',
|
||||
})
|
||||
|
||||
const fileList = ref<UploadUserFile[]>([])
|
||||
const selectedFile = ref<File | null>(null)
|
||||
|
||||
/** 出生日期由身份证 7-14 位带出(与后端 deriveBirthDate 一致)。 */
|
||||
const birthDate = computed(() => deriveBirthDate(form.idCardNo))
|
||||
|
||||
function deriveBirthDate(idCardNo: string): string {
|
||||
if (!/^\d{17}[\dXx]$/.test(idCardNo)) return ''
|
||||
return `${idCardNo.slice(6, 10)}-${idCardNo.slice(10, 12)}-${idCardNo.slice(12, 14)}`
|
||||
}
|
||||
|
||||
/** 身份证 18 位 + 校验位校验(GB11643-1999)。 */
|
||||
function isValidIdCard(idCardNo: string): boolean {
|
||||
if (!/^\d{17}[\dXx]$/.test(idCardNo)) return false
|
||||
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
|
||||
const codes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
|
||||
const sum = weights.reduce((acc, w, i) => acc + w * Number(idCardNo[i]), 0)
|
||||
return codes[sum % 11] === idCardNo[17].toUpperCase()
|
||||
}
|
||||
|
||||
const rules: FormRules = {
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
|
||||
idCardNo: [
|
||||
{ required: true, message: '请输入身份证号', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (value && !isValidIdCard(String(value))) callback(new Error('身份证号格式不正确(须 18 位,末位可为 X)'))
|
||||
else callback()
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
address: [{ required: true, message: '请输入居住地址', trigger: 'blur' }],
|
||||
locCode: [{ required: true, message: '请输入参保地', trigger: 'blur' }],
|
||||
insurNo: [{ required: true, message: '请输入医保参保号', trigger: 'blur' }],
|
||||
insurType: [{ required: true, message: '请选择参保类型', trigger: 'change' }],
|
||||
}
|
||||
|
||||
function handleFileChange(uploadFile: UploadFile): void {
|
||||
selectedFile.value = uploadFile.raw ?? null
|
||||
}
|
||||
|
||||
function handleFileRemove(): void {
|
||||
selectedFile.value = null
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const agency = await getAgency()
|
||||
if (agency?.locCode) form.locCode = agency.locCode
|
||||
} catch {
|
||||
// 机构信息拉取失败不阻断建档,用户可手动填写参保地
|
||||
}
|
||||
})
|
||||
|
||||
async function submit(): Promise<ElderCreateData> {
|
||||
await formRef.value?.validate()
|
||||
// 前端联动校验:填了等级却没传副本 → 拦截;反之(传副本未填等级)允许。
|
||||
if ((form.assessLevel || form.careLevel) && !selectedFile.value) {
|
||||
ElMessage.warning('填写等级须先上传评估结论副本')
|
||||
throw new Error('填写等级须先上传评估结论副本')
|
||||
}
|
||||
if (form.benefitStart && form.benefitEnd && form.benefitEnd <= form.benefitStart) {
|
||||
ElMessage.warning('待遇有效期止须晚于待遇有效期起')
|
||||
throw new Error('待遇有效期止须晚于待遇有效期起')
|
||||
}
|
||||
let assessEvidenceFile: string | undefined
|
||||
if (selectedFile.value) {
|
||||
const { key } = await uploadEvidence(selectedFile.value)
|
||||
assessEvidenceFile = key
|
||||
}
|
||||
// 可选字段空串转为 undefined,避免后端 @IsDateString/@IsString 对空串校验失败或存空串。
|
||||
const opt = (v: string | undefined): string | undefined => (v === '' ? undefined : v)
|
||||
return {
|
||||
locCode: form.locCode,
|
||||
name: form.name,
|
||||
gender: form.gender,
|
||||
idCardNo: form.idCardNo.trim(),
|
||||
phone: opt(form.phone),
|
||||
address: form.address,
|
||||
insurNo: form.insurNo,
|
||||
insurType: form.insurType,
|
||||
insurVerifyStatus: opt(form.insurVerifyStatus),
|
||||
assessLevel: opt(form.assessLevel),
|
||||
careLevel: opt(form.careLevel),
|
||||
assessDate: opt(form.assessDate),
|
||||
assessOrg: opt(form.assessOrg),
|
||||
benefitStart: opt(form.benefitStart),
|
||||
benefitEnd: opt(form.benefitEnd),
|
||||
note: opt(form.note),
|
||||
assessEvidenceFile,
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ submit })
|
||||
</script>
|
||||
84
src/pages/ltc/elder/constants.ts
Normal file
84
src/pages/ltc/elder/constants.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import type { TagType } from '@/types/crud'
|
||||
|
||||
export interface LabeledValue {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export const GENDER_OPTIONS: LabeledValue[] = [
|
||||
{ label: '男', value: 'M' },
|
||||
{ label: '女', value: 'F' },
|
||||
]
|
||||
|
||||
export const INSURE_TYPE_OPTIONS: LabeledValue[] = [
|
||||
{ label: '职工医保', value: 'employee' },
|
||||
{ label: '居民医保', value: 'resident' },
|
||||
]
|
||||
|
||||
export const INSURE_VERIFY_STATUS_OPTIONS: LabeledValue[] = [
|
||||
{ label: '已核验', value: 'verified' },
|
||||
{ label: '未核验', value: 'unverified' },
|
||||
]
|
||||
|
||||
export const ASSESS_LEVEL_OPTIONS: LabeledValue[] = [
|
||||
{ label: '重度一级(最高级)', value: 'sev_1' },
|
||||
{ label: '重度二级', value: 'sev_2' },
|
||||
{ label: '重度三级', value: 'sev_3' },
|
||||
]
|
||||
|
||||
export const CARE_LEVEL_OPTIONS: LabeledValue[] = [
|
||||
{ label: '一级护理(最高级)', value: 'care_1' },
|
||||
{ label: '二级护理', value: 'care_2' },
|
||||
{ label: '三级护理', value: 'care_3' },
|
||||
]
|
||||
|
||||
/** 待遇状态十态(值唯一来源:后端 elder TREAT_STATUSES)。 */
|
||||
export const TREAT_STATUS_OPTIONS: LabeledValue[] = [
|
||||
{ label: '待建档', value: 'pending_file' },
|
||||
{ label: '待起付', value: 'waiting_period' },
|
||||
{ label: '正常享受', value: 'active' },
|
||||
{ label: '住院暂停', value: 'hosp_pause' },
|
||||
{ label: '复评等待期', value: 'review_waiting' },
|
||||
{ label: '跨统筹区转移中', value: 'transferring' },
|
||||
{ label: '死亡', value: 'dead' },
|
||||
{ label: '长期异地居住', value: 'long_away' },
|
||||
{ label: '待遇到期', value: 'benefit_expired' },
|
||||
{ label: '已终止', value: 'terminated' },
|
||||
]
|
||||
|
||||
export const TREAT_STATUS_TAG_MAP: Record<string, { label: string; type: TagType }> = {
|
||||
pending_file: { label: '待建档', type: 'info' },
|
||||
waiting_period: { label: '待起付', type: 'warning' },
|
||||
active: { label: '正常享受', type: 'success' },
|
||||
hosp_pause: { label: '住院暂停', type: 'warning' },
|
||||
review_waiting: { label: '复评等待期', type: 'warning' },
|
||||
transferring: { label: '跨统筹区转移中', type: 'info' },
|
||||
dead: { label: '死亡', type: 'danger' },
|
||||
long_away: { label: '长期异地居住', type: 'info' },
|
||||
benefit_expired: { label: '待遇到期', type: 'danger' },
|
||||
terminated: { label: '已终止', type: 'danger' },
|
||||
}
|
||||
|
||||
/** 按值取显示名,空值回退 '-'。 */
|
||||
export function labelOf(options: LabeledValue[], value: unknown): string {
|
||||
if (value == null || value === '') return '-'
|
||||
return options.find((o) => o.value === value)?.label ?? String(value)
|
||||
}
|
||||
|
||||
/** 毫秒时间戳/日期字符串 → YYYY-MM-DD(供展示)。 */
|
||||
export function formatDate(value: unknown): string {
|
||||
if (value == null || value === '') return '-'
|
||||
const d = typeof value === 'number' ? new Date(value) : new Date(String(value))
|
||||
if (Number.isNaN(d.getTime())) return '-'
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
||||
}
|
||||
|
||||
/** 毫秒时间戳/日期字符串 → YYYY-MM-DD HH:mm(供时间线展示)。 */
|
||||
export function formatDateTime(value: unknown): string {
|
||||
if (value == null || value === '') return '-'
|
||||
const d = typeof value === 'number' ? new Date(value) : new Date(String(value))
|
||||
if (Number.isNaN(d.getTime())) return '-'
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
@ -1,7 +1,100 @@
|
||||
<template>
|
||||
<div>服务对象(C1 实现)</div>
|
||||
<ty-crud
|
||||
ref="crudRef"
|
||||
entity-name="服务对象"
|
||||
:columns="columns"
|
||||
:search-schema="searchSchema"
|
||||
:form-schema="formSchema"
|
||||
:list="list"
|
||||
:create="create"
|
||||
:update="update"
|
||||
:remove="remove"
|
||||
:actions="actions"
|
||||
:show-create="false"
|
||||
id-key="id"
|
||||
name-key="name"
|
||||
>
|
||||
<template #toolbar>
|
||||
<el-button v-permission="'elder:create'" type="primary" @click="openCreate">建档</el-button>
|
||||
</template>
|
||||
</ty-crud>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { CrudAction, JsonSchema, PageQuery, TableColumn } from '@/types/crud'
|
||||
import { showDialog } from '@/utils/dialog'
|
||||
import { createElder, listElders, type ElderCreateData, type ElderListQuery } from '@/api/elder'
|
||||
import { CARE_LEVEL_OPTIONS, GENDER_OPTIONS, TREAT_STATUS_OPTIONS, TREAT_STATUS_TAG_MAP, formatDate, labelOf } from './constants'
|
||||
import ElderForm from './components/elder-form.vue'
|
||||
import ElderDetail from './components/elder-detail.vue'
|
||||
|
||||
defineOptions({ name: 'elder' })
|
||||
|
||||
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: 150, formatter: (v) => labelOf(CARE_LEVEL_OPTIONS, v) },
|
||||
{ prop: 'treatStatus', label: '待遇状态', width: 120, align: 'center', type: 'tag', tagMap: TREAT_STATUS_TAG_MAP },
|
||||
{ prop: 'birthDate', label: '出生日期', width: 120, formatter: (v) => formatDate(v) },
|
||||
]
|
||||
|
||||
const searchSchema: JsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', title: '姓名', placeholder: '请输入姓名' },
|
||||
treatStatus: {
|
||||
type: 'string',
|
||||
title: '待遇状态',
|
||||
placeholder: '全部',
|
||||
enum: TREAT_STATUS_OPTIONS.map((o) => o.value),
|
||||
enumNames: TREAT_STATUS_OPTIONS.map((o) => o.label),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ty-crud 要求 formSchema,但建档走自定义表单(含日期/上传),此处仅为满足组件契约,不会被使用。
|
||||
const formSchema: JsonSchema = { type: 'object', properties: {} }
|
||||
|
||||
function toListQuery(query: PageQuery): ElderListQuery {
|
||||
const str = (v: unknown): string | undefined => (typeof v === 'string' && v !== '' ? v : undefined)
|
||||
return { page: query.page, pageSize: query.pageSize, name: str(query.name), treatStatus: str(query.treatStatus) }
|
||||
}
|
||||
|
||||
const list = async (query: PageQuery, signal?: AbortSignal) => {
|
||||
const result = await listElders(toListQuery(query), signal)
|
||||
return { ...result, list: result.list.map((e) => ({ ...e })) }
|
||||
}
|
||||
|
||||
// 建档/编辑/删除均不走 ty-crud 内置流程(建档走工具栏「建档」自定义表单),此处仅为满足组件契约,不会被调用。
|
||||
const create = () => Promise.reject(new Error('建档请使用工具栏「建档」按钮'))
|
||||
const update = () => Promise.reject(new Error('服务对象不支持编辑'))
|
||||
const remove = () => Promise.reject(new Error('服务对象不支持删除'))
|
||||
|
||||
const actions: CrudAction[] = [{ label: '查看', type: 'primary', permission: 'elder:detail', onClick: (row) => openDetail(row) }]
|
||||
|
||||
const crudRef = ref()
|
||||
|
||||
async function openCreate(): Promise<void> {
|
||||
const data = await showDialog({
|
||||
component: ElderForm,
|
||||
title: '服务对象建档',
|
||||
width: '780px',
|
||||
})
|
||||
if (data == null) return
|
||||
await createElder(data as ElderCreateData)
|
||||
ElMessage.success('建档成功')
|
||||
crudRef.value?.reload()
|
||||
}
|
||||
|
||||
async function openDetail(row: Record<string, any>): Promise<void> {
|
||||
await showDialog({
|
||||
component: ElderDetail,
|
||||
props: { elderId: String(row.id) },
|
||||
title: `服务对象详情(${row.name})`,
|
||||
width: '840px',
|
||||
showFooter: false,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user