This commit is contained in:
2026-05-31 12:09:14 +08:00
commit e4f9eedb80
1121 changed files with 209380 additions and 0 deletions
+603
View File
@@ -0,0 +1,603 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<template #extra>
<a-space>
<a-button @click="reload()">
<template #icon>
<ReloadOutlined />
</template>
刷新
</a-button>
</a-space>
</template>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<!-- 统计卡片 -->
<a-row :gutter="16" class="stats-row">
<a-col :span="6">
<a-statistic title="今日预约" :value="stats.todayCount" :value-style="{ color: '#0e932e' }">
<template #suffix>
<span class="stat-suffix">单</span>
</template>
</a-statistic>
</a-col>
<a-col :span="6">
<a-statistic title="本周预约" :value="stats.weekCount" :value-style="{ color: '#1890ff' }">
<template #suffix>
<span class="stat-suffix">单</span>
</template>
</a-statistic>
</a-col>
<a-col :span="6">
<a-statistic title="本月预约" :value="stats.monthCount" :value-style="{ color: '#722ed1' }">
<template #suffix>
<span class="stat-suffix">单</span>
</template>
</a-statistic>
</a-col>
<a-col :span="6">
<a-statistic title="待确认" :value="stats.pendingCount" :value-style="{ color: '#faad14' }">
<template #suffix>
<span class="stat-suffix">单</span>
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0' }" class="mt-4">
<!-- 搜索区域 -->
<div class="search-container" style="padding: 16px">
<a-form layout="inline" :model="searchForm" class="search-form">
<a-form-item label="门店筛选">
<a-select
v-model:value="searchForm.storeId"
placeholder="请选择门店"
allow-clear
style="width: 180px"
:loading="storeLoading"
@change="handleStoreChange"
>
<a-select-option :value="undefined">全部门店</a-select-option>
<a-select-option v-for="store in storeList" :key="store.id" :value="store.id">
{{ store.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="预约状态">
<a-select
v-model:value="searchForm.status"
placeholder="请选择状态"
allow-clear
style="width: 120px"
>
<a-select-option value="">全部状态</a-select-option>
<a-select-option value="pending">待确认</a-select-option>
<a-select-option value="confirmed">已确认</a-select-option>
<a-select-option value="in_progress">进行中</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="cancelled">已取消</a-select-option>
<a-select-option value="rescheduled">已改签</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="关键词">
<a-input
v-model:value="searchForm.keywords"
placeholder="编号/姓名/电话"
allow-clear
style="width: 150px"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button @click="handleReset">
<template #icon>
<ClearOutlined />
</template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
<!-- 数据表格 -->
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
:scroll="{ x: 1200 }"
@change="handleTableChange"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'bookingNo'">
<span class="booking-no">{{ record.bookingNo || record.id }}</span>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'price'">
<span class="price">¥{{ record.price || 0 }}</span>
</template>
<template v-else-if="column.key === 'createTime'">
{{ formatDateTime(record.createTime) }}
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="viewDetail(record)">
详情
</a-button>
<a-dropdown v-if="record.status === 'pending'">
<a-button type="link" size="small">
操作 <DownOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="confirm" @click="updateStatus(record.id, 'confirmed')">
确认预约
</a-menu-item>
<a-menu-item key="cancel" @click="updateStatus(record.id, 'cancelled')">
取消预约
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-dropdown v-if="record.status === 'confirmed'">
<a-button type="link" size="small">
操作 <DownOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="progress" @click="updateStatus(record.id, 'in_progress')">
开始服务
</a-menu-item>
<a-menu-item key="complete" @click="updateStatus(record.id, 'completed')">
完成服务
</a-menu-item>
<a-menu-item key="cancel" @click="updateStatus(record.id, 'cancelled')">
取消预约
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 详情弹窗 -->
<a-modal
v-model:open="detailVisible"
title="预约详情"
:footer="null"
width="600px"
>
<a-descriptions :column="2" bordered v-if="currentRecord">
<a-descriptions-item label="预约编号">
{{ currentRecord.bookingNo || currentRecord.id }}
</a-descriptions-item>
<a-descriptions-item label="预约状态">
<a-tag :color="getStatusColor(currentRecord.status)">
{{ getStatusText(currentRecord.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="门店名称" :span="2">
{{ currentRecord.storeName }}
</a-descriptions-item>
<a-descriptions-item label="预约日期">
{{ currentRecord.bookingDate }}
</a-descriptions-item>
<a-descriptions-item label="预约时段">
{{ currentRecord.bookingTime }}
</a-descriptions-item>
<a-descriptions-item label="服务类型" :span="2">
{{ currentRecord.serviceName }}
</a-descriptions-item>
<a-descriptions-item label="预约费用">
¥{{ currentRecord.price || 0 }}
</a-descriptions-item>
<a-descriptions-item label="联系人">
{{ currentRecord.contactName }}
</a-descriptions-item>
<a-descriptions-item label="联系电话" :span="2">
{{ currentRecord.contactPhone }}
</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">
{{ currentRecord.remark || '无' }}
</a-descriptions-item>
<a-descriptions-item label="创建时间">
{{ formatDateTime(currentRecord.createTime) }}
</a-descriptions-item>
<a-descriptions-item label="用户ID">
{{ currentRecord.userId }}
</a-descriptions-item>
</a-descriptions>
</a-modal>
</a-page-header>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import {
ReloadOutlined,
SearchOutlined,
ClearOutlined,
DownOutlined
} from '@ant-design/icons-vue'
import request from '@/utils/request'
import type { ApiResult, PageResult } from '@/api'
import dayjs from 'dayjs'
interface ShopBooking {
id: number
bookingNo: string
storeId: number
storeName: string
serviceName: string
bookingDate: string
bookingTime: string
status: string
price: number
remark: string
contactName: string
contactPhone: string
userId: number
createTime: string
}
interface ShopStore {
id: number
name: string
}
export default defineComponent({
name: 'ShopBookingList',
components: {
ReloadOutlined,
SearchOutlined,
ClearOutlined,
DownOutlined
},
setup() {
const loading = ref(false)
const storeLoading = ref(false)
const dataSource = ref<ShopBooking[]>([])
const storeList = ref<ShopStore[]>([])
const detailVisible = ref(false)
const currentRecord = ref<ShopBooking | null>(null)
const stats = reactive({
todayCount: 0,
weekCount: 0,
monthCount: 0,
pendingCount: 0,
totalCount: 0
})
const searchForm = reactive({
storeId: undefined as number | undefined,
status: '' as string,
keywords: ''
})
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条`
})
const columns = [
{
title: '预约编号',
key: 'bookingNo',
width: 150,
fixed: 'left'
},
{
title: '门店名称',
dataIndex: 'storeName',
key: 'storeName',
width: 150,
ellipsis: true
},
{
title: '预约日期',
dataIndex: 'bookingDate',
key: 'bookingDate',
width: 120
},
{
title: '预约时段',
dataIndex: 'bookingTime',
key: 'bookingTime',
width: 120
},
{
title: '服务类型',
dataIndex: 'serviceName',
key: 'serviceName',
width: 100
},
{
title: '联系人',
dataIndex: 'contactName',
key: 'contactName',
width: 80
},
{
title: '联系电话',
dataIndex: 'contactPhone',
key: 'contactPhone',
width: 120
},
{
title: '状态',
key: 'status',
width: 90
},
{
title: '费用',
key: 'price',
width: 80
},
{
title: '创建时间',
key: 'createTime',
width: 160
},
{
title: '操作',
key: 'action',
width: 150,
fixed: 'right'
}
]
// 获取页面标题
const getPageTitle = () => {
return '穿线预约订单'
}
// 状态颜色映射
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
pending: 'orange',
confirmed: 'blue',
in_progress: 'cyan',
completed: 'green',
cancelled: 'red',
rescheduled: 'purple'
}
return colorMap[status] || 'default'
}
// 状态文本映射
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
pending: '待确认',
confirmed: '已确认',
in_progress: '进行中',
completed: '已完成',
cancelled: '已取消',
rescheduled: '已改签'
}
return textMap[status] || status
}
// 格式化日期时间
const formatDateTime = (dateTime: string) => {
if (!dateTime) return '-'
return dayjs(dateTime).format('YYYY-MM-DD HH:mm')
}
// 获取统计数据
const fetchStats = async () => {
try {
const params: any = {}
if (searchForm.storeId) {
params.storeId = searchForm.storeId
}
const res = await request.get<ApiResult<any>>('/shop/shop-booking/admin/stats', { params })
if (res.data.code === 0 && res.data.data) {
stats.todayCount = res.data.data.todayCount || 0
stats.weekCount = res.data.data.weekCount || 0
stats.monthCount = res.data.data.monthCount || 0
stats.pendingCount = res.data.data.pendingCount || 0
}
} catch (e) {
console.error('获取统计数据失败:', e)
}
}
// 获取门店列表
const fetchStoreList = async () => {
try {
storeLoading.value = true
const res = await request.get<ApiResult<ShopStore[]>>('/shop/shop-store', { status: 1 })
if (res.data.code === 0 && res.data.data) {
storeList.value = res.data.data
}
} catch (e) {
console.error('获取门店列表失败:', e)
} finally {
storeLoading.value = false
}
}
// 获取预约列表
const fetchData = async () => {
try {
loading.value = true
const params: any = {
page: pagination.current,
limit: pagination.pageSize
}
if (searchForm.storeId) {
params.storeId = searchForm.storeId
}
if (searchForm.status) {
params.status = searchForm.status
}
if (searchForm.keywords) {
params.keywords = searchForm.keywords
}
const res = await request.get<ApiResult<PageResult<ShopBooking>>>(
'/shop/shop-booking/admin/page',
{ params }
)
if (res.data.code === 0 && res.data.data) {
dataSource.value = res.data.data.list || []
pagination.total = res.data.data.count || 0
} else {
console.error('获取预约列表失败:', res.data.message)
}
} catch (e) {
console.error('获取预约列表失败:', e)
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
pagination.current = 1
fetchData()
fetchStats()
}
// 重置
const handleReset = () => {
searchForm.storeId = undefined
searchForm.status = ''
searchForm.keywords = ''
pagination.current = 1
fetchData()
fetchStats()
}
// 门店变更
const handleStoreChange = () => {
pagination.current = 1
fetchData()
fetchStats()
}
// 表格分页变化
const handleTableChange = (pag: any) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
fetchData()
}
// 刷新
const reload = () => {
fetchData()
fetchStats()
}
// 查看详情
const viewDetail = (record: ShopBooking) => {
currentRecord.value = record
detailVisible.value = true
}
// 更新状态
const updateStatus = async (id: number, status: string) => {
try {
const res = await request.put<ApiResult<any>>(
`/shop/shop-booking/admin/status/${id}`,
null,
{ params: { status } }
)
if (res.data.code === 0) {
message.success('状态更新成功')
fetchData()
fetchStats()
} else {
message.error(res.data.message || '操作失败')
}
} catch (e) {
console.error('更新状态失败:', e)
message.error('操作失败')
}
}
onMounted(() => {
fetchStoreList()
fetchData()
fetchStats()
})
return {
loading,
storeLoading,
dataSource,
storeList,
stats,
searchForm,
pagination,
columns,
detailVisible,
currentRecord,
getPageTitle,
getStatusColor,
getStatusText,
formatDateTime,
handleSearch,
handleReset,
handleStoreChange,
handleTableChange,
reload,
viewDetail,
updateStatus
}
}
})
</script>
<style scoped>
.stats-row {
margin-bottom: 16px;
}
.stat-suffix {
font-size: 14px;
color: #999;
margin-left: 4px;
}
.booking-no {
font-family: monospace;
color: #666;
}
.price {
color: #0e932e;
font-weight: 500;
}
.mt-4 {
margin-top: 16px;
}
.search-container {
background: #fafafa;
border-radius: 4px;
}
</style>