|
|
<!-- components/QueueTicketDetail/QueueTicketDetail.vue -->
|
|
|
<template>
|
|
|
<!-- 弹窗遮罩 -->
|
|
|
<view v-if="show" class="ticket-modal-mask" @click="handleClose">
|
|
|
<view class="ticket-modal-content" @click.stop>
|
|
|
<!-- 弹窗头部 -->
|
|
|
<view class="ticket-modal-header">
|
|
|
<view class="header-main">
|
|
|
<text class="modal-title">{{ title }}</text>
|
|
|
<text class="modal-subtitle">排队取号详情信息</text>
|
|
|
</view>
|
|
|
<text class="close-btn" @click="handleClose">×</text>
|
|
|
</view>
|
|
|
|
|
|
<!-- 弹窗内容 -->
|
|
|
<scroll-view class="ticket-modal-body" scroll-y :show-scrollbar="true">
|
|
|
<!-- 票号基本信息卡片 -->
|
|
|
<view class="info-card">
|
|
|
<view class="card-header">
|
|
|
<text class="card-icon">🎫</text>
|
|
|
<text class="card-title">票号信息</text>
|
|
|
</view>
|
|
|
<view class="card-content">
|
|
|
<view class="info-row">
|
|
|
<text class="info-label">票号</text>
|
|
|
<text class="info-value highlight">{{ ticketData.ticketNumber }}</text>
|
|
|
</view>
|
|
|
<view class="info-row">
|
|
|
<text class="info-label">状态</text>
|
|
|
<view class="status-badge" :class="getStatusClass(ticketData.status)">
|
|
|
{{ getStatusText(ticketData.status) }}
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
<!-- 时间信息卡片 -->
|
|
|
<view class="info-card">
|
|
|
<view class="card-header">
|
|
|
<text class="card-icon">⏰</text>
|
|
|
<text class="card-title">时间信息</text>
|
|
|
</view>
|
|
|
<view class="card-content compact">
|
|
|
<view class="time-grid">
|
|
|
<view class="time-item">
|
|
|
<text class="time-label">取号日期</text>
|
|
|
<text class="time-value">{{ ticketData.queueDate }}</text>
|
|
|
</view>
|
|
|
<view class="time-item">
|
|
|
<text class="time-label">取号时间</text>
|
|
|
<text class="time-value">{{ ticketData.queueTime }}</text>
|
|
|
</view>
|
|
|
<view class="time-item">
|
|
|
<text class="time-label">等候时长</text>
|
|
|
<text class="time-value duration">{{ ticketData.waitingDuration }}</text>
|
|
|
</view>
|
|
|
<view class="time-item">
|
|
|
<text class="time-label">办理时长</text>
|
|
|
<text class="time-value duration">{{ ticketData.processingDuration }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
<!-- 办理信息卡片 -->
|
|
|
<view class="info-card">
|
|
|
<view class="card-header">
|
|
|
<text class="card-icon">🏢</text>
|
|
|
<text class="card-title">办理信息</text>
|
|
|
</view>
|
|
|
<view class="card-content compact">
|
|
|
<view class="info-row">
|
|
|
<text class="info-label">办理窗口</text>
|
|
|
<text class="info-value accent">{{ ticketData.serviceWindow }}</text>
|
|
|
</view>
|
|
|
<view class="info-row">
|
|
|
<text class="info-label">办理业务</text>
|
|
|
<text class="info-value">{{ ticketData.businessType }}</text>
|
|
|
</view>
|
|
|
<view class="info-row">
|
|
|
<text class="info-label">工作人员</text>
|
|
|
<text class="info-value primary">{{ ticketData.staffName }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
<!-- 评价信息卡片 -->
|
|
|
<view v-if="ticketData.rating" class="info-card">
|
|
|
<view class="card-header">
|
|
|
<text class="card-icon">⭐</text>
|
|
|
<text class="card-title">服务评价</text>
|
|
|
</view>
|
|
|
<view class="card-content">
|
|
|
<!-- 星级评价 -->
|
|
|
<view class="rating-display">
|
|
|
<view class="stars-container">
|
|
|
<text class="star" v-for="n in 5" :key="n"
|
|
|
:class="{ active: n <= getStarCount(ticketData.rating) }">★</text>
|
|
|
</view>
|
|
|
<text class="rating-text">{{ getRatingText(ticketData.rating) }}</text>
|
|
|
</view>
|
|
|
|
|
|
<!-- 评价详情 -->
|
|
|
<view class="rating-details">
|
|
|
<view class="info-row">
|
|
|
<text class="info-label">评价人</text>
|
|
|
<text class="info-value">{{ ticketData.ratingPerson }}</text>
|
|
|
</view>
|
|
|
<view class="info-row">
|
|
|
<text class="info-label">联系方式</text>
|
|
|
<text class="info-value contact">{{ ticketData.ratingContact }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
<!-- 无评价提示 -->
|
|
|
<view v-else class="empty-card">
|
|
|
<text class="empty-icon">💬</text>
|
|
|
<text class="empty-text">暂未收到评价</text>
|
|
|
<text class="empty-desc">服务完成后可进行评价</text>
|
|
|
</view>
|
|
|
</scroll-view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
import { computed } from 'vue';
|
|
|
import type {
|
|
|
QueueTicketDetailProps,
|
|
|
QueueTicketDetailEmits,
|
|
|
TicketDetail
|
|
|
} from './QueueTicketDetail.type';
|
|
|
import {
|
|
|
defaultTicketData,
|
|
|
TicketStatus,
|
|
|
RatingResult
|
|
|
} from './QueueTicketDetail.type';
|
|
|
|
|
|
// 组件Props
|
|
|
const props = withDefaults(defineProps<QueueTicketDetailProps>(), {
|
|
|
title: '票号详情',
|
|
|
ticketData: () => defaultTicketData
|
|
|
});
|
|
|
|
|
|
// 组件Emits
|
|
|
const emit = defineEmits<QueueTicketDetailEmits>();
|
|
|
|
|
|
// 计算属性
|
|
|
const ticketData = computed(() => props.ticketData);
|
|
|
|
|
|
// 状态文本映射
|
|
|
const getStatusText = (status: TicketStatus): string => {
|
|
|
const statusMap = {
|
|
|
[TicketStatus.WAITING]: '等待中',
|
|
|
[TicketStatus.PROCESSING]: '办理中',
|
|
|
[TicketStatus.COMPLETED]: '已完成',
|
|
|
[TicketStatus.CANCELLED]: '已取消'
|
|
|
};
|
|
|
return statusMap[status] || '未知状态';
|
|
|
};
|
|
|
|
|
|
// 状态样式类
|
|
|
const getStatusClass = (status: TicketStatus): string => {
|
|
|
const classMap = {
|
|
|
[TicketStatus.WAITING]: 'status-waiting',
|
|
|
[TicketStatus.PROCESSING]: 'status-processing',
|
|
|
[TicketStatus.COMPLETED]: 'status-completed',
|
|
|
[TicketStatus.CANCELLED]: 'status-cancelled'
|
|
|
};
|
|
|
return classMap[status] || '';
|
|
|
};
|
|
|
|
|
|
// 评价文本映射
|
|
|
const getRatingText = (rating: RatingResult): string => {
|
|
|
const ratingMap = {
|
|
|
[RatingResult.EXCELLENT]: '非常满意',
|
|
|
[RatingResult.GOOD]: '满意',
|
|
|
[RatingResult.AVERAGE]: '一般',
|
|
|
[RatingResult.POOR]: '不满意'
|
|
|
};
|
|
|
return ratingMap[rating] || '未评价';
|
|
|
};
|
|
|
|
|
|
// 获取星星数量
|
|
|
const getStarCount = (rating: RatingResult): number => {
|
|
|
const starMap = {
|
|
|
[RatingResult.EXCELLENT]: 5,
|
|
|
[RatingResult.GOOD]: 4,
|
|
|
[RatingResult.AVERAGE]: 3,
|
|
|
[RatingResult.POOR]: 2
|
|
|
};
|
|
|
return starMap[rating] || 0;
|
|
|
};
|
|
|
|
|
|
// 事件处理
|
|
|
const handleClose = () => {
|
|
|
emit('update:show', false);
|
|
|
emit('close');
|
|
|
};
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
.ticket-modal-mask {
|
|
|
position: fixed;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
right: 0;
|
|
|
bottom: 0;
|
|
|
background-color: rgba(0, 0, 0, 0.6);
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
z-index: 9999;
|
|
|
padding: 40rpx;
|
|
|
}
|
|
|
|
|
|
.ticket-modal-content {
|
|
|
background: white;
|
|
|
border-radius: 20rpx;
|
|
|
width: 100%;
|
|
|
max-height: 75vh;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.25);
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
/* 头部样式 */
|
|
|
.ticket-modal-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: flex-start;
|
|
|
padding: 32rpx 32rpx 24rpx;
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
position: sticky;
|
|
|
top: 0;
|
|
|
z-index: 10;
|
|
|
}
|
|
|
|
|
|
.header-main {
|
|
|
flex: 1;
|
|
|
}
|
|
|
|
|
|
.modal-title {
|
|
|
font-size: 36rpx;
|
|
|
font-weight: bold;
|
|
|
color: white;
|
|
|
display: block;
|
|
|
margin-bottom: 8rpx;
|
|
|
line-height: 1.2;
|
|
|
}
|
|
|
|
|
|
.modal-subtitle {
|
|
|
font-size: 24rpx;
|
|
|
color: rgba(255, 255, 255, 0.9);
|
|
|
display: block;
|
|
|
line-height: 1.2;
|
|
|
}
|
|
|
|
|
|
.close-btn {
|
|
|
font-size: 40rpx;
|
|
|
color: white;
|
|
|
padding: 8rpx 16rpx;
|
|
|
font-weight: 300;
|
|
|
line-height: 1;
|
|
|
}
|
|
|
|
|
|
/* 主体内容 */
|
|
|
.ticket-modal-body {
|
|
|
flex: 1;
|
|
|
padding: 0 24rpx 24rpx;
|
|
|
max-height: calc(75vh - 120rpx);
|
|
|
}
|
|
|
|
|
|
/* 信息卡片 */
|
|
|
.info-card {
|
|
|
background: white;
|
|
|
border-radius: 16rpx;
|
|
|
margin: 24rpx 0 0;
|
|
|
border: 1rpx solid #e8e8e8;
|
|
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.info-card:first-child {
|
|
|
margin-top: 24rpx;
|
|
|
}
|
|
|
|
|
|
.card-header {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
padding: 24rpx 28rpx;
|
|
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
|
|
border-bottom: 1rpx solid #f1f5f9;
|
|
|
}
|
|
|
|
|
|
.card-icon {
|
|
|
font-size: 28rpx;
|
|
|
margin-right: 16rpx;
|
|
|
}
|
|
|
|
|
|
.card-title {
|
|
|
font-size: 30rpx;
|
|
|
font-weight: 600;
|
|
|
color: #1e293b;
|
|
|
}
|
|
|
|
|
|
.card-content {
|
|
|
padding: 28rpx;
|
|
|
}
|
|
|
|
|
|
.card-content.compact {
|
|
|
padding: 20rpx 28rpx;
|
|
|
}
|
|
|
|
|
|
/* 信息行 */
|
|
|
.info-row {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
padding: 20rpx 0;
|
|
|
min-height: 40rpx;
|
|
|
}
|
|
|
|
|
|
.info-row:not(:last-child) {
|
|
|
border-bottom: 1rpx solid #f8fafc;
|
|
|
}
|
|
|
|
|
|
.info-label {
|
|
|
font-size: 28rpx;
|
|
|
color: #64748b;
|
|
|
font-weight: 500;
|
|
|
min-width: 180rpx;
|
|
|
}
|
|
|
|
|
|
.info-value {
|
|
|
font-size: 28rpx;
|
|
|
color: #1e293b;
|
|
|
font-weight: 600;
|
|
|
text-align: right;
|
|
|
flex: 1;
|
|
|
margin-left: 20rpx;
|
|
|
line-height: 1.4;
|
|
|
}
|
|
|
|
|
|
/* 特殊样式 */
|
|
|
.info-value.highlight {
|
|
|
color: #667eea;
|
|
|
font-size: 32rpx;
|
|
|
font-weight: bold;
|
|
|
}
|
|
|
|
|
|
.info-value.accent {
|
|
|
color: #f59e0b;
|
|
|
font-weight: bold;
|
|
|
}
|
|
|
|
|
|
.info-value.primary {
|
|
|
color: #10b981;
|
|
|
font-weight: bold;
|
|
|
}
|
|
|
|
|
|
.info-value.contact {
|
|
|
color: #667eea;
|
|
|
}
|
|
|
|
|
|
.info-value.duration {
|
|
|
color: #ef4444;
|
|
|
font-weight: bold;
|
|
|
}
|
|
|
|
|
|
/* 状态标签 */
|
|
|
.status-badge {
|
|
|
padding: 8rpx 20rpx;
|
|
|
border-radius: 20rpx;
|
|
|
font-size: 24rpx;
|
|
|
font-weight: 500;
|
|
|
min-width: 120rpx;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.status-waiting {
|
|
|
background: #fffbeb;
|
|
|
color: #d97706;
|
|
|
border: 1rpx solid #fcd34d;
|
|
|
}
|
|
|
|
|
|
.status-processing {
|
|
|
background: #eff6ff;
|
|
|
color: #1d4ed8;
|
|
|
border: 1rpx solid #93c5fd;
|
|
|
}
|
|
|
|
|
|
.status-completed {
|
|
|
background: #f0fdf4;
|
|
|
color: #166534;
|
|
|
border: 1rpx solid #86efac;
|
|
|
}
|
|
|
|
|
|
.status-cancelled {
|
|
|
background: #fef2f2;
|
|
|
color: #dc2626;
|
|
|
border: 1rpx solid #fca5a5;
|
|
|
}
|
|
|
|
|
|
/* 时间网格 */
|
|
|
.time-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
gap: 20rpx;
|
|
|
}
|
|
|
|
|
|
.time-item {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 8rpx;
|
|
|
}
|
|
|
|
|
|
.time-label {
|
|
|
font-size: 24rpx;
|
|
|
color: #64748b;
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.time-value {
|
|
|
font-size: 26rpx;
|
|
|
color: #1e293b;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
/* 评价显示 */
|
|
|
.rating-display {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
padding: 20rpx;
|
|
|
background: #f8fafc;
|
|
|
border-radius: 12rpx;
|
|
|
margin-bottom: 24rpx;
|
|
|
}
|
|
|
|
|
|
.stars-container {
|
|
|
display: flex;
|
|
|
gap: 8rpx;
|
|
|
margin-bottom: 12rpx;
|
|
|
}
|
|
|
|
|
|
.star {
|
|
|
font-size: 36rpx;
|
|
|
color: #e2e8f0;
|
|
|
transition: color 0.3s ease;
|
|
|
}
|
|
|
|
|
|
.star.active {
|
|
|
color: #f59e0b;
|
|
|
}
|
|
|
|
|
|
.rating-text {
|
|
|
font-size: 26rpx;
|
|
|
font-weight: 600;
|
|
|
color: #1e293b;
|
|
|
}
|
|
|
|
|
|
.rating-details {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 16rpx;
|
|
|
}
|
|
|
|
|
|
/* 空状态 */
|
|
|
.empty-card {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
padding: 60rpx 40rpx;
|
|
|
background: #f8fafc;
|
|
|
border-radius: 16rpx;
|
|
|
border: 2rpx dashed #e2e8f0;
|
|
|
margin-top: 24rpx;
|
|
|
}
|
|
|
|
|
|
.empty-icon {
|
|
|
font-size: 64rpx;
|
|
|
margin-bottom: 20rpx;
|
|
|
}
|
|
|
|
|
|
.empty-text {
|
|
|
font-size: 28rpx;
|
|
|
color: #64748b;
|
|
|
font-weight: 600;
|
|
|
margin-bottom: 8rpx;
|
|
|
}
|
|
|
|
|
|
.empty-desc {
|
|
|
font-size: 24rpx;
|
|
|
color: #94a3b8;
|
|
|
}
|
|
|
|
|
|
/* 响应式设计 */
|
|
|
@media (min-width: 768px) {
|
|
|
.ticket-modal-content {
|
|
|
width: 85%;
|
|
|
max-width: 650px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 移动端适配 */
|
|
|
@media (max-width: 768px) {
|
|
|
.ticket-modal-mask {
|
|
|
padding: 20rpx;
|
|
|
}
|
|
|
|
|
|
.ticket-modal-content {
|
|
|
max-height: 80vh;
|
|
|
}
|
|
|
|
|
|
.ticket-modal-body {
|
|
|
max-height: calc(80vh - 120rpx);
|
|
|
}
|
|
|
|
|
|
.info-row {
|
|
|
flex-direction: column;
|
|
|
align-items: flex-start;
|
|
|
gap: 8rpx;
|
|
|
padding: 16rpx 0;
|
|
|
}
|
|
|
|
|
|
.info-value {
|
|
|
text-align: left;
|
|
|
margin-left: 0;
|
|
|
}
|
|
|
|
|
|
.time-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
gap: 16rpx;
|
|
|
}
|
|
|
|
|
|
.status-badge {
|
|
|
align-self: flex-start;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 滚动条优化 */
|
|
|
::-webkit-scrollbar {
|
|
|
width: 4rpx;
|
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar-track {
|
|
|
background: #f1f1f1;
|
|
|
border-radius: 2rpx;
|
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
|
background: #c1c1c1;
|
|
|
border-radius: 2rpx;
|
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
|
background: #a8a8a8;
|
|
|
}
|
|
|
</style> |