appointment_system/admin/src/views/appointments/index.vue
2025-12-22 18:11:31 +08:00

1210 lines
41 KiB
Vue

<template>
<div class="appointments-container">
<!-- Search and Filter Bar -->
<el-card class="filter-card">
<el-form :inline="true" :model="filters" class="filter-form">
<el-form-item label="搜索">
<el-input
v-model="filters.search"
placeholder="订单号/用户名/手机号"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="filters.status" placeholder="全部" clearable style="width: 120px">
<el-option label="待处理" value="pending" />
<el-option label="已确认" value="confirmed" />
<el-option label="进行中" value="in-progress" />
<el-option label="已完成" value="completed" />
<el-option label="已取消" value="cancelled" />
</el-select>
</el-form-item>
<el-form-item label="服务分类">
<el-select v-model="filters.categoryId" placeholder="全部" clearable style="width: 140px">
<el-option
v-for="category in categories"
:key="category.id"
:label="category.nameZh"
:value="category.id"
/>
</el-select>
</el-form-item>
<el-form-item label="日期范围">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 240px"
@change="handleDateRangeChange"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- Appointment Table -->
<el-card class="table-card">
<el-table
v-loading="loading"
:data="appointments"
stripe
border
style="width: 100%"
@sort-change="handleSortChange"
>
<el-table-column prop="appointmentNo" label="订单号" width="180" show-overflow-tooltip />
<el-table-column label="用户信息" min-width="150">
<template #default="{ row }">
<div class="user-info">
<span class="nickname">{{ row.user?.nickname || '-' }}</span>
<span class="uid">UID: {{ row.user?.uid || '-' }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="服务项目" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
<div class="service-info">
<span class="service-name">{{ row.service?.titleZh || row.hotService?.name_zh || row.serviceType || '-' }}</span>
<el-tag size="small" type="info" v-if="row.service?.category">
{{ row.service.category.nameZh }}
</el-tag>
<el-tag size="small" type="warning" v-else-if="row.hotService">
热门服务
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="realName" label="预约人" width="100" show-overflow-tooltip />
<el-table-column label="联系方式" width="180">
<template #default="{ row }">
<div class="contact-info">
<template v-if="row.contactMethod && row.contactValue">
<el-tag :type="getContactMethodType(row.contactMethod)" size="small">
{{ getContactMethodLabel(row.contactMethod) }}
</el-tag>
<span class="contact-value">{{ row.contactValue }}</span>
</template>
<template v-else-if="row.phone || row.whatsapp || row.wechatId">
<template v-if="row.phone">
<el-tag type="" size="small">电话</el-tag>
<span class="contact-value">{{ row.phone }}</span>
</template>
<template v-else-if="row.whatsapp">
<el-tag type="success" size="small">WhatsApp</el-tag>
<span class="contact-value">{{ row.whatsapp }}</span>
</template>
<template v-else-if="row.wechatId">
<el-tag type="primary" size="small">微信</el-tag>
<span class="contact-value">{{ row.wechatId }}</span>
</template>
</template>
<span v-else>-</span>
</div>
</template>
</el-table-column>
<el-table-column label="预约时间" width="160">
<template #default="{ row }">
<div v-if="row.appointmentDate || row.departureDate || row.checkInDate || row.travelDate">
<div>{{ row.appointmentDate || row.departureDate || row.checkInDate || row.travelDate }}</div>
<div class="time-text">{{ row.appointmentTime || '-' }}</div>
</div>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)" size="small">
{{ getStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="170" sortable="custom">
<template #default="{ row }">
{{ formatDate(row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleViewDetails(row)">
<el-icon><View /></el-icon>
详情
</el-button>
<el-button type="warning" link size="small" @click="handleUpdateStatus(row)">
<el-icon><Edit /></el-icon>
状态
</el-button>
<el-button type="success" link size="small" @click="handleCreatePaymentOrder(row)">
<el-icon><Money /></el-icon>
收款
</el-button>
</template>
</el-table-column>
</el-table>
<!-- Pagination -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.limit"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
<!-- Appointment Details Dialog -->
<el-dialog
v-model="detailsDialogVisible"
title="订单详情"
width="800px"
destroy-on-close
>
<div v-loading="detailsLoading" class="appointment-details">
<template v-if="appointmentDetails">
<!-- Basic Info -->
<el-descriptions title="订单信息" :column="2" border>
<el-descriptions-item label="订单号">
{{ appointmentDetails.appointmentNo }}
</el-descriptions-item>
<el-descriptions-item label="订单状态">
<el-tag :type="getStatusTagType(appointmentDetails.status)" size="small">
{{ getStatusLabel(appointmentDetails.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="服务项目">
{{ appointmentDetails.service?.titleZh || appointmentDetails.hotService?.name_zh || '-' }}
</el-descriptions-item>
<el-descriptions-item label="服务分类">
{{ appointmentDetails.service?.category?.nameZh || (appointmentDetails.hotService ? '热门服务' : '-') }}
</el-descriptions-item>
<el-descriptions-item label="服务类型" v-if="appointmentDetails.serviceType">
{{ getServiceTypeLabel(appointmentDetails.serviceType) }}
</el-descriptions-item>
<el-descriptions-item label="订单金额">
<span class="amount" v-if="appointmentDetails.amount">
¥{{ parseFloat(appointmentDetails.amount).toFixed(2) }}
</span>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="支付时间">
{{ appointmentDetails.paidAt ? formatDate(appointmentDetails.paidAt) : '-' }}
</el-descriptions-item>
<el-descriptions-item label="预约日期" v-if="appointmentDetails.appointmentDate">
{{ appointmentDetails.appointmentDate }}
</el-descriptions-item>
<el-descriptions-item label="预约时间" v-if="appointmentDetails.appointmentTime">
{{ appointmentDetails.appointmentTime }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(appointmentDetails.createdAt) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ formatDate(appointmentDetails.updatedAt) }}
</el-descriptions-item>
</el-descriptions>
<!-- Contact Info -->
<el-descriptions title="联系信息" :column="2" border class="mt-20">
<el-descriptions-item label="预约人姓名">
{{ appointmentDetails.realName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="联系方式" v-if="appointmentDetails.contactMethod && appointmentDetails.contactValue">
<el-tag :type="getContactMethodType(appointmentDetails.contactMethod)" size="small">
{{ getContactMethodLabel(appointmentDetails.contactMethod) }}
</el-tag>
{{ appointmentDetails.contactValue }}
</el-descriptions-item>
<el-descriptions-item label="手机号" v-if="appointmentDetails.phone">
{{ appointmentDetails.phoneCountryCode ? `+${appointmentDetails.phoneCountryCode} ` : '' }}{{ appointmentDetails.phone }}
</el-descriptions-item>
<el-descriptions-item label="WhatsApp" v-if="appointmentDetails.whatsapp">
{{ appointmentDetails.whatsapp }}
</el-descriptions-item>
<el-descriptions-item label="微信号" v-if="appointmentDetails.wechatId">
{{ appointmentDetails.wechatId }}
</el-descriptions-item>
</el-descriptions>
<!-- Flight Booking Fields -->
<el-descriptions
title="机票预约信息"
:column="2"
border
class="mt-20"
v-if="hasFlightFields(appointmentDetails)"
>
<el-descriptions-item label="行程类型" v-if="appointmentDetails.tripType">
{{ appointmentDetails.tripType === 'single' ? '单程' : '往返' }}
</el-descriptions-item>
<el-descriptions-item label="舱位类型" v-if="appointmentDetails.cabinType">
{{ getCabinTypeLabel(appointmentDetails.cabinType) }}
</el-descriptions-item>
<el-descriptions-item label="出发城市" v-if="appointmentDetails.departureCity">
{{ appointmentDetails.departureCity }}
</el-descriptions-item>
<el-descriptions-item label="到达城市" v-if="appointmentDetails.arrivalCity">
{{ appointmentDetails.arrivalCity }}
</el-descriptions-item>
<el-descriptions-item label="出发日期" v-if="appointmentDetails.departureDate">
{{ appointmentDetails.departureDate }}
</el-descriptions-item>
<el-descriptions-item label="返程日期" v-if="appointmentDetails.returnDate">
{{ appointmentDetails.returnDate }}
</el-descriptions-item>
<el-descriptions-item label="成人人数" v-if="appointmentDetails.adultCount">
{{ appointmentDetails.adultCount }}人
</el-descriptions-item>
<el-descriptions-item label="儿童人数" v-if="appointmentDetails.childCount">
{{ appointmentDetails.childCount }}人
</el-descriptions-item>
<el-descriptions-item label="婴儿人数" v-if="appointmentDetails.infantCount">
{{ appointmentDetails.infantCount }}人
</el-descriptions-item>
<el-descriptions-item label="行李件数" v-if="appointmentDetails.luggageCount">
{{ appointmentDetails.luggageCount }}件
</el-descriptions-item>
</el-descriptions>
<!-- Hotel Booking Fields -->
<el-descriptions
title="酒店预订信息"
:column="2"
border
class="mt-20"
v-if="hasHotelFields(appointmentDetails)"
>
<el-descriptions-item label="入住日期" v-if="appointmentDetails.checkInDate">
{{ appointmentDetails.checkInDate }}
</el-descriptions-item>
<el-descriptions-item label="退房日期" v-if="appointmentDetails.checkOutDate">
{{ appointmentDetails.checkOutDate }}
</el-descriptions-item>
<el-descriptions-item label="国家/城市" v-if="appointmentDetails.countryCity">
{{ appointmentDetails.countryCity }}
</el-descriptions-item>
<el-descriptions-item label="酒店名称" v-if="appointmentDetails.hotelName">
{{ appointmentDetails.hotelName }}
</el-descriptions-item>
<el-descriptions-item label="房间数量" v-if="appointmentDetails.roomCount">
{{ appointmentDetails.roomCount }}间
</el-descriptions-item>
<el-descriptions-item label="房间类型" v-if="appointmentDetails.roomType">
{{ appointmentDetails.roomType }}
</el-descriptions-item>
<el-descriptions-item label="成人人数" v-if="appointmentDetails.adultCount">
{{ appointmentDetails.adultCount }}人
</el-descriptions-item>
<el-descriptions-item label="儿童人数" v-if="appointmentDetails.childCount">
{{ appointmentDetails.childCount }}人
</el-descriptions-item>
<el-descriptions-item label="是否需要餐食" v-if="appointmentDetails.needMeal !== null && appointmentDetails.needMeal !== undefined">
{{ appointmentDetails.needMeal ? '是' : '否' }}
</el-descriptions-item>
<el-descriptions-item label="餐食计划" v-if="appointmentDetails.mealPlan">
{{ getMealPlanLabel(appointmentDetails.mealPlan) }}
</el-descriptions-item>
</el-descriptions>
<!-- VIP Lounge / Airport Transfer Fields -->
<el-descriptions
title="VIP贵宾室/接送机信息"
:column="2"
border
class="mt-20"
v-if="hasVipFields(appointmentDetails)"
>
<el-descriptions-item label="到达航班号" v-if="appointmentDetails.arrivalFlightNo">
{{ appointmentDetails.arrivalFlightNo }}
</el-descriptions-item>
<el-descriptions-item label="出发航班号" v-if="appointmentDetails.departureFlightNo">
{{ appointmentDetails.departureFlightNo }}
</el-descriptions-item>
<el-descriptions-item label="机场/航站楼" v-if="appointmentDetails.airportTerminal">
{{ appointmentDetails.airportTerminal }}
</el-descriptions-item>
<el-descriptions-item label="送达地址" v-if="appointmentDetails.deliveryAddress" :span="2">
{{ appointmentDetails.deliveryAddress }}
</el-descriptions-item>
</el-descriptions>
<!-- Unaccompanied Minor Fields -->
<el-descriptions
title="无成人陪伴儿童信息"
:column="2"
border
class="mt-20"
v-if="hasUnaccompaniedMinorFields(appointmentDetails)"
>
<el-descriptions-item label="儿童年龄" v-if="appointmentDetails.childAge">
{{ appointmentDetails.childAge }}岁
</el-descriptions-item>
<el-descriptions-item label="男孩数量" v-if="appointmentDetails.boyCount">
{{ appointmentDetails.boyCount }}人
</el-descriptions-item>
<el-descriptions-item label="女孩数量" v-if="appointmentDetails.girlCount">
{{ appointmentDetails.girlCount }}人
</el-descriptions-item>
<el-descriptions-item label="行程" v-if="appointmentDetails.itinerary" :span="2">
{{ appointmentDetails.itinerary }}
</el-descriptions-item>
</el-descriptions>
<!-- Train Ticket Fields -->
<el-descriptions
title="高铁票预订信息"
:column="2"
border
class="mt-20"
v-if="hasTrainFields(appointmentDetails)"
>
<el-descriptions-item label="出发站" v-if="appointmentDetails.originStation">
{{ appointmentDetails.originStation }}
</el-descriptions-item>
<el-descriptions-item label="到达站" v-if="appointmentDetails.destinationStation">
{{ appointmentDetails.destinationStation }}
</el-descriptions-item>
<el-descriptions-item label="出发日期" v-if="appointmentDetails.departureDate">
{{ appointmentDetails.departureDate }}
</el-descriptions-item>
<el-descriptions-item label="座位等级" v-if="appointmentDetails.seatClass">
{{ getSeatClassLabel(appointmentDetails.seatClass) }}
</el-descriptions-item>
<el-descriptions-item label="乘客数量" v-if="appointmentDetails.passengerCount">
{{ appointmentDetails.passengerCount }}人
</el-descriptions-item>
</el-descriptions>
<!-- Medical / Special Assistance Fields -->
<el-descriptions
title="医疗/特殊需求信息"
:column="2"
border
class="mt-20"
v-if="hasMedicalFields(appointmentDetails)"
>
<el-descriptions-item label="医院名称" v-if="appointmentDetails.hospitalName">
{{ appointmentDetails.hospitalName }}
</el-descriptions-item>
<el-descriptions-item label="病情描述" v-if="appointmentDetails.conditionDescription" :span="2">
{{ appointmentDetails.conditionDescription }}
</el-descriptions-item>
<el-descriptions-item label="特殊协助原因" v-if="appointmentDetails.specialAssistanceReason" :span="2">
{{ appointmentDetails.specialAssistanceReason }}
</el-descriptions-item>
</el-descriptions>
<!-- Pet Transport Fields -->
<el-descriptions
title="宠物托运信息"
:column="2"
border
class="mt-20"
v-if="hasPetFields(appointmentDetails)"
>
<el-descriptions-item label="出发地" v-if="appointmentDetails.origin">
{{ appointmentDetails.origin }}
</el-descriptions-item>
<el-descriptions-item label="目的地" v-if="appointmentDetails.destination">
{{ appointmentDetails.destination }}
</el-descriptions-item>
<el-descriptions-item label="航班号" v-if="appointmentDetails.flightNo">
{{ appointmentDetails.flightNo }}
</el-descriptions-item>
<el-descriptions-item label="宠物类型" v-if="appointmentDetails.petType">
{{ appointmentDetails.petType }}
</el-descriptions-item>
<el-descriptions-item label="宠物名称" v-if="appointmentDetails.petName">
{{ appointmentDetails.petName }}
</el-descriptions-item>
<el-descriptions-item label="检疫证明" v-if="appointmentDetails.hasQuarantineCert !== null && appointmentDetails.hasQuarantineCert !== undefined">
{{ appointmentDetails.hasQuarantineCert ? '有' : '无' }}
</el-descriptions-item>
</el-descriptions>
<!-- Guide / Translation Service Fields -->
<el-descriptions
title="导游/翻译服务信息"
:column="2"
border
class="mt-20"
v-if="appointmentDetails.serviceDays"
>
<el-descriptions-item label="服务天数">
{{ appointmentDetails.serviceDays }}天
</el-descriptions-item>
</el-descriptions>
<!-- Logistics Fields -->
<el-descriptions
title="物流信息"
:column="2"
border
class="mt-20"
v-if="hasLogisticsFields(appointmentDetails)"
>
<el-descriptions-item label="物品名称" v-if="appointmentDetails.itemName">
{{ appointmentDetails.itemName }}
</el-descriptions-item>
<el-descriptions-item label="物品数量" v-if="appointmentDetails.itemQuantity">
{{ appointmentDetails.itemQuantity }}
</el-descriptions-item>
<el-descriptions-item label="起运港" v-if="appointmentDetails.originPort">
{{ appointmentDetails.originPort }}
</el-descriptions-item>
<el-descriptions-item label="目的港" v-if="appointmentDetails.destinationPort">
{{ appointmentDetails.destinationPort }}
</el-descriptions-item>
<el-descriptions-item label="货物名称" v-if="appointmentDetails.cargoName">
{{ appointmentDetails.cargoName }}
</el-descriptions-item>
<el-descriptions-item label="货物数量" v-if="appointmentDetails.cargoQuantity">
{{ appointmentDetails.cargoQuantity }}
</el-descriptions-item>
</el-descriptions>
<!-- Travel Planning Fields -->
<el-descriptions
title="旅游规划信息"
:column="2"
border
class="mt-20"
v-if="hasTravelFields(appointmentDetails)"
>
<el-descriptions-item label="出行日期" v-if="appointmentDetails.travelDate">
{{ appointmentDetails.travelDate }}
</el-descriptions-item>
<el-descriptions-item label="旅游目的地" v-if="appointmentDetails.travelDestination">
{{ appointmentDetails.travelDestination }}
</el-descriptions-item>
<el-descriptions-item label="旅游天数" v-if="appointmentDetails.travelDays">
{{ appointmentDetails.travelDays }}天
</el-descriptions-item>
</el-descriptions>
<!-- Consulting Service Fields -->
<el-descriptions
title="咨询服务信息"
:column="1"
border
class="mt-20"
v-if="appointmentDetails.specificRequirements"
>
<el-descriptions-item label="具体需求">
{{ appointmentDetails.specificRequirements }}
</el-descriptions-item>
</el-descriptions>
<!-- User Info -->
<el-descriptions title="用户信息" :column="2" border class="mt-20" v-if="appointmentDetails.user">
<el-descriptions-item label="用户昵称">
{{ appointmentDetails.user.nickname || '-' }}
</el-descriptions-item>
<el-descriptions-item label="UID">
{{ appointmentDetails.user.uid || '-' }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
语言偏好
<el-tooltip content="用户在小程序中设置的界面语言,联系用户时可参考使用对应语言沟通" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
{{ getLanguageLabel(appointmentDetails.user.language) }}
</el-descriptions-item>
</el-descriptions>
<!-- Notes -->
<div class="notes-section mt-20" v-if="appointmentDetails.notes">
<h4>备注信息</h4>
<el-card shadow="never" class="notes-card">
{{ appointmentDetails.notes }}
</el-card>
</div>
</template>
</div>
<template #footer>
<el-button @click="detailsDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="handleUpdateStatusFromDetails" v-if="appointmentDetails">
更新状态
</el-button>
</template>
</el-dialog>
<!-- Update Status Dialog -->
<el-dialog
v-model="statusDialogVisible"
title="更新订单状态"
width="400px"
destroy-on-close
>
<el-form :model="statusForm" label-width="80px">
<el-form-item label="当前状态">
<el-tag :type="getStatusTagType(statusForm.currentStatus)" size="small">
{{ getStatusLabel(statusForm.currentStatus) }}
</el-tag>
</el-form-item>
<el-form-item label="新状态" required>
<el-select v-model="statusForm.newStatus" placeholder="请选择新状态" style="width: 100%">
<el-option
v-for="status in statusOptions"
:key="status.value"
:label="status.label"
:value="status.value"
:disabled="status.value === statusForm.currentStatus"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="statusDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUpdateStatus" :loading="statusUpdating">
确认更新
</el-button>
</template>
</el-dialog>
<!-- Create Payment Order Dialog -->
<el-dialog
v-model="paymentDialogVisible"
title="创建支付订单"
width="500px"
destroy-on-close
>
<el-form :model="paymentForm" :rules="paymentRules" ref="paymentFormRef" label-width="100px">
<el-form-item label="关联订单">
<el-input :value="paymentForm.appointmentNo" disabled />
</el-form-item>
<el-form-item label="用户">
<el-input :value="paymentForm.userNickname" disabled />
</el-form-item>
<el-form-item label="服务内容" prop="serviceContent">
<el-input v-model="paymentForm.serviceContent" placeholder="服务内容描述" />
</el-form-item>
<el-form-item label="金额" prop="amount">
<el-input-number
v-model="paymentForm.amount"
:min="0.01"
:precision="2"
:step="10"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="支付时间" prop="paymentTime">
<el-date-picker
v-model="paymentForm.paymentTime"
type="datetime"
placeholder="选择支付时间"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="paymentForm.notes"
type="textarea"
:rows="2"
placeholder="可选,填写备注信息"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="paymentDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmCreatePaymentOrder" :loading="paymentCreating">
创建支付订单
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/utils/api'
// State
const loading = ref(false)
const appointments = ref([])
const categories = ref([])
const dateRange = ref(null)
const pagination = reactive({
page: 1,
limit: 20,
total: 0,
totalPages: 0
})
const filters = reactive({
search: '',
status: '',
categoryId: '',
startDate: '',
endDate: ''
})
const sortConfig = reactive({
sortBy: 'createdAt',
sortOrder: 'DESC'
})
// Details dialog state
const detailsDialogVisible = ref(false)
const detailsLoading = ref(false)
const appointmentDetails = ref(null)
// Status update dialog state
const statusDialogVisible = ref(false)
const statusUpdating = ref(false)
const statusForm = reactive({
appointmentId: '',
currentStatus: '',
newStatus: ''
})
// Status options
const statusOptions = [
{ value: 'pending', label: '待处理' },
{ value: 'confirmed', label: '已确认' },
{ value: 'in-progress', label: '进行中' },
{ value: 'completed', label: '已完成' },
{ value: 'cancelled', label: '已取消' }
]
// Payment order dialog state
const paymentDialogVisible = ref(false)
const paymentCreating = ref(false)
const paymentFormRef = ref(null)
const paymentForm = reactive({
appointmentId: '',
appointmentNo: '',
userId: '',
userNickname: '',
serviceContent: '',
amount: 0,
paymentTime: '',
notes: ''
})
const paymentRules = {
serviceContent: [
{ required: true, message: '请输入服务内容', trigger: 'blur' }
],
amount: [
{ required: true, message: '请输入金额', trigger: 'blur' },
{ type: 'number', min: 0.01, message: '金额必须大于0', trigger: 'blur' }
],
paymentTime: [
{ required: true, message: '请选择支付时间', trigger: 'change' }
]
}
// Fetch categories for filter
async function fetchCategories() {
try {
const response = await api.get('/api/v1/categories')
if (response.data.code === 0 || response.data.success) {
categories.value = response.data.data.categories || response.data.data || []
}
} catch (error) {
console.error('Failed to fetch categories:', error)
}
}
// Fetch appointment list
async function fetchAppointments() {
loading.value = true
try {
const params = {
page: pagination.page,
limit: pagination.limit,
...filters,
...sortConfig
}
// Remove empty params
Object.keys(params).forEach(key => {
if (params[key] === '' || params[key] === null || params[key] === undefined) {
delete params[key]
}
})
const response = await api.get('/api/v1/admin/appointments', { params })
console.log('Appointments response:', response.data)
if (response.data.success || response.data.code === 0) {
appointments.value = response.data.data.appointments
pagination.total = response.data.data.pagination.total
pagination.totalPages = response.data.data.pagination.totalPages
}
} catch (error) {
console.error('Failed to fetch appointments:', error)
ElMessage.error('获取订单列表失败')
} finally {
loading.value = false
}
}
// Fetch appointment details
async function fetchAppointmentDetails(appointmentId) {
detailsLoading.value = true
try {
const response = await api.get(`/api/v1/admin/appointments/${appointmentId}`)
if (response.data.success || response.data.code === 0) {
appointmentDetails.value = response.data.data
}
} catch (error) {
console.error('Failed to fetch appointment details:', error)
ElMessage.error('获取订单详情失败')
} finally {
detailsLoading.value = false
}
}
// Handle date range change
function handleDateRangeChange(value) {
if (value && value.length === 2) {
filters.startDate = value[0]
filters.endDate = value[1]
} else {
filters.startDate = ''
filters.endDate = ''
}
}
// Handle search
function handleSearch() {
pagination.page = 1
fetchAppointments()
}
// Handle reset
function handleReset() {
filters.search = ''
filters.status = ''
filters.categoryId = ''
filters.startDate = ''
filters.endDate = ''
dateRange.value = null
pagination.page = 1
fetchAppointments()
}
// Handle page change
function handlePageChange(page) {
pagination.page = page
fetchAppointments()
}
// Handle page size change
function handleSizeChange(size) {
pagination.limit = size
pagination.page = 1
fetchAppointments()
}
// Handle sort change
function handleSortChange({ prop, order }) {
if (prop && order) {
sortConfig.sortBy = prop
sortConfig.sortOrder = order === 'ascending' ? 'ASC' : 'DESC'
} else {
sortConfig.sortBy = 'createdAt'
sortConfig.sortOrder = 'DESC'
}
fetchAppointments()
}
// Handle view details
function handleViewDetails(row) {
appointmentDetails.value = null
detailsDialogVisible.value = true
fetchAppointmentDetails(row.id)
}
// Handle update status
function handleUpdateStatus(row) {
statusForm.appointmentId = row.id
statusForm.currentStatus = row.status
statusForm.newStatus = ''
statusDialogVisible.value = true
}
// Handle update status from details dialog
function handleUpdateStatusFromDetails() {
if (appointmentDetails.value) {
statusForm.appointmentId = appointmentDetails.value.id
statusForm.currentStatus = appointmentDetails.value.status
statusForm.newStatus = ''
statusDialogVisible.value = true
}
}
// Confirm update status
async function confirmUpdateStatus() {
if (!statusForm.newStatus) {
ElMessage.warning('请选择新状态')
return
}
statusUpdating.value = true
try {
const response = await api.put(`/api/v1/admin/appointments/${statusForm.appointmentId}`, {
status: statusForm.newStatus
})
if (response.data.success || response.data.code === 0) {
ElMessage.success('订单状态更新成功')
statusDialogVisible.value = false
// Update the appointment in the list
const index = appointments.value.findIndex(a => a.id === statusForm.appointmentId)
if (index !== -1) {
appointments.value[index].status = statusForm.newStatus
}
// Update details if open
if (appointmentDetails.value && appointmentDetails.value.id === statusForm.appointmentId) {
appointmentDetails.value.status = statusForm.newStatus
}
}
} catch (error) {
console.error('Failed to update appointment status:', error)
ElMessage.error('更新订单状态失败')
} finally {
statusUpdating.value = false
}
}
// Handle create payment order
function handleCreatePaymentOrder(row) {
paymentForm.appointmentId = row.id
paymentForm.appointmentNo = row.appointmentNo
paymentForm.userId = row.user?.id || row.userId
paymentForm.userNickname = `${row.user?.nickname || '-'} (UID: ${row.user?.uid || '-'})`
paymentForm.serviceContent = row.service?.titleZh || row.hotService?.name_zh || row.serviceType || ''
paymentForm.amount = parseFloat(row.amount) || 0
// 默认设置为当前时间
paymentForm.paymentTime = new Date().toISOString().slice(0, 19).replace('T', ' ')
paymentForm.notes = ''
paymentDialogVisible.value = true
}
// Confirm create payment order
async function confirmCreatePaymentOrder() {
if (!paymentFormRef.value) return
await paymentFormRef.value.validate(async (valid) => {
if (!valid) return
paymentCreating.value = true
try {
const response = await api.post('/api/v1/admin/payment-orders', {
userId: paymentForm.userId,
appointmentId: paymentForm.appointmentNo,
amount: paymentForm.amount,
serviceContent: paymentForm.serviceContent,
paymentTime: paymentForm.paymentTime,
notes: paymentForm.notes
})
if (response.data.success || response.data.code === 0) {
ElMessage.success('支付订单创建成功')
paymentDialogVisible.value = false
}
} catch (error) {
console.error('Failed to create payment order:', error)
ElMessage.error(error.response?.data?.message || '创建支付订单失败')
} finally {
paymentCreating.value = false
}
})
}
// Helper functions
function formatDate(dateStr) {
if (!dateStr) return '-'
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
function getStatusLabel(status) {
const labels = {
pending: '待处理',
confirmed: '已确认',
'in-progress': '进行中',
completed: '已完成',
cancelled: '已取消'
}
return labels[status] || status
}
function getStatusTagType(status) {
const types = {
pending: 'warning',
confirmed: 'primary',
'in-progress': '',
completed: 'success',
cancelled: 'danger'
}
return types[status] || 'info'
}
function getContactMethodLabel(method) {
const labels = {
phone: '电话',
whatsapp: 'WhatsApp',
wechat: '微信'
}
return labels[method] || method
}
function getContactMethodType(method) {
const types = {
phone: '',
whatsapp: 'success',
wechat: 'primary'
}
return types[method] || 'info'
}
function getLanguageLabel(lang) {
const labels = {
zh: '中文',
en: 'English',
es: 'Español'
}
return labels[lang] || lang
}
function getServiceTypeLabel(type) {
const labels = {
travel: '旅行服务',
accommodation: '住宿服务',
guide: '导游服务',
translation: '翻译服务',
consulting: '咨询服务',
flight: '机票预订',
hotel: '酒店预订',
train: '高铁票预订',
vip_lounge: 'VIP贵宾室',
airport_transfer: '接送机',
pet_transport: '宠物托运',
medical: '医疗服务',
logistics: '物流服务',
other: '其他服务'
}
return labels[type] || type
}
function getCabinTypeLabel(type) {
const labels = {
economy: '经济舱',
premium_economy: '超级经济舱',
business: '商务舱'
}
return labels[type] || type
}
function getMealPlanLabel(plan) {
const labels = {
breakfast: '早餐',
three_meals: '三餐',
all_inclusive: '全包'
}
return labels[plan] || plan
}
function getSeatClassLabel(seatClass) {
const labels = {
first: '一等座',
second: '二等座',
third: '三等座'
}
return labels[seatClass] || seatClass
}
// Field detection helpers
function hasFlightFields(data) {
return data && (
data.tripType || data.cabinType || data.departureCity || data.arrivalCity ||
data.departureDate || data.returnDate || data.luggageCount ||
(data.adultCount && !data.checkInDate && !data.originStation)
)
}
function hasHotelFields(data) {
return data && (
data.checkInDate || data.checkOutDate || data.countryCity ||
data.hotelName || data.roomCount || data.roomType ||
data.needMeal !== null || data.mealPlan
)
}
function hasVipFields(data) {
return data && (
data.arrivalFlightNo || data.departureFlightNo ||
data.airportTerminal || data.deliveryAddress
)
}
function hasUnaccompaniedMinorFields(data) {
return data && (
data.childAge || data.boyCount || data.girlCount || data.itinerary
)
}
function hasTrainFields(data) {
return data && (
data.originStation || data.destinationStation || data.seatClass || data.passengerCount
)
}
function hasMedicalFields(data) {
return data && (
data.hospitalName || data.conditionDescription || data.specialAssistanceReason
)
}
function hasPetFields(data) {
return data && (
data.petType || data.petName || data.hasQuarantineCert !== null ||
(data.origin && data.destination && data.flightNo)
)
}
function hasLogisticsFields(data) {
return data && (
data.itemName || data.itemQuantity || data.originPort ||
data.destinationPort || data.cargoName || data.cargoQuantity
)
}
function hasTravelFields(data) {
return data && (
data.travelDate || data.travelDestination || data.travelDays
)
}
// Initialize
onMounted(() => {
fetchCategories()
fetchAppointments()
})
</script>
<style lang="scss" scoped>
.appointments-container {
.filter-card {
margin-bottom: 20px;
.filter-form {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
}
.table-card {
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
}
.user-info {
display: flex;
flex-direction: column;
gap: 4px;
.nickname {
font-weight: 500;
color: #303133;
}
.uid {
font-size: 12px;
color: #909399;
}
}
.service-info {
display: flex;
flex-direction: column;
gap: 4px;
.service-name {
font-weight: 500;
}
}
.contact-info {
display: flex;
flex-direction: column;
gap: 4px;
.contact-value {
font-size: 12px;
color: #606266;
}
}
.time-text {
font-size: 12px;
color: #909399;
}
.appointment-details {
.mt-20 {
margin-top: 20px;
}
.amount {
font-weight: 600;
color: #409eff;
font-size: 16px;
}
.notes-section {
h4 {
margin-bottom: 12px;
color: #303133;
font-size: 14px;
}
.notes-card {
background-color: #f5f7fa;
:deep(.el-card__body) {
padding: 12px 16px;
color: #606266;
line-height: 1.6;
white-space: pre-wrap;
}
}
}
}
}
</style>