Compare commits
No commits in common. "7802cfdf808fc5fc1b641ef83a9a33e5d71ec81e" and "4e481dad5f65f26422bb97b4d776578adf440e41" have entirely different histories.
7802cfdf80
...
4e481dad5f
@ -15,12 +15,6 @@ export interface Agency {
|
|||||||
contactPhone: string
|
contactPhone: string
|
||||||
bedCount: number | null
|
bedCount: number | null
|
||||||
signDate: number | null
|
signDate: number | null
|
||||||
/** 质控时限(小时)。 */
|
|
||||||
qcThreshold: number | null
|
|
||||||
/** 对接档位:A/B/C。 */
|
|
||||||
dockingTier: string | null
|
|
||||||
/** 国家新增项通知开关。 */
|
|
||||||
newNationalNotify: boolean
|
|
||||||
createdAt: number
|
createdAt: number
|
||||||
createdBy: string | null
|
createdBy: string | null
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
@ -41,9 +35,6 @@ export type AgencyUpdateData = Partial<
|
|||||||
| 'contactPhone'
|
| 'contactPhone'
|
||||||
| 'bedCount'
|
| 'bedCount'
|
||||||
| 'signDate'
|
| 'signDate'
|
||||||
| 'qcThreshold'
|
|
||||||
| 'dockingTier'
|
|
||||||
| 'newNationalNotify'
|
|
||||||
>
|
>
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export interface SvcNational {
|
|||||||
version: number
|
version: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** L4 机构项目库记录(机构勾选 + 配价 + 拍照分档)。 */
|
/** L4 机构项目库记录(机构勾选 + 配价)。 */
|
||||||
export interface SvcOrg {
|
export interface SvcOrg {
|
||||||
id: string
|
id: string
|
||||||
tenantCode: string
|
tenantCode: string
|
||||||
@ -36,8 +36,6 @@ export interface SvcOrg {
|
|||||||
isEnabled: boolean
|
isEnabled: boolean
|
||||||
price: string | null
|
price: string | null
|
||||||
priceEffective: number | null
|
priceEffective: number | null
|
||||||
/** 拍照分档:must/free/optional。 */
|
|
||||||
photoTier: string
|
|
||||||
isNewNational: boolean
|
isNewNational: boolean
|
||||||
createdAt: number
|
createdAt: number
|
||||||
createdBy: string | null
|
createdBy: string | null
|
||||||
@ -71,22 +69,3 @@ export function configSvcOrg(data: ConfigSvcOrgData): Promise<SvcOrg> {
|
|||||||
export function listNewNational(signal?: AbortSignal): Promise<SvcNational[]> {
|
export function listNewNational(signal?: AbortSignal): Promise<SvcNational[]> {
|
||||||
return http.get<SvcNational[]>('/svc/org/new-national', { signal })
|
return http.get<SvcNational[]>('/svc/org/new-national', { signal })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 拍照分档批量更新项。 */
|
|
||||||
export interface UpdatePhotoTierItem {
|
|
||||||
id: string
|
|
||||||
/** must/free/optional。 */
|
|
||||||
photoTier: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 拍照分档批量更新结果:must→free/optional 视为「放宽」,收集进 warnings。 */
|
|
||||||
export interface UpdatePhotoTierResult {
|
|
||||||
updated: number
|
|
||||||
warnings: { id: string; from: string; to: string }[]
|
|
||||||
message?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 批量更新拍照分档。 */
|
|
||||||
export function updatePhotoTier(items: UpdatePhotoTierItem[]): Promise<UpdatePhotoTierResult> {
|
|
||||||
return http.post<UpdatePhotoTierResult>('/svc/org/photo-tier/batch', { items })
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,103 +0,0 @@
|
|||||||
import { http } from '@/api/http'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文书档案(一人一档)导出 API。
|
|
||||||
* 后端返回结构化 JSON(身份证已掩码),前端负责组装下载文件。
|
|
||||||
* 字段锚点见 shared/data-model spec(document 导出契约)。
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** 导出水印头。 */
|
|
||||||
export interface DocumentWatermark {
|
|
||||||
orgName: string
|
|
||||||
exportTime: string
|
|
||||||
operator: string
|
|
||||||
notice: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 老人基本信息(@db.Date 字段已转 YYYY-MM-DD,idCardNo 已掩码)。 */
|
|
||||||
export interface DocumentElderInfo {
|
|
||||||
name: string
|
|
||||||
gender: string
|
|
||||||
birthDate: string | null
|
|
||||||
idCardNo: string
|
|
||||||
phone: string | null
|
|
||||||
address: string | null
|
|
||||||
insurNo: string | null
|
|
||||||
careLevel: string | null
|
|
||||||
treatStatus: string
|
|
||||||
benefitStart: string | null
|
|
||||||
benefitEnd: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 计划项(项目名快照)。 */
|
|
||||||
export interface DocumentPlanItem {
|
|
||||||
svcId: string
|
|
||||||
svcName: string
|
|
||||||
freqType: string | null
|
|
||||||
freqValue: number | null
|
|
||||||
durationMin: number | null
|
|
||||||
photoTier: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 服务计划。 */
|
|
||||||
export interface DocumentPlan {
|
|
||||||
id: string
|
|
||||||
planPeriodStart: string | null
|
|
||||||
planPeriodEnd: string | null
|
|
||||||
careLevel: string
|
|
||||||
careMode: string
|
|
||||||
planStatus: string
|
|
||||||
items: DocumentPlanItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 留痕项目明细。 */
|
|
||||||
export interface DocumentRecordItem {
|
|
||||||
svcId: string
|
|
||||||
svcName: string
|
|
||||||
execResult: string
|
|
||||||
undoReason: string | null
|
|
||||||
extraFlag: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 履约留痕。 */
|
|
||||||
export interface DocumentRecord {
|
|
||||||
id: string
|
|
||||||
checkinTime: string | null
|
|
||||||
checkoutTime: string | null
|
|
||||||
durationMin: number | null
|
|
||||||
recordStatus: string
|
|
||||||
items: DocumentRecordItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 异常上报。 */
|
|
||||||
export interface DocumentAbnormal {
|
|
||||||
id: string
|
|
||||||
type: string
|
|
||||||
desc: string | null
|
|
||||||
handleStatus: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 一人一档结构化导出。 */
|
|
||||||
export interface DocumentExportData {
|
|
||||||
watermark: DocumentWatermark
|
|
||||||
elder: DocumentElderInfo
|
|
||||||
plans: DocumentPlan[]
|
|
||||||
records: DocumentRecord[]
|
|
||||||
abnormals: DocumentAbnormal[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExportDocumentQuery {
|
|
||||||
elderId: string
|
|
||||||
/** 留痕起始日期(YYYY-MM-DD)。 */
|
|
||||||
startDate?: string
|
|
||||||
/** 留痕结束日期(YYYY-MM-DD)。 */
|
|
||||||
endDate?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 按老人(+可选时间范围)导出一人一档。 */
|
|
||||||
export function exportDocument(query: ExportDocumentQuery, signal?: AbortSignal): Promise<DocumentExportData> {
|
|
||||||
const params: Record<string, unknown> = { elderId: query.elderId }
|
|
||||||
if (query.startDate) params.startDate = query.startDate
|
|
||||||
if (query.endDate) params.endDate = query.endDate
|
|
||||||
return http.get<DocumentExportData>('/document/export', { params, signal })
|
|
||||||
}
|
|
||||||
@ -1,109 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="catalog-page">
|
<div>服务项目与配价(C1 实现)</div>
|
||||||
<el-card shadow="never">
|
|
||||||
<div class="mb-md text-xs text-text-placeholder">
|
|
||||||
机构只能勾选「是否开展」与「价格」,目录项目名称与来源由属地/国家库决定,不可修改。
|
|
||||||
</div>
|
|
||||||
<el-table v-loading="loading" :data="rows" border>
|
|
||||||
<el-table-column prop="name" label="项目" min-width="180">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.name }}
|
|
||||||
<el-tag v-if="row.isNewNational" type="warning" size="small" class="ml-xs">国家新增</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="来源" width="90" align="center">
|
|
||||||
<template #default="{ row }">{{ row.srcType === 'local' ? '属地' : '国家' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="是否开展" width="110" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-switch
|
|
||||||
v-model="row.isEnabled"
|
|
||||||
:loading="togglingId === row.id"
|
|
||||||
@change="toggleEnabled(row)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="价格" width="160">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-input v-model="row.price" placeholder="预留,本期不参与结算" clearable />
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button
|
|
||||||
v-permission="'svc-org:config'"
|
|
||||||
link
|
|
||||||
type="primary"
|
|
||||||
:loading="savingId === row.id"
|
|
||||||
@click="savePrice(row)"
|
|
||||||
>
|
|
||||||
保存
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { ApiError } from '@/api/http'
|
|
||||||
import { configSvcOrg } from '@/api/catalog'
|
|
||||||
import { loadL4Projects, type L4ProjectRow } from '../l4-projects'
|
|
||||||
|
|
||||||
defineOptions({ name: 'svc-config-catalog' })
|
defineOptions({ name: 'svc-config-catalog' })
|
||||||
|
|
||||||
const rows = ref<L4ProjectRow[]>([])
|
|
||||||
const loading = ref(false)
|
|
||||||
const togglingId = ref<string | null>(null)
|
|
||||||
const savingId = ref<string | null>(null)
|
|
||||||
|
|
||||||
async function load(): Promise<void> {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
rows.value = await loadL4Projects()
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof ApiError) ElMessage.error(e.message)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 开关即存:只改 isEnabled。失败回滚开关。 */
|
|
||||||
async function toggleEnabled(row: Record<string, any>): Promise<void> {
|
|
||||||
togglingId.value = row.id
|
|
||||||
const before = row.isEnabled
|
|
||||||
try {
|
|
||||||
await configSvcOrg({ id: row.id, isEnabled: row.isEnabled })
|
|
||||||
ElMessage.success('已保存')
|
|
||||||
} catch (e) {
|
|
||||||
row.isEnabled = !before
|
|
||||||
if (e instanceof ApiError) ElMessage.error(e.message)
|
|
||||||
} finally {
|
|
||||||
togglingId.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 配价:只改 price。 */
|
|
||||||
async function savePrice(row: Record<string, any>): Promise<void> {
|
|
||||||
savingId.value = row.id
|
|
||||||
try {
|
|
||||||
await configSvcOrg({ id: row.id, price: row.price ?? undefined })
|
|
||||||
ElMessage.success('已保存')
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof ApiError) ElMessage.error(e.message)
|
|
||||||
} finally {
|
|
||||||
savingId.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.catalog-page {
|
|
||||||
@apply flex h-full flex-col;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,73 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="docking-page">
|
<div>对接档位(C1 实现)</div>
|
||||||
<el-card shadow="never">
|
|
||||||
<div v-loading="loading">
|
|
||||||
<div class="mb-md">
|
|
||||||
<div class="text-sm text-text-regular">当前对接档位</div>
|
|
||||||
<div class="mt-sm flex items-center gap-sm">
|
|
||||||
<el-tag v-if="dockingTier" :type="dockingTier === 'C' ? 'info' : 'success'" size="large">
|
|
||||||
{{ dockingTier }} 档
|
|
||||||
</el-tag>
|
|
||||||
<span v-else class="text-text-placeholder">未设置</span>
|
|
||||||
<span v-if="dockingTier === 'C'" class="text-xs text-text-placeholder">待接口发布</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider content-position="left">切换档位</el-divider>
|
|
||||||
<el-radio-group v-model="dockingTier" :disabled="saving" @change="save">
|
|
||||||
<el-radio-button value="A">A 档</el-radio-button>
|
|
||||||
<el-radio-button value="B">B 档</el-radio-button>
|
|
||||||
<el-radio-button value="C" disabled>C 档(待接口发布)</el-radio-button>
|
|
||||||
</el-radio-group>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { ApiError } from '@/api/http'
|
|
||||||
import { getAgency, updateAgency } from '@/api/agency'
|
|
||||||
|
|
||||||
defineOptions({ name: 'svc-config-docking' })
|
defineOptions({ name: 'svc-config-docking' })
|
||||||
|
|
||||||
const dockingTier = ref<string>('')
|
|
||||||
const loading = ref(false)
|
|
||||||
const saving = ref(false)
|
|
||||||
|
|
||||||
async function load(): Promise<void> {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const agency = await getAgency()
|
|
||||||
dockingTier.value = agency.dockingTier ?? ''
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof ApiError) ElMessage.error(e.message)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function save(value: string | number | boolean | undefined): Promise<void> {
|
|
||||||
if (value !== 'A' && value !== 'B') return
|
|
||||||
saving.value = true
|
|
||||||
try {
|
|
||||||
await updateAgency({ dockingTier: value })
|
|
||||||
ElMessage.success('对接档位已切换')
|
|
||||||
await load()
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof ApiError) ElMessage.error(e.message)
|
|
||||||
await load()
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.docking-page {
|
|
||||||
@apply flex h-full flex-col;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,130 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="info-page">
|
<div>机构信息(C1 实现)</div>
|
||||||
<el-card shadow="never">
|
|
||||||
<div v-loading="loading">
|
|
||||||
<el-form label-width="120px" class="info-page__form">
|
|
||||||
<el-form-item label="机构名称">
|
|
||||||
<el-input v-model="form.orgName" placeholder="请输入机构名称" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="地址">
|
|
||||||
<el-input v-model="form.address" placeholder="请输入地址" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="法人/联系人">
|
|
||||||
<el-input v-model="form.contactName" placeholder="请输入法人/联系人" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="联系方式">
|
|
||||||
<el-input v-model="form.contactPhone" placeholder="请输入联系方式" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="参保地">
|
|
||||||
<el-input :model-value="locLabel" disabled />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="质控阈值(小时)">
|
|
||||||
<el-input-number v-model="form.qcThreshold" :min="0" :step="1" step-strictly />
|
|
||||||
<span class="ml-xs text-xs text-text-placeholder">留痕提交后超过该时限未质控即触发提醒</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="对接档位">
|
|
||||||
<el-select v-model="form.dockingTier" placeholder="请选择" style="width: 200px">
|
|
||||||
<el-option label="A 档" value="A" />
|
|
||||||
<el-option label="B 档" value="B" />
|
|
||||||
<el-option label="C 档(待接口发布)" value="C" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="国家新增项提示">
|
|
||||||
<el-switch v-model="form.newNationalNotify" />
|
|
||||||
<span class="ml-xs text-xs text-text-placeholder">出现国家新增服务项目时提示</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button v-permission="'agency:update'" type="primary" :loading="saving" @click="save">保存</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { ApiError } from '@/api/http'
|
|
||||||
import { getAgency, updateAgency } from '@/api/agency'
|
|
||||||
|
|
||||||
defineOptions({ name: 'svc-config-info' })
|
defineOptions({ name: 'svc-config-info' })
|
||||||
|
|
||||||
interface FormModel {
|
|
||||||
orgName: string
|
|
||||||
address: string
|
|
||||||
contactName: string
|
|
||||||
contactPhone: string
|
|
||||||
locCode: string
|
|
||||||
locName: string
|
|
||||||
qcThreshold: number | null
|
|
||||||
dockingTier: string | null
|
|
||||||
newNationalNotify: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const form = reactive<FormModel>({
|
|
||||||
orgName: '',
|
|
||||||
address: '',
|
|
||||||
contactName: '',
|
|
||||||
contactPhone: '',
|
|
||||||
locCode: '',
|
|
||||||
locName: '',
|
|
||||||
qcThreshold: null,
|
|
||||||
dockingTier: null,
|
|
||||||
newNationalNotify: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const saving = ref(false)
|
|
||||||
|
|
||||||
const locLabel = computed(() => form.locName || form.locCode || '—')
|
|
||||||
|
|
||||||
async function load(): Promise<void> {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const agency = await getAgency()
|
|
||||||
form.orgName = agency.orgName ?? ''
|
|
||||||
form.address = agency.address ?? ''
|
|
||||||
form.contactName = agency.contactName ?? ''
|
|
||||||
form.contactPhone = agency.contactPhone ?? ''
|
|
||||||
form.locCode = agency.locCode ?? ''
|
|
||||||
form.locName = agency.locName ?? ''
|
|
||||||
form.qcThreshold = agency.qcThreshold ?? null
|
|
||||||
form.dockingTier = agency.dockingTier ?? null
|
|
||||||
form.newNationalNotify = agency.newNationalNotify ?? false
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof ApiError) ElMessage.error(e.message)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function save(): Promise<void> {
|
|
||||||
saving.value = true
|
|
||||||
try {
|
|
||||||
await updateAgency({
|
|
||||||
orgName: form.orgName,
|
|
||||||
address: form.address,
|
|
||||||
contactName: form.contactName,
|
|
||||||
contactPhone: form.contactPhone,
|
|
||||||
qcThreshold: form.qcThreshold,
|
|
||||||
dockingTier: form.dockingTier,
|
|
||||||
newNationalNotify: form.newNationalNotify,
|
|
||||||
})
|
|
||||||
ElMessage.success('机构信息已保存')
|
|
||||||
await load()
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof ApiError) ElMessage.error(e.message)
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.info-page__form {
|
|
||||||
@apply max-w-[640px];
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
import { getAgency } from '@/api/agency'
|
|
||||||
import { listLocal, listNewNational, listOrg, type SvcOrg } from '@/api/catalog'
|
|
||||||
|
|
||||||
/** L4 机构项目 + 解析后的展示名(join L2 属地目录 / L1 国家目录)。 */
|
|
||||||
export interface L4ProjectRow extends SvcOrg {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载当前机构 L4 项目库,并按 srcType/srcId join 目录解析展示名。
|
|
||||||
* L4(svc_org)只有 srcType/srcId,无名称,需按来源 join L2/L1 目录。
|
|
||||||
*/
|
|
||||||
export async function loadL4Projects(): Promise<L4ProjectRow[]> {
|
|
||||||
const [agency, orgs, nationals] = await Promise.all([getAgency(), listOrg(), listNewNational()])
|
|
||||||
const locals = await listLocal(agency.locCode)
|
|
||||||
|
|
||||||
const localMap = new Map<string, string>()
|
|
||||||
const nationalMap = new Map<string, string>()
|
|
||||||
for (const it of locals) {
|
|
||||||
if ('localItemName' in it) localMap.set(it.id, it.localItemName)
|
|
||||||
else if ('natName' in it) nationalMap.set(it.id, it.natName)
|
|
||||||
}
|
|
||||||
for (const it of nationals) nationalMap.set(it.id, it.natName)
|
|
||||||
|
|
||||||
return orgs.map((o) => {
|
|
||||||
const name = o.srcType === 'local' ? localMap.get(o.srcId) : nationalMap.get(o.srcId)
|
|
||||||
return { ...o, name: name ?? o.srcId }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,171 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="loc-page">
|
<div>属地与目录(C1 实现)</div>
|
||||||
<el-card class="mb-md" shadow="never">
|
|
||||||
<div v-loading="agencyLoading" class="flex items-center">
|
|
||||||
<span class="mr-sm">国家新增项提示</span>
|
|
||||||
<el-switch
|
|
||||||
v-model="newNationalNotify"
|
|
||||||
:loading="notifySaving"
|
|
||||||
@change="saveNotify"
|
|
||||||
/>
|
|
||||||
<span class="ml-xs text-xs text-text-placeholder">出现国家新增服务项目时提示</span>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card shadow="never">
|
|
||||||
<el-tabs v-model="activeTab">
|
|
||||||
<el-tab-pane label="国家基准库(L1)" name="national">
|
|
||||||
<el-table v-loading="loading" :data="nationals" border size="small">
|
|
||||||
<el-table-column prop="natName" label="项目" min-width="160" />
|
|
||||||
<el-table-column prop="natCategory" label="分类" width="120" />
|
|
||||||
<el-table-column prop="natSubCategory" label="子分类" min-width="140">
|
|
||||||
<template #default="{ row }">{{ row.natSubCategory ?? '—' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="建议人员" min-width="120">
|
|
||||||
<template #default="{ row }">{{ row.recommendStaff ?? '—' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="建议时长" width="100">
|
|
||||||
<template #default="{ row }">{{ row.recommendDuration ?? '—' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="医疗项目" width="90" align="center">
|
|
||||||
<template #default="{ row }">{{ row.isMedical ? '是' : '—' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="属地目录(L2)" name="local">
|
|
||||||
<el-table v-loading="loading" :data="locals" border size="small">
|
|
||||||
<el-table-column prop="localItemName" label="项目" min-width="160" />
|
|
||||||
<el-table-column prop="localCategory" label="分类" min-width="120">
|
|
||||||
<template #default="{ row }">{{ row.localCategory ?? '—' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="服务时长" width="100">
|
|
||||||
<template #default="{ row }">{{ row.svcTime ?? '—' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="最低频次" width="100">
|
|
||||||
<template #default="{ row }">{{ row.minFreq ?? '—' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="人员资质" min-width="120">
|
|
||||||
<template #default="{ row }">{{ row.staffQual ?? '—' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="医疗项目" width="90" align="center">
|
|
||||||
<template #default="{ row }">{{ row.isMedical ? '是' : '—' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="映射状态" width="100">
|
|
||||||
<template #default="{ row }">{{ row.mapStatus ?? '—' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<el-empty v-if="!loading && locals.length === 0" description="本参保地暂无属地目录" :image-size="60" />
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="机构项目(L4)" name="org">
|
|
||||||
<el-table v-loading="loading" :data="orgs" border size="small">
|
|
||||||
<el-table-column prop="name" label="项目" min-width="160" />
|
|
||||||
<el-table-column label="来源" width="90" align="center">
|
|
||||||
<template #default="{ row }">{{ row.srcType === 'local' ? '属地' : '国家' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="是否开展" width="90" align="center">
|
|
||||||
<template #default="{ row }">{{ row.isEnabled ? '是' : '否' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="价格" width="100" align="right">
|
|
||||||
<template #default="{ row }">{{ row.price ?? '—' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="拍照分档" width="100" align="center">
|
|
||||||
<template #default="{ row }">{{ photoTierLabel(row.photoTier) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="国家新增" width="90" align="center">
|
|
||||||
<template #default="{ row }">{{ row.isNewNational ? '是' : '—' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { ApiError } from '@/api/http'
|
|
||||||
import { getAgency, updateAgency } from '@/api/agency'
|
|
||||||
import { listLocal, listNewNational, type SvcLocal, type SvcNational } from '@/api/catalog'
|
|
||||||
import { loadL4Projects, type L4ProjectRow } from '../l4-projects'
|
|
||||||
|
|
||||||
defineOptions({ name: 'svc-config-loc' })
|
defineOptions({ name: 'svc-config-loc' })
|
||||||
|
|
||||||
const activeTab = ref('national')
|
|
||||||
|
|
||||||
const nationals = ref<SvcNational[]>([])
|
|
||||||
const locals = ref<SvcLocal[]>([])
|
|
||||||
const orgs = ref<L4ProjectRow[]>([])
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const agencyLoading = ref(false)
|
|
||||||
const notifySaving = ref(false)
|
|
||||||
const newNationalNotify = ref(false)
|
|
||||||
|
|
||||||
const PHOTO_TIER_LABELS: Record<string, string> = {
|
|
||||||
must: '必拍',
|
|
||||||
free: '不拍',
|
|
||||||
optional: '选拍',
|
|
||||||
}
|
|
||||||
|
|
||||||
function photoTierLabel(v: string): string {
|
|
||||||
return PHOTO_TIER_LABELS[v] ?? v
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAgency(): Promise<void> {
|
|
||||||
agencyLoading.value = true
|
|
||||||
try {
|
|
||||||
const agency = await getAgency()
|
|
||||||
newNationalNotify.value = agency.newNationalNotify ?? false
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof ApiError) ElMessage.error(e.message)
|
|
||||||
} finally {
|
|
||||||
agencyLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load(): Promise<void> {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const [agency, nationalRes, orgRes] = await Promise.all([
|
|
||||||
getAgency(),
|
|
||||||
listNewNational(),
|
|
||||||
loadL4Projects(),
|
|
||||||
])
|
|
||||||
nationals.value = nationalRes
|
|
||||||
orgs.value = orgRes
|
|
||||||
|
|
||||||
const localRes = await listLocal(agency.locCode)
|
|
||||||
locals.value = localRes.filter((it): it is SvcLocal => 'localItemName' in it)
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof ApiError) ElMessage.error(e.message)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveNotify(): Promise<void> {
|
|
||||||
notifySaving.value = true
|
|
||||||
try {
|
|
||||||
await updateAgency({ newNationalNotify: newNationalNotify.value })
|
|
||||||
ElMessage.success('已保存')
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof ApiError) ElMessage.error(e.message)
|
|
||||||
} finally {
|
|
||||||
notifySaving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
void loadAgency()
|
|
||||||
void load()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.loc-page {
|
|
||||||
@apply flex h-full flex-col;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,141 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="photo-page">
|
<div>拍照分档配置(C1 实现)</div>
|
||||||
<el-card shadow="never">
|
|
||||||
<div class="mb-md flex items-center gap-sm">
|
|
||||||
<el-select v-model="targetTier" placeholder="目标分档" style="width: 160px">
|
|
||||||
<el-option label="必拍(must)" value="must" />
|
|
||||||
<el-option label="选拍(optional)" value="optional" />
|
|
||||||
<el-option label="不拍(free)" value="free" />
|
|
||||||
</el-select>
|
|
||||||
<el-button
|
|
||||||
v-permission="'svc-org:config'"
|
|
||||||
type="primary"
|
|
||||||
:disabled="selectedRows.length === 0"
|
|
||||||
:loading="saving"
|
|
||||||
@click="applyBatch"
|
|
||||||
>
|
|
||||||
批量应用(已选 {{ selectedRows.length }} 项)
|
|
||||||
</el-button>
|
|
||||||
<span class="text-xs text-text-placeholder">必拍项目放宽为选拍/不拍会增加漏拍风险,不建议放宽。</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table
|
|
||||||
v-loading="loading"
|
|
||||||
:data="rows"
|
|
||||||
border
|
|
||||||
@selection-change="onSelectionChange"
|
|
||||||
>
|
|
||||||
<el-table-column type="selection" width="48" />
|
|
||||||
<el-table-column prop="name" label="项目" min-width="180">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.name }}
|
|
||||||
<el-tag v-if="row.isNewNational" type="warning" size="small" class="ml-xs">国家新增</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="当前分档" width="120" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="PHOTO_TIER_TAG_MAP[row.photoTier]?.type ?? 'info'" size="small">
|
|
||||||
{{ PHOTO_TIER_TAG_MAP[row.photoTier]?.label ?? row.photoTier }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="是否开展" width="100" align="center">
|
|
||||||
<template #default="{ row }">{{ row.isEnabled ? '是' : '否' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
||||||
import type { TagType } from '@/types/crud'
|
|
||||||
import { ApiError } from '@/api/http'
|
|
||||||
import { updatePhotoTier } from '@/api/catalog'
|
|
||||||
import { loadL4Projects, type L4ProjectRow } from '../l4-projects'
|
|
||||||
|
|
||||||
defineOptions({ name: 'svc-config-photo' })
|
defineOptions({ name: 'svc-config-photo' })
|
||||||
|
|
||||||
const PHOTO_TIER_TAG_MAP: Record<string, { label: string; type: TagType }> = {
|
|
||||||
must: { label: '必拍', type: 'danger' },
|
|
||||||
free: { label: '不拍', type: 'info' },
|
|
||||||
optional: { label: '选拍', type: 'warning' },
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = ref<L4ProjectRow[]>([])
|
|
||||||
const selectedRows = ref<L4ProjectRow[]>([])
|
|
||||||
const targetTier = ref('')
|
|
||||||
const loading = ref(false)
|
|
||||||
const saving = ref(false)
|
|
||||||
|
|
||||||
const selectedIds = computed(() => selectedRows.value.map((r) => r.id))
|
|
||||||
|
|
||||||
function onSelectionChange(rows: L4ProjectRow[]): void {
|
|
||||||
selectedRows.value = rows
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load(): Promise<void> {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
rows.value = await loadL4Projects()
|
|
||||||
selectedRows.value = []
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof ApiError) ElMessage.error(e.message)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 应用前预检:must→free/optional 视为「放宽」,需用户确认(不建议放宽)。 */
|
|
||||||
async function applyBatch(): Promise<void> {
|
|
||||||
if (!targetTier.value) {
|
|
||||||
ElMessage.warning('请选择目标分档')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const relaxCount = selectedRows.value.filter(
|
|
||||||
(r) => r.photoTier === 'must' && targetTier.value !== 'must',
|
|
||||||
).length
|
|
||||||
if (relaxCount > 0) {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(
|
|
||||||
`已放宽 ${relaxCount} 项,请确认(必拍项目不建议放宽)。`,
|
|
||||||
'确认批量调整',
|
|
||||||
{ type: 'warning', confirmButtonText: '确认调整', cancelButtonText: '取消' },
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
return // 用户取消
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saving.value = true
|
|
||||||
try {
|
|
||||||
const result = await updatePhotoTier(
|
|
||||||
selectedIds.value.map((id) => ({ id, photoTier: targetTier.value })),
|
|
||||||
)
|
|
||||||
if (result.warnings.length > 0) {
|
|
||||||
ElMessageBox.alert(
|
|
||||||
`已放宽 ${result.warnings.length} 项,请确认(不建议放宽)。`,
|
|
||||||
'放宽提醒',
|
|
||||||
{ type: 'warning' },
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ElMessage.success(`已更新 ${result.updated} 项`)
|
|
||||||
}
|
|
||||||
targetTier.value = ''
|
|
||||||
await load()
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof ApiError) ElMessage.error(e.message)
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.photo-page {
|
|
||||||
@apply flex h-full flex-col;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,69 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="qc-page">
|
<div>质控提醒配置(C1 实现)</div>
|
||||||
<el-card shadow="never">
|
|
||||||
<div v-loading="loading">
|
|
||||||
<el-form label-width="140px" class="qc-page__form">
|
|
||||||
<el-form-item label="质控时限(小时)">
|
|
||||||
<el-input-number v-model="qcThreshold" :min="0" :step="1" step-strictly />
|
|
||||||
<span class="ml-xs text-xs text-text-placeholder">留痕提交后超过该时限未质控即触发提醒</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button v-permission="'agency:update'" type="primary" :loading="saving" @click="save">
|
|
||||||
保存
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { ApiError } from '@/api/http'
|
|
||||||
import { getAgency, updateAgency } from '@/api/agency'
|
|
||||||
|
|
||||||
defineOptions({ name: 'svc-config-qc' })
|
defineOptions({ name: 'svc-config-qc' })
|
||||||
|
|
||||||
const qcThreshold = ref<number | null>(null)
|
|
||||||
const loading = ref(false)
|
|
||||||
const saving = ref(false)
|
|
||||||
|
|
||||||
async function load(): Promise<void> {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const agency = await getAgency()
|
|
||||||
qcThreshold.value = agency.qcThreshold ?? null
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof ApiError) ElMessage.error(e.message)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function save(): Promise<void> {
|
|
||||||
if (qcThreshold.value == null) {
|
|
||||||
ElMessage.warning('请设置质控时限')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
saving.value = true
|
|
||||||
try {
|
|
||||||
await updateAgency({ qcThreshold: qcThreshold.value })
|
|
||||||
ElMessage.success('质控时限已保存')
|
|
||||||
await load()
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof ApiError) ElMessage.error(e.message)
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.qc-page__form {
|
|
||||||
@apply max-w-[560px];
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,327 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="document-page">
|
<div>文书档案(C1–C4 实现)</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="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 v-permission="'export:export'" 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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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' })
|
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>
|
</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>
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user