583 lines
23 KiB
Vue
583 lines
23 KiB
Vue
<template>
|
||
<div class="guahao-list">
|
||
<el-card class="!border-none" shadow="never">
|
||
<el-form class="mb-[-16px]" :model="queryParams" :inline="true" @submit.prevent>
|
||
<el-form-item label="预约日期">
|
||
<daterange-picker
|
||
v-model:startTime="queryParams.start_date"
|
||
v-model:endTime="queryParams.end_date"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="患者姓名">
|
||
<el-input
|
||
v-model="queryParams.patient_name"
|
||
placeholder="模糊搜索"
|
||
clearable
|
||
class="!w-[160px]"
|
||
@keyup.enter="resetPage"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="医生">
|
||
<el-input
|
||
v-model="queryParams.doctor_name"
|
||
placeholder="模糊搜索"
|
||
clearable
|
||
class="!w-[140px]"
|
||
@keyup.enter="resetPage"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="医助">
|
||
<el-select
|
||
v-model="queryParams.assistant_id"
|
||
placeholder="全部"
|
||
clearable
|
||
filterable
|
||
class="!w-[180px]"
|
||
>
|
||
<el-option
|
||
v-for="a in assistantOptions"
|
||
:key="a.id"
|
||
:label="a.name + (a.account ? ` (${a.account})` : '')"
|
||
:value="a.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="queryParams.status" placeholder="全部" clearable class="!w-[130px]">
|
||
<el-option label="已预约" :value="1" />
|
||
<el-option label="已取消" :value="2" />
|
||
<el-option label="已完成" :value="3" />
|
||
<el-option label="已过号" :value="4" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="渠道">
|
||
<el-select
|
||
v-model="queryParams.channel_source"
|
||
placeholder="全部"
|
||
clearable
|
||
filterable
|
||
class="!w-[200px]"
|
||
>
|
||
<el-option
|
||
v-for="item in channelOptions"
|
||
:key="String(item.value)"
|
||
:label="item.name"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||
<el-button @click="resetFilter">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<el-card class="!border-none mt-4" shadow="never">
|
||
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||
<el-button
|
||
v-perms="['doctor.appointment/edit']"
|
||
type="primary"
|
||
:disabled="selectedRows.length === 0"
|
||
@click="openBatchChannelDialog"
|
||
>
|
||
批量修改渠道
|
||
</el-button>
|
||
<span v-if="selectedRows.length > 0" class="text-sm text-gray-500">
|
||
已选 {{ selectedRows.length }} 条
|
||
</span>
|
||
</div>
|
||
<el-table
|
||
ref="tableRef"
|
||
v-loading="pager.loading"
|
||
row-key="id"
|
||
:data="pager.lists"
|
||
size="large"
|
||
stripe
|
||
@selection-change="onSelectionChange"
|
||
>
|
||
<el-table-column type="selection" width="48" align="center" reserve-selection />
|
||
<el-table-column label="ID" prop="id" width="72" align="center" />
|
||
<el-table-column label="诊单/患者" min-width="140">
|
||
<template #default="{ row }">
|
||
<div class="text-sm">
|
||
<div class="font-medium">{{ row.patient_name || '—' }}</div>
|
||
<div class="text-gray-500">{{ maskPhone(row.patient_phone) }}</div>
|
||
<div class="text-xs text-gray-400">诊单 #{{ row.diagnosis_id ?? row.patient_id }}</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="医生" prop="doctor_name" width="100" show-overflow-tooltip />
|
||
<el-table-column label="医助" prop="assistant_name" width="100" show-overflow-tooltip />
|
||
<el-table-column label="预约时间" min-width="128">
|
||
<template #default="{ row }">
|
||
<div>{{ row.appointment_date }}</div>
|
||
<div class="text-gray-500">{{ formatHm(row.appointment_time) }} · {{ row.period_desc }}</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="类型" prop="appointment_type_desc" width="100" />
|
||
<el-table-column label="渠道" min-width="130" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<div class="text-sm">{{ row.channel_source_desc || '—' }}</div>
|
||
<div v-if="row.channel_source_detail" class="text-xs text-gray-400 truncate">
|
||
{{ row.channel_source_detail }}
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="96" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag
|
||
:type="
|
||
row.status === 1 ? 'success' : row.status === 2 ? 'info' : row.status === 3 ? 'primary' : 'danger'
|
||
"
|
||
size="small"
|
||
effect="light"
|
||
>
|
||
{{ row.status_desc }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
|
||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<el-button v-perms="['doctor.appointment/edit']" type="primary" link @click="openEdit(row)">
|
||
编辑
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="flex justify-end mt-4">
|
||
<pagination v-model="pager" @change="getLists" />
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-dialog v-model="editVisible" title="编辑挂号" width="560px" destroy-on-close @closed="resetEditForm">
|
||
<el-form :model="editForm" label-width="96px">
|
||
<el-form-item label="预约日期" required>
|
||
<el-date-picker
|
||
v-model="editForm.appointment_date"
|
||
type="date"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="选择日期"
|
||
class="!w-full"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="预约时间" required>
|
||
<el-time-picker
|
||
v-model="editForm.appointment_time"
|
||
format="HH:mm"
|
||
value-format="HH:mm"
|
||
placeholder="时间"
|
||
class="!w-full"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="时段" required>
|
||
<el-radio-group v-model="editForm.period">
|
||
<el-radio-button label="morning">上午</el-radio-button>
|
||
<el-radio-button label="afternoon">下午</el-radio-button>
|
||
<el-radio-button label="all">全天</el-radio-button>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="问诊类型" required>
|
||
<el-select v-model="editForm.appointment_type" class="!w-full">
|
||
<el-option label="视频问诊" value="video" />
|
||
<el-option label="图文问诊" value="text" />
|
||
<el-option label="电话问诊" value="phone" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="渠道来源" required>
|
||
<el-select
|
||
v-model="editForm.channel_source"
|
||
placeholder="请选择渠道来源"
|
||
filterable
|
||
class="!w-full"
|
||
@change="onChannelSourceChange"
|
||
>
|
||
<el-option
|
||
v-for="item in channelOptions"
|
||
:key="String(item.value)"
|
||
:label="item.name"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item v-if="needsChannelSourceDetail" label="自媒体补充" required>
|
||
<el-input
|
||
v-model="editForm.channel_source_detail"
|
||
maxlength="128"
|
||
show-word-limit
|
||
placeholder="请输入自媒体相关补充内容"
|
||
clearable
|
||
class="!w-full"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="状态" required>
|
||
<el-select v-model="editForm.status" class="!w-full">
|
||
<el-option label="已预约" :value="1" />
|
||
<el-option label="已取消" :value="2" />
|
||
<el-option label="已完成" :value="3" />
|
||
<el-option label="已过号" :value="4" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="挂号医助">
|
||
<el-select
|
||
v-model="editForm.assistant_id"
|
||
placeholder="不修改可留空"
|
||
clearable
|
||
filterable
|
||
class="!w-full"
|
||
>
|
||
<el-option
|
||
v-for="a in assistantOptions"
|
||
:key="a.id"
|
||
:label="a.name + (a.account ? ` (${a.account})` : '')"
|
||
:value="a.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input v-model="editForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="editVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="editSaving" @click="submitEdit">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="batchChannelVisible"
|
||
title="批量修改渠道"
|
||
width="520px"
|
||
destroy-on-close
|
||
@closed="resetBatchChannelForm"
|
||
>
|
||
<p class="text-sm text-gray-600 mb-4">将对已选 {{ selectedRows.length }} 条挂号写入同一渠道来源(仅改渠道,不改其它字段)。</p>
|
||
<el-form :model="batchChannelForm" label-width="96px">
|
||
<el-form-item label="渠道来源" required>
|
||
<el-select
|
||
v-model="batchChannelForm.channel_source"
|
||
placeholder="请选择渠道来源"
|
||
filterable
|
||
class="!w-full"
|
||
@change="onBatchChannelSourceChange"
|
||
>
|
||
<el-option
|
||
v-for="item in channelOptions"
|
||
:key="String(item.value)"
|
||
:label="item.name"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item v-if="needsBatchChannelSourceDetail" label="自媒体补充" required>
|
||
<el-input
|
||
v-model="batchChannelForm.channel_source_detail"
|
||
maxlength="128"
|
||
show-word-limit
|
||
placeholder="请输入自媒体相关补充内容"
|
||
clearable
|
||
class="!w-full"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="batchChannelVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="batchChannelSaving" @click="submitBatchChannel">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup name="consumerPrescriptionGuahao">
|
||
import DaterangePicker from '@/components/daterange-picker/index.vue'
|
||
import { getDictData } from '@/api/app'
|
||
import { appointmentAdminEdit, appointmentBatchEditChannel, appointmentLists } from '@/api/doctor'
|
||
import { getAssistants } from '@/api/tcm'
|
||
import { usePaging } from '@/hooks/usePaging'
|
||
import feedback from '@/utils/feedback'
|
||
|
||
const queryParams = reactive({
|
||
start_date: '',
|
||
end_date: '',
|
||
patient_name: '',
|
||
doctor_name: '',
|
||
assistant_id: undefined as number | undefined,
|
||
status: '' as number | '',
|
||
channel_source: '' as string
|
||
})
|
||
|
||
const queryInit = {
|
||
start_date: '',
|
||
end_date: '',
|
||
patient_name: '',
|
||
doctor_name: '',
|
||
assistant_id: undefined as number | undefined,
|
||
status: '' as number | '',
|
||
channel_source: ''
|
||
}
|
||
|
||
const { pager, getLists, resetPage } = usePaging({
|
||
fetchFun: appointmentLists,
|
||
params: queryParams
|
||
})
|
||
|
||
const assistantOptions = ref<Array<{ id: number; name: string; account?: string }>>([])
|
||
|
||
const tableRef = ref<{ clearSelection?: () => void } | null>(null)
|
||
const selectedRows = ref<Record<string, unknown>[]>([])
|
||
|
||
function onSelectionChange(rows: Record<string, unknown>[]) {
|
||
selectedRows.value = rows
|
||
}
|
||
|
||
/** 与诊单预约弹窗一致:这些字典 name 需填「自媒体补充」 */
|
||
const CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL = new Set([
|
||
'自媒体4H',
|
||
'自媒体3Q',
|
||
'自媒体3H',
|
||
'自媒体2H',
|
||
'自媒体2Q'
|
||
])
|
||
|
||
function channelNameRequiresSelfMediaDetail(name: string) {
|
||
return CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL.has(String(name ?? '').trim())
|
||
}
|
||
|
||
const channelOptions = ref<Array<{ name: string; value: string }>>([])
|
||
|
||
const selectedChannelDictName = computed(() => {
|
||
const v = editForm.channel_source
|
||
if (v === '' || v == null) return ''
|
||
const row = channelOptions.value.find((item: { value: string }) => String(item.value) === String(v))
|
||
return row ? String(row.name ?? '').trim() : ''
|
||
})
|
||
|
||
const needsChannelSourceDetail = computed(() => channelNameRequiresSelfMediaDetail(selectedChannelDictName.value))
|
||
|
||
const batchSelectedChannelDictName = computed(() => {
|
||
const v = batchChannelForm.channel_source
|
||
if (v === '' || v == null) return ''
|
||
const row = channelOptions.value.find((item: { value: string }) => String(item.value) === String(v))
|
||
return row ? String(row.name ?? '').trim() : ''
|
||
})
|
||
|
||
const needsBatchChannelSourceDetail = computed(() =>
|
||
channelNameRequiresSelfMediaDetail(batchSelectedChannelDictName.value)
|
||
)
|
||
|
||
function onBatchChannelSourceChange(val: string) {
|
||
const row = channelOptions.value.find((item: { value: string }) => String(item.value) === String(val))
|
||
const name = row ? String(row.name ?? '').trim() : ''
|
||
if (!channelNameRequiresSelfMediaDetail(name)) {
|
||
batchChannelForm.channel_source_detail = ''
|
||
}
|
||
}
|
||
|
||
const batchChannelVisible = ref(false)
|
||
const batchChannelSaving = ref(false)
|
||
const batchChannelForm = reactive({
|
||
channel_source: '' as string,
|
||
channel_source_detail: '' as string
|
||
})
|
||
|
||
function openBatchChannelDialog() {
|
||
if (selectedRows.value.length === 0) {
|
||
feedback.msgWarning('请先勾选挂号记录')
|
||
return
|
||
}
|
||
batchChannelForm.channel_source = ''
|
||
batchChannelForm.channel_source_detail = ''
|
||
batchChannelVisible.value = true
|
||
}
|
||
|
||
function resetBatchChannelForm() {
|
||
batchChannelForm.channel_source = ''
|
||
batchChannelForm.channel_source_detail = ''
|
||
}
|
||
|
||
async function submitBatchChannel() {
|
||
if (selectedRows.value.length === 0) {
|
||
feedback.msgWarning('请先勾选挂号记录')
|
||
return
|
||
}
|
||
if (!batchChannelForm.channel_source) {
|
||
feedback.msgError('请选择渠道来源')
|
||
return
|
||
}
|
||
if (needsBatchChannelSourceDetail.value && !batchChannelForm.channel_source_detail.trim()) {
|
||
feedback.msgError('请填写自媒体补充说明')
|
||
return
|
||
}
|
||
batchChannelSaving.value = true
|
||
try {
|
||
const ids = selectedRows.value.map((r) => Number(r.id))
|
||
await appointmentBatchEditChannel({
|
||
ids,
|
||
channel_source: batchChannelForm.channel_source,
|
||
channel_source_detail: batchChannelForm.channel_source_detail.trim()
|
||
})
|
||
feedback.msgSuccess('批量修改成功')
|
||
batchChannelVisible.value = false
|
||
tableRef.value?.clearSelection?.()
|
||
selectedRows.value = []
|
||
getLists()
|
||
} finally {
|
||
batchChannelSaving.value = false
|
||
}
|
||
}
|
||
|
||
function onChannelSourceChange(val: string) {
|
||
const row = channelOptions.value.find((item: { value: string }) => String(item.value) === String(val))
|
||
const name = row ? String(row.name ?? '').trim() : ''
|
||
if (!channelNameRequiresSelfMediaDetail(name)) {
|
||
editForm.channel_source_detail = ''
|
||
}
|
||
}
|
||
|
||
const editVisible = ref(false)
|
||
const editSaving = ref(false)
|
||
const editForm = reactive({
|
||
id: 0,
|
||
appointment_date: '',
|
||
appointment_time: '' as string,
|
||
period: 'morning' as 'morning' | 'afternoon' | 'all',
|
||
appointment_type: 'video',
|
||
status: 1,
|
||
remark: '',
|
||
assistant_id: undefined as number | undefined,
|
||
channel_source: '' as string,
|
||
channel_source_detail: '' as string
|
||
})
|
||
|
||
function maskPhone(phone?: string) {
|
||
const p = String(phone || '')
|
||
if (p.length >= 11) {
|
||
return p.slice(0, 3) + '****' + p.slice(-4)
|
||
}
|
||
return p || '—'
|
||
}
|
||
|
||
function formatHm(t?: string) {
|
||
const s = String(t || '')
|
||
return s.length >= 5 ? s.slice(0, 5) : s || '—'
|
||
}
|
||
|
||
function resetFilter() {
|
||
Object.assign(queryParams, queryInit)
|
||
resetPage()
|
||
}
|
||
|
||
function rowPeriod(row: Record<string, unknown>): 'morning' | 'afternoon' | 'all' {
|
||
const v = String(row.period ?? row.type ?? 'morning')
|
||
if (v === 'afternoon' || v === 'all') {
|
||
return v
|
||
}
|
||
return 'morning'
|
||
}
|
||
|
||
async function loadAssistants() {
|
||
try {
|
||
const res: any = await getAssistants()
|
||
assistantOptions.value = res?.lists ?? res ?? []
|
||
} catch {
|
||
assistantOptions.value = []
|
||
}
|
||
}
|
||
|
||
async function loadChannelOptions() {
|
||
try {
|
||
const data: any = await getDictData({ type: 'channels' })
|
||
const rows = (data?.channels || []).filter((row: { status?: number }) => row.status !== 0)
|
||
rows.sort((a: { sort?: number; id?: number }, b: { sort?: number; id?: number }) => {
|
||
const ds = Number(b?.sort ?? 0) - Number(a?.sort ?? 0)
|
||
if (ds !== 0) return ds
|
||
return Number(b?.id ?? 0) - Number(a?.id ?? 0)
|
||
})
|
||
channelOptions.value = rows
|
||
} catch {
|
||
channelOptions.value = []
|
||
}
|
||
}
|
||
|
||
function openEdit(row: Record<string, unknown>) {
|
||
editForm.id = Number(row.id)
|
||
editForm.appointment_date = String(row.appointment_date || '')
|
||
editForm.appointment_time = formatHm(String(row.appointment_time || ''))
|
||
editForm.period = rowPeriod(row)
|
||
editForm.appointment_type = String(row.appointment_type || 'video')
|
||
editForm.status = Number(row.status)
|
||
editForm.remark = String(row.remark || '')
|
||
const raw = row.appointment_assistant_id
|
||
editForm.assistant_id =
|
||
raw !== undefined && raw !== null && raw !== '' ? Number(raw) : undefined
|
||
editForm.channel_source = String(row.channel_source ?? row.channels ?? '')
|
||
editForm.channel_source_detail = String(row.channel_source_detail ?? '')
|
||
editVisible.value = true
|
||
}
|
||
|
||
function resetEditForm() {
|
||
editForm.id = 0
|
||
editForm.appointment_date = ''
|
||
editForm.appointment_time = ''
|
||
editForm.period = 'morning'
|
||
editForm.appointment_type = 'video'
|
||
editForm.status = 1
|
||
editForm.remark = ''
|
||
editForm.assistant_id = undefined
|
||
editForm.channel_source = ''
|
||
editForm.channel_source_detail = ''
|
||
}
|
||
|
||
async function submitEdit() {
|
||
if (!editForm.appointment_date) {
|
||
feedback.msgError('请选择预约日期')
|
||
return
|
||
}
|
||
if (!editForm.appointment_time) {
|
||
feedback.msgError('请选择预约时间')
|
||
return
|
||
}
|
||
if (!editForm.channel_source) {
|
||
feedback.msgError('请选择渠道来源')
|
||
return
|
||
}
|
||
if (needsChannelSourceDetail.value && !editForm.channel_source_detail.trim()) {
|
||
feedback.msgError('请填写自媒体补充说明')
|
||
return
|
||
}
|
||
editSaving.value = true
|
||
try {
|
||
const payload: Record<string, unknown> = {
|
||
id: editForm.id,
|
||
appointment_date: editForm.appointment_date,
|
||
appointment_time: editForm.appointment_time,
|
||
period: editForm.period,
|
||
appointment_type: editForm.appointment_type,
|
||
status: editForm.status,
|
||
remark: editForm.remark,
|
||
assistant_id: editForm.assistant_id ?? '',
|
||
channel_source: editForm.channel_source,
|
||
channel_source_detail: editForm.channel_source_detail.trim()
|
||
}
|
||
await appointmentAdminEdit(payload)
|
||
feedback.msgSuccess('保存成功')
|
||
editVisible.value = false
|
||
getLists()
|
||
} finally {
|
||
editSaving.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadAssistants()
|
||
loadChannelOptions()
|
||
})
|
||
|
||
onActivated(() => {
|
||
getLists()
|
||
})
|
||
|
||
getLists()
|
||
</script>
|