Compare commits

..

No commits in common. "ebbd46899cc04453779992cf01bbca9060fc8f6e" and "56056ec3d098a29ba08cbd9afabcf08f86938332" have entirely different histories.

6 changed files with 1 additions and 746 deletions

22
components.d.ts vendored
View File

@ -20,16 +20,9 @@ declare module 'vue' {
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElderDetail: typeof import('./src/components/elder-detail.vue')['default']
ElderForm: typeof import('./src/components/elder-form.vue')['default']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
@ -48,7 +41,6 @@ declare module 'vue' {
ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElResult: typeof import('element-plus/es')['ElResult'] ElResult: typeof import('element-plus/es')['ElResult']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElSlider: typeof import('element-plus/es')['ElSlider'] ElSlider: typeof import('element-plus/es')['ElSlider']
@ -57,11 +49,8 @@ declare module 'vue' {
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag'] 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'] ElTree: typeof import('element-plus/es')['ElTree']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload']
Icon: typeof import('./src/components/icon/icon.vue')['default'] Icon: typeof import('./src/components/icon/icon.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
@ -97,16 +86,9 @@ declare global {
const ElCard: typeof import('element-plus/es')['ElCard'] const ElCard: typeof import('element-plus/es')['ElCard']
const ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] const ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
const ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] const ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
const ElCol: typeof import('element-plus/es')['ElCol']
const ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] const ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
const ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] const ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
const ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
const ElderDetail: typeof import('./src/components/elder-detail.vue')['default']
const ElderForm: typeof import('./src/components/elder-form.vue')['default']
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 ElDialog: typeof import('element-plus/es')['ElDialog']
const ElDivider: typeof import('element-plus/es')['ElDivider']
const ElDrawer: typeof import('element-plus/es')['ElDrawer'] const ElDrawer: typeof import('element-plus/es')['ElDrawer']
const ElDropdown: typeof import('element-plus/es')['ElDropdown'] const ElDropdown: typeof import('element-plus/es')['ElDropdown']
const ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] const ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
@ -125,7 +107,6 @@ declare global {
const ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] const ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
const ElResult: typeof import('element-plus/es')['ElResult'] const ElResult: typeof import('element-plus/es')['ElResult']
const ElRow: typeof import('element-plus/es')['ElRow']
const ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] const ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
const ElSelect: typeof import('element-plus/es')['ElSelect'] const ElSelect: typeof import('element-plus/es')['ElSelect']
const ElSlider: typeof import('element-plus/es')['ElSlider'] const ElSlider: typeof import('element-plus/es')['ElSlider']
@ -134,11 +115,8 @@ declare global {
const ElTable: typeof import('element-plus/es')['ElTable'] const ElTable: typeof import('element-plus/es')['ElTable']
const ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] const ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
const ElTag: typeof import('element-plus/es')['ElTag'] 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 ElTree: typeof import('element-plus/es')['ElTree']
const ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] 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 Icon: typeof import('./src/components/icon/icon.vue')['default']
const RouterLink: typeof import('vue-router')['RouterLink'] const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView'] const RouterView: typeof import('vue-router')['RouterView']

View File

@ -1,125 +0,0 @@
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)
}

View File

@ -1,131 +0,0 @@
<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>
<div v-if="!isTerminal" v-permission="'elder:status-change'">
<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 statusOptions" :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>
</div>
<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>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { changeElderStatus, getElder, getStatusLog, type Elder, type ElderStatusLog } from '@/api/elder'
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 '@/pages/ltc/elder/constants'
defineOptions({ name: 'ElderDetail' })
const props = defineProps<{ elderId: string }>()
const elder = ref<Elder>()
const statusLog = ref<ElderStatusLog[]>([])
const loading = ref(false)
const changing = ref(false)
const statusForm = reactive({ toStatus: '', changeReason: '' })
/** 终态(dead/terminated):后端 IRREVERSIBLE 源态守卫,禁止从终态再变更 → 隐藏整个状态变更区块。 */
const isTerminal = computed(() => {
const s = elder.value?.treatStatus
return s === 'dead' || s === 'terminated'
})
/** 目标状态下拉:允许转入全部十态(含 dead/terminated 合法目标态),仅排除当前态。 */
const statusOptions = computed(() => {
const current = elder.value?.treatStatus
return TREAT_STATUS_OPTIONS.filter((o) => o.value !== current)
})
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
}
}
onMounted(load)
</script>

View File

@ -1,256 +0,0 @@
<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 '@/pages/ltc/elder/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>

View File

@ -1,84 +0,0 @@
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())}`
}

View File

@ -1,134 +1,7 @@
<template> <template>
<div class="elder-page"> <div>服务对象(C1 实现)</div>
<el-card class="mb-md" shadow="never">
<ty-search-form :schema="searchSchema" :model-value="query" @search="handleSearch" @reset="handleReset" />
</el-card>
<el-card class="elder-page__table" shadow="never">
<div class="mb-md flex items-center">
<el-button v-permission="'elder:create'" type="primary" @click="openCreate">建档</el-button>
</div>
<ty-table
:columns="columns"
:data="list"
:loading="loading"
:total="total"
v-model:page="page"
v-model:page-size="pageSize"
:actions-width="120"
>
<template #actions="{ row }">
<el-button link type="primary" @click="openDetail(row)">查看</el-button>
</template>
</ty-table>
</el-card>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import type { JsonSchema, TableColumn } from '@/types/crud'
import { showDialog } from '@/utils/dialog'
import { useRequest } from '@/composables/useRequest'
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' }) 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) },
// 下次服务为占位列,本期无数据,统一显示「—」
{ prop: 'nextService', label: '下次服务', width: 120, formatter: () => '—' },
]
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),
},
},
}
const query = reactive<Record<string, any>>({ name: '', treatStatus: '' })
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), treatStatus: str(query.treatStatus) }
}
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
}
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('建档成功')
fetchList()
}
async function openDetail(row: Record<string, any>): Promise<void> {
await showDialog({
component: ElderDetail,
props: { elderId: String(row.id) },
title: `服务对象详情(${row.name})`,
width: '840px',
confirmText: '关闭',
})
}
onMounted(() => fetchList())
</script> </script>
<style scoped lang="scss">
.elder-page {
@apply flex h-full flex-col;
}
.elder-page__table {
@apply flex min-h-0 flex-1 flex-col;
:deep(.el-card__body) {
@apply flex min-h-0 flex-1 flex-col;
}
}
</style>