This commit is contained in:
Your Name
2026-04-21 18:02:27 +08:00
parent a1d2ab4649
commit d899e0fe0a
301 changed files with 1937 additions and 344 deletions
+53 -10
View File
@@ -67,6 +67,8 @@
<div class="filter-group">
<span class="filter-label">日期</span>
<el-radio-group v-model="formData.date_preset" size="small" @change="handleDatePresetChange" class="filter-radio">
<el-radio-button value="yesterday">昨天挂号</el-radio-button>
<el-radio-button value="day_before">前天挂号</el-radio-button>
<el-radio-button value="today">当天挂号</el-radio-button>
<el-radio-button value="tomorrow">明天挂号</el-radio-button>
<el-radio-button value="day_after">后天挂号</el-radio-button>
@@ -153,10 +155,20 @@
</template>
</el-table-column>
<el-table-column label="开方" width="80" align="center">
<el-table-column label="开方" width="120" align="center">
<template #default="{ row }">
<el-tag v-if="row.has_prescription" type="success" size="small" effect="plain">已开方</el-tag>
<el-tag v-else type="info" size="small" effect="plain">未开方</el-tag>
<div class="prescription-tags">
<el-tag v-if="row.has_prescription" type="success" size="small" effect="plain">已开方</el-tag>
<el-tag v-else type="info" size="small" effect="plain">未开方</el-tag>
<el-tag
v-if="row.has_prescription && row.prescription_is_system_auto === 1"
type="warning"
size="small"
effect="plain"
>
系统代开
</el-tag>
</div>
</template>
</el-table-column>
@@ -561,7 +573,7 @@ const formData = reactive({
status: '' as string | number,
start_date: '',
end_date: '',
date_preset: 'today' as '' | 'today' | 'tomorrow' | 'day_after',
date_preset: 'today' as '' | 'yesterday' | 'day_before' | 'today' | 'tomorrow' | 'day_after',
diagnosis_confirmed: '' as '' | '0' | '1', // ''=全部 1=已确认 0=未确认
/** 为 1 时后端 extend 返回各状态数量,避免额外 4 次列表请求 */
include_status_counts: 0 as 0 | 1
@@ -589,14 +601,31 @@ watch(
if (!formData.date_preset) return
const today = new Date()
const pad = (n: number) => String(n).padStart(2, '0')
const todayStr = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`
const fmt = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
const todayStr = fmt(today)
const yesterday = new Date(today)
yesterday.setDate(yesterday.getDate() - 1)
const yesterdayStr = fmt(yesterday)
const dayBefore = new Date(today)
dayBefore.setDate(dayBefore.getDate() - 2)
const dayBeforeStr = fmt(dayBefore)
const tomorrow = new Date(today)
tomorrow.setDate(tomorrow.getDate() + 1)
const tomorrowStr = `${tomorrow.getFullYear()}-${pad(tomorrow.getMonth() + 1)}-${pad(tomorrow.getDate())}`
const tomorrowStr = fmt(tomorrow)
const dayAfter = new Date(today)
dayAfter.setDate(dayAfter.getDate() + 2)
const dayAfterStr = `${dayAfter.getFullYear()}-${pad(dayAfter.getMonth() + 1)}-${pad(dayAfter.getDate())}`
const expected = formData.date_preset === 'today' ? todayStr : formData.date_preset === 'tomorrow' ? tomorrowStr : dayAfterStr
const dayAfterStr = fmt(dayAfter)
const preset = formData.date_preset
const expected =
preset === 'yesterday'
? yesterdayStr
: preset === 'day_before'
? dayBeforeStr
: preset === 'today'
? todayStr
: preset === 'tomorrow'
? tomorrowStr
: dayAfterStr
const startStr = (start || '').split(' ')[0]
if (startStr !== expected) {
formData.date_preset = ''
@@ -677,7 +706,13 @@ const handleDatePresetChange = (val: string | number | boolean | undefined) => {
const today = new Date()
const pad = (n: number) => String(n).padStart(2, '0')
let target: Date
if (v === 'today') {
if (v === 'yesterday') {
target = new Date(today)
target.setDate(target.getDate() - 1)
} else if (v === 'day_before') {
target = new Date(today)
target.setDate(target.getDate() - 2)
} else if (v === 'today') {
target = today
} else if (v === 'tomorrow') {
target = new Date(today)
@@ -687,7 +722,7 @@ const handleDatePresetChange = (val: string | number | boolean | undefined) => {
target.setDate(target.getDate() + 2)
}
const dateStr = `${target.getFullYear()}-${pad(target.getMonth() + 1)}-${pad(target.getDate())}`
formData.date_preset = v as '' | 'today' | 'tomorrow' | 'day_after'
formData.date_preset = v as '' | 'yesterday' | 'day_before' | 'today' | 'tomorrow' | 'day_after'
formData.start_date = dateStr
formData.end_date = dateStr
pager.page = 1
@@ -1144,6 +1179,14 @@ onUnmounted(() => {
padding: 0 16px 20px;
}
.prescription-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: center;
align-items: center;
}
.action-btns {
display: flex;
align-items: flex-start;
+79 -22
View File
@@ -50,7 +50,26 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手机号" prop="phone">
<template v-if="showPhoneMaskedEdit">
<el-input
:model-value="
phoneRevealUnlocked ? originalPhone || '' : maskPhone(originalPhone || '')
"
readonly
placeholder="点击可查看完整手机号"
maxlength="11"
class="cursor-pointer phone-mask-toggle"
@click="togglePhoneReveal"
/>
<p
v-if="originalPhone && !phoneRevealUnlocked"
class="text-xs text-gray-400 mt-1"
>
当前为脱敏显示点击输入框可查看完整号码
</p>
</template>
<el-input
v-else
v-model="formData.phone"
placeholder="请输入手机号"
maxlength="11"
@@ -695,6 +714,12 @@ const visible = ref(false)
const formRef = ref()
const mode = ref('add')
const submitting = ref(false)
/** 拥有后手机号可正常编辑,失焦不再强制脱敏 */
const hasPhonePlainPermission = computed(() => hasPermission(['tcm.diagnosis/phonePlain']))
/** 编辑且无明文权限:只读脱敏,点击切换查看完整号 */
const showPhoneMaskedEdit = computed(() => mode.value === 'edit' && !hasPhonePlainPermission.value)
const phoneRevealUnlocked = ref(false)
const activeTab = ref('basic')
const drawerTitle = computed(() => (mode.value === 'add' ? '新增诊单' : '编辑诊单'))
@@ -803,6 +828,11 @@ const maskPhone = (phone: string) => {
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
const togglePhoneReveal = () => {
if (!originalPhone.value) return
phoneRevealUnlocked.value = !phoneRevealUnlocked.value
}
const maskIdCard = (idCard: string) => {
if (!idCard) return idCard
if (idCard.length === 15) {
@@ -813,30 +843,55 @@ const maskIdCard = (idCard: string) => {
return idCard
}
// 手机号聚焦 - 显示完整数据
// 手机号聚焦 - 显示完整数据(无「明文」权限时仅新增模式在失焦后可再次展开编辑)
const handlePhoneFocus = () => {
isPhoneFocused.value = true
if (originalPhone.value) {
if (!originalPhone.value) return
if (hasPhonePlainPermission.value) {
formData.value.phone = originalPhone.value
return
}
if (mode.value === 'add') {
formData.value.phone = originalPhone.value
}
}
// 手机号失焦 - 恢复脱敏并验证
// 手机号失焦:有明文权限则保持明文并校验;新增且无明文权限时仍脱敏展示
const handlePhoneBlur = async () => {
isPhoneFocused.value = false
// 保存原始数据并脱敏显示
if (hasPhonePlainPermission.value) {
if (formData.value.phone && formData.value.phone.length === 11) {
originalPhone.value = formData.value.phone
if (/^1[3-9]\d{9}$/.test(formData.value.phone)) {
try {
const result = await checkPhone({
phone: formData.value.phone,
id: formData.value.id || ''
})
if (result.exists) {
ElMessage.warning(result.message)
}
} catch (error) {
console.error('检查手机号失败:', error)
}
}
}
return
}
if (mode.value !== 'add') {
return
}
if (formData.value.phone && formData.value.phone.length === 11) {
originalPhone.value = formData.value.phone
// 验证手机号唯一性
if (/^1[3-9]\d{9}$/.test(formData.value.phone)) {
try {
const result = await checkPhone({
phone: formData.value.phone,
id: formData.value.id || ''
})
if (result.exists) {
ElMessage.warning(result.message)
}
@@ -844,8 +899,6 @@ const handlePhoneBlur = async () => {
console.error('检查手机号失败:', error)
}
}
// 脱敏显示
formData.value.phone = maskPhone(formData.value.phone)
}
}
@@ -1026,21 +1079,22 @@ const open = async (type: string, id?: number) => {
mode.value = type
visible.value = true
activeTab.value = 'basic' // 重置到基本信息标签页
phoneRevealUnlocked.value = false
// 加载字典数据
await getDictOptions()
if (type === 'edit' && id) {
const data = await tcmDiagnosisDetail({ id })
// 保存原始完整数据
originalPhone.value = data.phone || ''
originalIdCard.value = data.id_card || ''
// 显示脱敏数据
data.phone = maskPhone(data.phone || '')
// 有「手机号明文」权限:表单中保持完整号码;否则表单内为脱敏(编辑无权限时用只读区+点击展开)
data.phone = hasPhonePlainPermission.value ? data.phone || '' : maskPhone(data.phone || '')
data.id_card = maskIdCard(data.id_card || '')
formData.value = data
const y = data.diabetes_discovery_year
formData.value.diabetes_discovery_year =
@@ -1069,8 +1123,9 @@ const handleSubmit = async () => {
// 如果是新增,保存成功后切换到编辑模式,这样可以添加血糖血压记录
if (result && result.id) {
mode.value = 'edit'
phoneRevealUnlocked.value = false
formData.value.id = result.id
// 重新获取详情,确保patient_id等字段正确
try {
const detail = await tcmDiagnosisDetail({ id: result.id })
@@ -1079,9 +1134,10 @@ const handleSubmit = async () => {
// 更新原始数据
originalPhone.value = detail.phone || ''
originalIdCard.value = detail.id_card || ''
// 显示脱敏数据
formData.value.phone = maskPhone(detail.phone || '')
formData.value.phone = hasPhonePlainPermission.value
? detail.phone || ''
: maskPhone(detail.phone || '')
formData.value.id_card = maskIdCard(detail.id_card || '')
} catch (error) {
console.error('获取详情失败:', error)
@@ -1110,7 +1166,7 @@ const handleOpenPrescription = () => {
diagnosis_id: formData.value.id,
appointment_id: 0,
patient_name: formData.value.patient_name,
patient_phone: formData.value.phone,
patient_phone: originalPhone.value || formData.value.phone,
patient_gender: formData.value.gender,
patient_age: formData.value.age,
case_record: JSON.parse(JSON.stringify(formData.value))
@@ -1182,7 +1238,8 @@ const handleClose = () => {
originalIdCard.value = ''
isPhoneFocused.value = false
isIdCardFocused.value = false
phoneRevealUnlocked.value = false
visible.value = false
}
+105 -4
View File
@@ -7,12 +7,35 @@
<span
v-for="tab in dateTabs"
:key="tab.value"
:class="['chip', { active: formData.pending_assign !== '1' && formData.appointment_date === tab.value }]"
:class="[
'chip',
{
active:
formData.pending_assign !== '1' &&
formData.pending_booking !== '1' &&
formData.completed_appointment !== '1' &&
formData.appointment_date === tab.value
}
]"
@click="handleDateTabClick(tab.value)"
>
{{ tab.label }}
<em v-if="tab.value && dateCounts[tab.value] !== undefined">{{ dateCounts[tab.value] }}</em>
</span>
<span
:class="['chip', 'chip-pending-booking', { active: formData.pending_booking === '1' }]"
@click="handlePendingBookingTabClick"
>
待预约
<em v-if="pendingBookingCount !== undefined">{{ pendingBookingCount }}</em>
</span>
<span
:class="['chip', 'chip-completed-visit', { active: formData.completed_appointment === '1' }]"
@click="handleCompletedVisitTabClick"
>
已完成
<em v-if="completedVisitCount !== undefined">{{ completedVisitCount }}</em>
</span>
<span
:class="['chip', 'chip-pending', { active: formData.pending_assign === '1' }]"
@click="handlePendingAssignTabClick"
@@ -584,7 +607,11 @@ const formData = reactive({
diagnosis_confirmed: '' as '' | '0' | '1',
appointment_date: '' as string,
has_appointment: '' as '' | '0' | '1',
pending_assign: '' as '' | '1'
pending_assign: '' as '' | '1',
/** 顶部「待预约」Tab:仅展示已建诊单且尚未挂号(无有效预约) */
pending_booking: '' as '' | '1',
/** 顶部「已完成」Tab:至少有一条挂号记录为已完成 status=3 */
completed_appointment: '' as '' | '1'
})
const activeTab = ref('all')
@@ -603,6 +630,8 @@ const confirmedOptions = [
const setHasAppointment = (v: string) => {
formData.has_appointment = v as '' | '0' | '1'
formData.pending_booking = ''
formData.completed_appointment = ''
pager.page = 1
getLists()
fetchDateCounts()
@@ -610,6 +639,8 @@ const setHasAppointment = (v: string) => {
const setConfirmed = (v: string) => {
formData.diagnosis_confirmed = v as '' | '0' | '1'
formData.pending_booking = ''
formData.completed_appointment = ''
pager.page = 1
getLists()
fetchDateCounts()
@@ -630,31 +661,65 @@ const dateTabs = computed(() => [
])
const dateCounts = ref<Record<string, number>>({})
const pendingBookingCount = ref<number | undefined>(undefined)
const completedVisitCount = ref<number | undefined>(undefined)
const pendingAssignCount = ref<number | undefined>(undefined)
const handleDateTabClick = async (value: string) => {
formData.pending_assign = ''
formData.pending_booking = ''
formData.completed_appointment = ''
formData.has_appointment = ''
formData.appointment_date = value
pager.page = 1
await getLists()
fetchDateCounts()
}
/** 已创建诊单、尚未挂号的记录(与第二行「未挂号」同条件,便于从 Tab 一键进入) */
const handlePendingBookingTabClick = async () => {
formData.pending_assign = ''
formData.pending_booking = '1'
formData.completed_appointment = ''
formData.appointment_date = ''
formData.has_appointment = '0'
pager.page = 1
await getLists()
fetchDateCounts()
}
/** 至少有一条挂号为「已完成」(后端 appointment.status=3) */
const handleCompletedVisitTabClick = async () => {
formData.pending_assign = ''
formData.pending_booking = ''
formData.completed_appointment = '1'
formData.has_appointment = ''
formData.appointment_date = ''
pager.page = 1
await getLists()
fetchDateCounts()
}
const handlePendingAssignTabClick = async () => {
formData.pending_assign = '1'
formData.pending_booking = ''
formData.completed_appointment = ''
formData.has_appointment = ''
formData.appointment_date = ''
pager.page = 1
await getLists()
fetchDateCounts()
}
// 获取各日期挂号数量 + 待分配医助数量
// 获取各日期挂号数量 + 待预约 + 已完成 + 待分配医助数量
const fetchDateCounts = async () => {
try {
const [today, tomorrow, dayAfter, pending] = await Promise.all([
const [today, tomorrow, dayAfter, noApt, doneVisit, pending] = await Promise.all([
tcmDiagnosisLists({ appointment_date: todayStr.value, page_no: 1, page_size: 1 }),
tcmDiagnosisLists({ appointment_date: tomorrowStr.value, page_no: 1, page_size: 1 }),
tcmDiagnosisLists({ appointment_date: dayAfterStr.value, page_no: 1, page_size: 1 }),
tcmDiagnosisLists({ has_appointment: 0, page_no: 1, page_size: 1 }),
tcmDiagnosisLists({ completed_appointment: 1, page_no: 1, page_size: 1 }),
tcmDiagnosisLists({ pending_assign: 1, page_no: 1, page_size: 1 })
])
dateCounts.value = {
@@ -662,6 +727,8 @@ const fetchDateCounts = async () => {
[tomorrowStr.value]: tomorrow?.count ?? 0,
[dayAfterStr.value]: dayAfter?.count ?? 0
}
pendingBookingCount.value = noApt?.count ?? 0
completedVisitCount.value = doneVisit?.count ?? 0
pendingAssignCount.value = pending?.count ?? 0
} catch (e) {
console.error('获取日期数量失败', e)
@@ -710,6 +777,8 @@ const handleTabChange = (tabName: string | number) => {
formData.diagnosis_confirmed = ''
formData.has_appointment = ''
formData.appointment_date = ''
formData.pending_booking = ''
formData.completed_appointment = ''
if (tabName === 'unconfirmed') {
formData.diagnosis_confirmed = '0'
} else if (tabName === 'confirmed') {
@@ -790,6 +859,8 @@ const handleReset = () => {
formData.appointment_date = ''
formData.has_appointment = ''
formData.pending_assign = ''
formData.pending_booking = ''
formData.completed_appointment = ''
pager.page = 1
getLists()
fetchDateCounts()
@@ -1517,6 +1588,36 @@ onUnmounted(() => {
}
}
.chip-pending-booking {
margin-left: 8px;
color: var(--el-color-info);
background: var(--el-color-info-light-9);
&:hover {
background: var(--el-color-info-light-8);
}
&.active {
color: #fff;
background: var(--el-color-info);
}
}
.chip-completed-visit {
margin-left: 8px;
color: var(--el-color-success);
background: var(--el-color-success-light-9);
&:hover {
background: var(--el-color-success-light-8);
}
&.active {
color: #fff;
background: var(--el-color-success);
}
}
.chip-pending {
margin-left: 8px;
color: var(--el-color-warning);