275 lines
8.9 KiB
Vue
275 lines
8.9 KiB
Vue
<template>
|
|
<div class="page-card">
|
|
<h2 class="page-title">预约中心</h2>
|
|
<div class="filter-row">
|
|
<el-date-picker
|
|
v-model="dateRange"
|
|
type="daterange"
|
|
value-format="YYYY-MM-DD"
|
|
start-placeholder="开始日"
|
|
end-placeholder="结束日"
|
|
style="width: 260px"
|
|
/>
|
|
<el-select v-model="status" clearable placeholder="状态" style="width: 140px">
|
|
<el-option label="待开始" value="new" />
|
|
<el-option label="服务中" value="doing" />
|
|
<el-option label="已完成" value="done" />
|
|
<el-option label="已取消" value="cancel" />
|
|
</el-select>
|
|
<el-select v-model="staffId" clearable placeholder="技师" style="width: 140px">
|
|
<el-option v-for="s in staffOptions" :key="s.id" :label="s.name" :value="s.id" />
|
|
</el-select>
|
|
<el-checkbox v-model="missingReport">待出报告</el-checkbox>
|
|
<el-input v-model="keyword" clearable placeholder="宠主/手机/宠物名" style="width: 200px" />
|
|
<el-button type="primary" @click="load">查询</el-button>
|
|
<el-button @click="reset">重置</el-button>
|
|
</div>
|
|
|
|
<el-table :data="rows" v-loading="loading" stripe>
|
|
<el-table-column prop="appointmentTime" label="预约时间" min-width="160" />
|
|
<el-table-column label="状态" width="100">
|
|
<template #default="{ row }">{{ statusLabel(row.status) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="宠主" min-width="140">
|
|
<template #default="{ row }">
|
|
{{ row.customerName || '—' }}
|
|
<span class="muted">{{ row.customerPhoneMasked || '' }}</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="petName" label="宠物" width="100" />
|
|
<el-table-column prop="serviceType" label="服务" min-width="110" />
|
|
<el-table-column prop="staffName" label="技师" width="100" />
|
|
<el-table-column label="报告" width="80">
|
|
<template #default="{ row }">
|
|
<el-button v-if="row.hasReport && row.reportId" link type="primary" @click="goReport(row)">有</el-button>
|
|
<span v-else>无</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="260" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
|
|
<el-button v-if="row.status === 'new'" link type="primary" @click="onStart(row)">开始服务</el-button>
|
|
<el-button
|
|
v-if="row.status === 'new' || row.status === 'doing'"
|
|
link
|
|
type="danger"
|
|
@click="onCancel(row)"
|
|
>取消</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<el-drawer v-model="drawer" title="预约详情" size="420px">
|
|
<template v-if="current">
|
|
<el-descriptions :column="1" border>
|
|
<el-descriptions-item label="时间">{{ current.appointmentTime }}</el-descriptions-item>
|
|
<el-descriptions-item label="状态">{{ statusLabel(current.status) }}</el-descriptions-item>
|
|
<el-descriptions-item label="宠主">{{ current.customerName }} {{ current.customerPhoneMasked }}</el-descriptions-item>
|
|
<el-descriptions-item label="宠物">{{ current.petName }} / {{ current.petType }}</el-descriptions-item>
|
|
<el-descriptions-item label="服务">{{ current.serviceType }}</el-descriptions-item>
|
|
<el-descriptions-item label="技师">{{ current.staffName || '—' }}</el-descriptions-item>
|
|
<el-descriptions-item label="备注">{{ current.remark || '—' }}</el-descriptions-item>
|
|
<el-descriptions-item label="报告">{{ current.hasReport ? `已有 #${current.reportId}` : '无' }}</el-descriptions-item>
|
|
</el-descriptions>
|
|
<el-alert
|
|
class="hint"
|
|
type="info"
|
|
:closable="false"
|
|
title="拍照与填报告请回小程序完成"
|
|
/>
|
|
<div class="drawer-actions">
|
|
<el-button v-if="current.status === 'new'" type="primary" @click="onStart(current)">开始服务</el-button>
|
|
<el-button v-if="current.hasReport && current.reportId" @click="goReport(current)">查看报告</el-button>
|
|
</div>
|
|
</template>
|
|
</el-drawer>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, ref } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import {
|
|
cancelAppointment,
|
|
getAppointmentDetail,
|
|
getAppointmentList,
|
|
getStaffList,
|
|
startAppointment,
|
|
} from '@/api'
|
|
|
|
type Appt = {
|
|
id: number
|
|
appointmentTime?: string
|
|
status?: string
|
|
petName?: string
|
|
petType?: string
|
|
serviceType?: string
|
|
customerUserId?: number
|
|
createdByUserId?: number
|
|
assignedStaffId?: number
|
|
assignedUserId?: number
|
|
staffName?: string
|
|
customerName?: string
|
|
customerPhoneMasked?: string
|
|
hasReport?: boolean
|
|
reportId?: number | null
|
|
remark?: string
|
|
updateTime?: string
|
|
}
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const loading = ref(false)
|
|
const rows = ref<Appt[]>([])
|
|
const status = ref<string>((route.query.status as string) || '')
|
|
const keyword = ref('')
|
|
const dateFilter = ref((route.query.date as string) || '')
|
|
const dateRange = ref<[string, string] | ''>('')
|
|
const staffId = ref<number | undefined>()
|
|
const missingReport = ref(route.query.missingReport === '1')
|
|
const staffOptions = ref<Array<{ id: number; name: string }>>([])
|
|
const drawer = ref(false)
|
|
const current = ref<Appt | null>(null)
|
|
|
|
function statusLabel(s?: string) {
|
|
switch (s) {
|
|
case 'new': return '待开始'
|
|
case 'doing': return '服务中'
|
|
case 'done': return '已完成'
|
|
case 'cancel': return '已取消'
|
|
default: return s || '-'
|
|
}
|
|
}
|
|
|
|
function todayPrefix() {
|
|
const d = new Date()
|
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
}
|
|
|
|
function inDateRange(iso?: string) {
|
|
if (!iso) return false
|
|
const day = String(iso).slice(0, 10)
|
|
if (dateFilter.value === 'today') {
|
|
return day === todayPrefix()
|
|
}
|
|
if (Array.isArray(dateRange.value) && dateRange.value.length === 2) {
|
|
return day >= dateRange.value[0] && day <= dateRange.value[1]
|
|
}
|
|
return true
|
|
}
|
|
|
|
async function loadStaff() {
|
|
const res = await getStaffList()
|
|
const list = Array.isArray(res.data) ? (res.data as Array<Record<string, unknown>>) : []
|
|
staffOptions.value = list.map((u) => ({
|
|
id: Number(u.id),
|
|
name: String(u.name || u.phone || u.id),
|
|
}))
|
|
}
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
try {
|
|
const res = await getAppointmentList({
|
|
status: status.value || undefined,
|
|
page: 1,
|
|
pageSize: 200,
|
|
})
|
|
let list = Array.isArray(res.data) ? (res.data as Appt[]) : []
|
|
list = list.filter((a) => inDateRange(a.appointmentTime))
|
|
if (staffId.value != null) {
|
|
list = list.filter((a) => (a.assignedStaffId ?? a.assignedUserId) === staffId.value)
|
|
}
|
|
if (missingReport.value) {
|
|
list = list.filter((a) => a.status === 'doing' && !a.hasReport)
|
|
}
|
|
if (keyword.value.trim()) {
|
|
const q = keyword.value.trim()
|
|
list = list.filter((a) =>
|
|
(a.petName || '').includes(q)
|
|
|| (a.customerName || '').includes(q)
|
|
|| (a.customerPhoneMasked || '').includes(q),
|
|
)
|
|
}
|
|
rows.value = list
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function reset() {
|
|
status.value = ''
|
|
keyword.value = ''
|
|
dateFilter.value = ''
|
|
dateRange.value = ''
|
|
staffId.value = undefined
|
|
missingReport.value = false
|
|
load()
|
|
}
|
|
|
|
async function openDetail(row: Appt) {
|
|
const res = await getAppointmentDetail(row.id)
|
|
if (res.code !== 200 || !res.data) {
|
|
ElMessage.error(res.message || '加载详情失败')
|
|
return
|
|
}
|
|
current.value = res.data as Appt
|
|
drawer.value = true
|
|
}
|
|
|
|
function goReport(row: Appt) {
|
|
if (!row.reportId) return
|
|
router.push({ path: '/reports', query: { q: String(row.reportId) } })
|
|
}
|
|
|
|
async function onStart(row: Appt) {
|
|
await ElMessageBox.confirm(`确认开始服务「${row.petName || row.id}」?`, '开始服务')
|
|
const res = await startAppointment(row.id)
|
|
if (res.code !== 200) {
|
|
ElMessage.error(res.message || '开始失败')
|
|
return
|
|
}
|
|
ElMessage.success('已开始服务')
|
|
drawer.value = false
|
|
await load()
|
|
}
|
|
|
|
async function onCancel(row: Appt) {
|
|
await ElMessageBox.confirm(`确认取消预约「${row.petName || row.id}」?`, '取消预约', { type: 'warning' })
|
|
const res = await cancelAppointment(row.id)
|
|
if (res.code !== 200) {
|
|
ElMessage.error(res.message || '取消失败')
|
|
return
|
|
}
|
|
ElMessage.success('已取消')
|
|
drawer.value = false
|
|
await load()
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (dateFilter.value === 'today') {
|
|
const t = todayPrefix()
|
|
dateRange.value = [t, t]
|
|
}
|
|
await loadStaff()
|
|
await load()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.muted {
|
|
margin-left: 6px;
|
|
color: #8a94a6;
|
|
font-size: 12px;
|
|
}
|
|
.hint {
|
|
margin-top: 16px;
|
|
}
|
|
.drawer-actions {
|
|
margin-top: 16px;
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
</style>
|