1210 lines
41 KiB
Vue
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>
|