petstore-admin/src/views/AppointmentsView.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>