1
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user