You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
662 lines
15 KiB
662 lines
15 KiB
<!--服务列表内容组件-->
|
|
<template>
|
|
<view class="service-list-card">
|
|
<!-- 加载状态 -->
|
|
<view class="loading-state" v-if="loading">
|
|
<view class="loading-icon">🔄</view>
|
|
<view class="loading-text">加载中...</view>
|
|
</view>
|
|
|
|
<!-- 错误状态 -->
|
|
<view class="error-state" v-else-if="error">
|
|
<view class="error-icon">⚠️</view>
|
|
<view class="error-text">{{ error }}</view>
|
|
<view class="retry-button" @click.stop="fetchServiceList">重试</view>
|
|
</view>
|
|
|
|
<!-- 服务列表 -->
|
|
<view class="service-list" v-else-if="actualServiceList && actualServiceList.length > 0">
|
|
<view
|
|
class="service-item"
|
|
v-for="(service, index) in actualServiceList"
|
|
:key="service.id || index"
|
|
@click.stop="viewServiceDetail(service)"
|
|
>
|
|
<!-- 服务头部 -->
|
|
<view class="service-header">
|
|
<view class="service-image" v-if="service.preview_image_url">
|
|
<image
|
|
:src="service.preview_image_url"
|
|
mode="aspectFill"
|
|
class="service-img"
|
|
></image>
|
|
</view>
|
|
<view class="service-image placeholder" v-else>
|
|
<text class="placeholder-text">🛠️</text>
|
|
</view>
|
|
|
|
<view class="service-info">
|
|
<view class="service-name">{{ service.service_name || '未知服务' }}</view>
|
|
<view :class="['service-status',getStatusClass(service.status)]">
|
|
{{ getStatusText(service.status) }}
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 服务描述 -->
|
|
<view class="service-description" v-if="service.description">
|
|
{{ service.description }}
|
|
</view>
|
|
|
|
<!-- 服务记录详情 -->
|
|
<view class="service-logs" v-if="service.logs && service.logs.length > 0">
|
|
<view class="logs-title">服务记录</view>
|
|
<view class="log-list">
|
|
<view
|
|
class="log-item"
|
|
v-for="(log, logIndex) in service.logs"
|
|
:key="log.id || logIndex"
|
|
>
|
|
<view class="log-header">
|
|
<view class="log-time" v-if="log.service_time">
|
|
{{ formatTime(log.service_time) }}
|
|
</view>
|
|
<view :class="['log-status',getLogStatusClass(log.status)]">
|
|
{{ getLogStatusText(log.status) }}
|
|
</view>
|
|
</view>
|
|
|
|
<view class="log-details">
|
|
<view class="log-detail-item">
|
|
<text class="detail-label">服务内容:</text>
|
|
<text class="detail-value">{{ log.service_content }}</text>
|
|
</view>
|
|
|
|
<view class="log-detail-item">
|
|
<text class="detail-label">服务人员:</text>
|
|
<text class="detail-value">{{ log.service_staff }}</text>
|
|
</view>
|
|
|
|
<view class="log-detail-item">
|
|
<text class="detail-label">服务时间:</text>
|
|
<text class="detail-value">{{ log.updated_at }}</text>
|
|
</view>
|
|
|
|
<view class="log-detail-item" v-if="log.customer_feedback">
|
|
<text class="detail-label">客户反馈:</text>
|
|
<text class="detail-value feedback">{{ log.customer_feedback }}</text>
|
|
</view>
|
|
|
|
<view class="log-detail-item" v-if="log.service_rating">
|
|
<text class="detail-label">服务评分:</text>
|
|
<view class="rating-stars">
|
|
<text
|
|
class="star"
|
|
v-for="n in 5"
|
|
:key="n"
|
|
:class="{ 'active': n <= log.service_rating }"
|
|
>★</text>
|
|
<text class="rating-text">({{ log.service_rating }}/5)</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="log-detail-item">
|
|
<text class="detail-label">备注:</text>
|
|
<text class="detail-value remark">{{ log.remark }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 服务统计信息 -->
|
|
<view class="service-stats" v-if="service.total_count || service.completed_count">
|
|
<view class="stat-item">
|
|
<text class="stat-label">总次数:</text>
|
|
<text class="stat-value">{{ service.total_count || 0 }}次</text>
|
|
</view>
|
|
<view class="stat-item">
|
|
<text class="stat-label">已完成:</text>
|
|
<text class="stat-value">{{ service.completed_count || 0 }}次</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 空状态 -->
|
|
<view class="empty-state" v-else>
|
|
<view class="empty-icon">🔧</view>
|
|
<view class="empty-text">暂无服务记录</view>
|
|
<view class="empty-tip">客户还未使用任何服务</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import apiRoute from '@/api/apiRoute.js'
|
|
|
|
export default {
|
|
name: 'ServiceListCard',
|
|
props: {
|
|
// 服务列表数据
|
|
serviceList: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
// 学员ID(可选,用于自动获取数据)
|
|
studentId: {
|
|
type: [String, Number],
|
|
default: null
|
|
}
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
internalServiceList: [],
|
|
loading: false,
|
|
error: null
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
// 实际使用的服务列表数据
|
|
actualServiceList() {
|
|
// 如果传入了serviceList则优先使用
|
|
if (this.serviceList && this.serviceList.length > 0) {
|
|
return this.serviceList
|
|
}
|
|
// 否则使用内部获取的数据
|
|
return this.internalServiceList
|
|
}
|
|
},
|
|
|
|
mounted() {
|
|
// 只有在没有传入serviceList且有studentId时才自动获取数据
|
|
if ((!this.serviceList || this.serviceList.length === 0) && this.studentId) {
|
|
this.fetchServiceList()
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
studentId: {
|
|
handler(newVal) {
|
|
if (newVal && (!this.serviceList || this.serviceList.length === 0)) {
|
|
this.fetchServiceList()
|
|
}
|
|
},
|
|
immediate: true
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
// 获取服务记录列表
|
|
async fetchServiceList() {
|
|
if (!this.studentId) {
|
|
console.error('获取服务记录失败:学员ID不能为空')
|
|
return
|
|
}
|
|
|
|
this.loading = true
|
|
this.error = null
|
|
|
|
try {
|
|
const response = await apiRoute.getStudentServiceList({
|
|
student_id: this.studentId
|
|
})
|
|
|
|
if (response.code === 1) {
|
|
this.internalServiceList = response.data || []
|
|
} else {
|
|
this.error = response.msg || '获取服务记录失败'
|
|
console.error('获取服务记录失败:', response.msg)
|
|
uni.showToast({
|
|
title: this.error,
|
|
icon: 'none',
|
|
duration: 2000
|
|
})
|
|
}
|
|
} catch (error) {
|
|
this.error = '网络请求失败'
|
|
console.error('获取服务记录异常:', error)
|
|
uni.showToast({
|
|
title: '网络请求失败',
|
|
icon: 'none',
|
|
duration: 2000
|
|
})
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
|
|
// 查看服务详情
|
|
viewServiceDetail(service) {
|
|
this.$emit('view-detail', service)
|
|
},
|
|
|
|
// 获取服务状态样式类
|
|
getStatusClass(status) {
|
|
const statusMap = {
|
|
'active': 'status-active',
|
|
'completed': 'status-completed',
|
|
'suspended': 'status-suspended',
|
|
'expired': 'status-expired'
|
|
}
|
|
return statusMap[status] || 'status-default'
|
|
},
|
|
|
|
// 获取服务状态文本
|
|
getStatusText(status) {
|
|
const statusMap = {
|
|
'active': '进行中',
|
|
'completed': '已完成',
|
|
'suspended': '已暂停',
|
|
'expired': '已过期'
|
|
}
|
|
return statusMap[status] || '未知状态'
|
|
},
|
|
|
|
// 获取日志状态样式类
|
|
getLogStatusClass(status) {
|
|
const statusMap = {
|
|
'completed': 'log-completed',
|
|
'cancelled': 'log-cancelled',
|
|
'pending': 'log-pending'
|
|
}
|
|
return statusMap[status] || 'log-default'
|
|
},
|
|
|
|
// 获取日志状态文本
|
|
getLogStatusText(status) {
|
|
const statusMap = {
|
|
'completed': '已完成',
|
|
'cancelled': '已取消',
|
|
'pending': '待处理'
|
|
}
|
|
return statusMap[status] || '未知'
|
|
},
|
|
|
|
// 格式化时间
|
|
formatTime(timeStr) {
|
|
if (!timeStr) return ''
|
|
try {
|
|
const date = new Date(timeStr)
|
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
|
} catch (e) {
|
|
return timeStr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.service-list-card {
|
|
padding: 0;
|
|
max-height: 60vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
// 加载状态样式
|
|
.loading-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 80rpx 40rpx;
|
|
text-align: center;
|
|
}
|
|
|
|
.loading-icon {
|
|
font-size: 80rpx;
|
|
margin-bottom: 24rpx;
|
|
animation: rotate 1s linear infinite;
|
|
}
|
|
|
|
@keyframes rotate {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.loading-text {
|
|
font-size: 28rpx;
|
|
color: #999999;
|
|
}
|
|
|
|
// 错误状态样式
|
|
.error-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 80rpx 40rpx;
|
|
text-align: center;
|
|
}
|
|
|
|
.error-icon {
|
|
font-size: 80rpx;
|
|
margin-bottom: 24rpx;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.error-text {
|
|
font-size: 28rpx;
|
|
color: #F44336;
|
|
margin-bottom: 32rpx;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.retry-button {
|
|
padding: 12rpx 32rpx;
|
|
background: #29D3B4;
|
|
color: #ffffff;
|
|
font-size: 26rpx;
|
|
border-radius: 24rpx;
|
|
transition: all 0.3s ease;
|
|
|
|
&:active {
|
|
background: #24B89E;
|
|
transform: scale(0.98);
|
|
}
|
|
}
|
|
|
|
.service-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 32rpx;
|
|
max-height: 50vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.service-item {
|
|
background: #3A3A3A;
|
|
border-radius: 16rpx;
|
|
padding: 32rpx;
|
|
border: 1px solid #404040;
|
|
transition: all 0.3s ease;
|
|
|
|
&:active {
|
|
background: #4A4A4A;
|
|
}
|
|
}
|
|
|
|
.service-header {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.service-image {
|
|
width: 80rpx;
|
|
height: 80rpx;
|
|
border-radius: 16rpx;
|
|
margin-right: 24rpx;
|
|
overflow: hidden;
|
|
|
|
&.placeholder {
|
|
background: rgba(41, 211, 180, 0.2);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
.service-img {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.placeholder-text {
|
|
font-size: 40rpx;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.service-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.service-name {
|
|
font-size: 32rpx;
|
|
font-weight: 600;
|
|
color: #ffffff;
|
|
margin-bottom: 8rpx;
|
|
}
|
|
|
|
.service-status {
|
|
padding: 6rpx 12rpx;
|
|
border-radius: 16rpx;
|
|
font-size: 22rpx;
|
|
font-weight: 500;
|
|
display: inline-block;
|
|
|
|
&.status-active {
|
|
background: rgba(41, 211, 180, 0.2);
|
|
color: #29D3B4;
|
|
}
|
|
|
|
&.status-completed {
|
|
background: rgba(76, 175, 80, 0.2);
|
|
color: #4CAF50;
|
|
}
|
|
|
|
&.status-suspended {
|
|
background: rgba(255, 193, 7, 0.2);
|
|
color: #FFC107;
|
|
}
|
|
|
|
&.status-expired {
|
|
background: rgba(244, 67, 54, 0.2);
|
|
color: #F44336;
|
|
}
|
|
|
|
&.status-default {
|
|
background: rgba(158, 158, 158, 0.2);
|
|
color: #9E9E9E;
|
|
}
|
|
}
|
|
|
|
.service-description {
|
|
font-size: 26rpx;
|
|
color: #cccccc;
|
|
line-height: 1.5;
|
|
margin-bottom: 24rpx;
|
|
padding: 20rpx;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 12rpx;
|
|
}
|
|
|
|
.service-logs {
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.logs-title {
|
|
font-size: 28rpx;
|
|
font-weight: 600;
|
|
color: #ffffff;
|
|
margin-bottom: 20rpx;
|
|
padding-bottom: 12rpx;
|
|
border-bottom: 1px solid #404040;
|
|
}
|
|
|
|
.log-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.log-item {
|
|
background: rgba(255, 255, 255, 0.03);
|
|
border-radius: 12rpx;
|
|
padding: 24rpx;
|
|
border-left: 3rpx solid #29D3B4;
|
|
}
|
|
|
|
.log-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16rpx;
|
|
}
|
|
|
|
.log-time {
|
|
font-size: 24rpx;
|
|
color: #999999;
|
|
}
|
|
|
|
.log-status {
|
|
padding: 4rpx 10rpx;
|
|
border-radius: 12rpx;
|
|
font-size: 20rpx;
|
|
font-weight: 500;
|
|
|
|
&.log-completed {
|
|
background: rgba(76, 175, 80, 0.2);
|
|
color: #4CAF50;
|
|
}
|
|
|
|
&.log-cancelled {
|
|
background: rgba(244, 67, 54, 0.2);
|
|
color: #F44336;
|
|
}
|
|
|
|
&.log-pending {
|
|
background: rgba(255, 193, 7, 0.2);
|
|
color: #FFC107;
|
|
}
|
|
|
|
&.log-default {
|
|
background: rgba(158, 158, 158, 0.2);
|
|
color: #9E9E9E;
|
|
}
|
|
}
|
|
|
|
.log-details {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12rpx;
|
|
}
|
|
|
|
.log-detail-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.detail-label {
|
|
font-size: 24rpx;
|
|
color: #999999;
|
|
min-width: 120rpx;
|
|
margin-right: 16rpx;
|
|
}
|
|
|
|
.detail-value {
|
|
font-size: 24rpx;
|
|
color: #ffffff;
|
|
flex: 1;
|
|
|
|
&.feedback,
|
|
&.remark {
|
|
line-height: 1.5;
|
|
}
|
|
}
|
|
|
|
.rating-stars {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4rpx;
|
|
}
|
|
|
|
.star {
|
|
font-size: 28rpx;
|
|
color: #404040;
|
|
|
|
&.active {
|
|
color: #FFC107;
|
|
}
|
|
}
|
|
|
|
.rating-text {
|
|
font-size: 24rpx;
|
|
color: #999999;
|
|
margin-left: 12rpx;
|
|
}
|
|
|
|
.service-stats {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 32rpx;
|
|
padding: 20rpx;
|
|
background: rgba(41, 211, 180, 0.05);
|
|
border-radius: 12rpx;
|
|
border-left: 4rpx solid #29D3B4;
|
|
}
|
|
|
|
.stat-item {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 24rpx;
|
|
color: #999999;
|
|
margin-right: 8rpx;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 24rpx;
|
|
color: #ffffff;
|
|
font-weight: 600;
|
|
|
|
&.highlight {
|
|
color: #29D3B4;
|
|
}
|
|
}
|
|
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 80rpx 40rpx;
|
|
text-align: center;
|
|
}
|
|
|
|
.empty-icon {
|
|
font-size: 120rpx;
|
|
margin-bottom: 32rpx;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.empty-text {
|
|
font-size: 32rpx;
|
|
color: #ffffff;
|
|
margin-bottom: 16rpx;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.empty-tip {
|
|
font-size: 26rpx;
|
|
color: #999999;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* 滚动条样式 */
|
|
.service-list-card::-webkit-scrollbar,
|
|
.service-list::-webkit-scrollbar {
|
|
width: 6rpx;
|
|
}
|
|
|
|
.service-list-card::-webkit-scrollbar-track,
|
|
.service-list::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.service-list-card::-webkit-scrollbar-thumb,
|
|
.service-list::-webkit-scrollbar-thumb {
|
|
background: #29D3B4;
|
|
border-radius: 3rpx;
|
|
}
|
|
|
|
.service-list-card::-webkit-scrollbar-thumb:hover,
|
|
.service-list::-webkit-scrollbar-thumb:hover {
|
|
background: #24B89E;
|
|
}
|
|
</style>
|