Initial commit of akmon project
This commit is contained in:
1001
pages/mall/service/index.uvue
Normal file
1001
pages/mall/service/index.uvue
Normal file
File diff suppressed because it is too large
Load Diff
997
pages/mall/service/profile.uvue
Normal file
997
pages/mall/service/profile.uvue
Normal file
@@ -0,0 +1,997 @@
|
||||
<!-- 客服端 - 个人中心 -->
|
||||
<template>
|
||||
<view class="service-profile">
|
||||
<!-- 客服信息头部 -->
|
||||
<view class="profile-header">
|
||||
<image :src="serviceInfo.avatar_url || '/static/default-avatar.png'" class="service-avatar" @click="editProfile" />
|
||||
<view class="service-info">
|
||||
<text class="service-name">{{ serviceInfo.nickname || serviceInfo.phone }}</text>
|
||||
<text class="service-status">{{ getServiceStatus() }}</text>
|
||||
<view class="service-stats">
|
||||
<text class="stat-item">评分: {{ serviceRating }}/5.0</text>
|
||||
<text class="stat-item">工号: {{ serviceInfo.work_id }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="settings-icon" @click="goToSettings">⚙️</view>
|
||||
</view>
|
||||
|
||||
<!-- 在线状态控制 -->
|
||||
<view class="online-status">
|
||||
<view class="section-title">服务状态</view>
|
||||
<view class="status-controls">
|
||||
<view class="status-toggle" @click="toggleOnlineStatus">
|
||||
<text class="toggle-label">{{ onlineStatus === 1 ? '在线服务' : '离线状态' }}</text>
|
||||
<view class="toggle-switch" :class="{ active: onlineStatus === 1 }">
|
||||
<view class="toggle-handle"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="onlineStatus === 1" class="queue-info">
|
||||
<text class="queue-text">当前排队: {{ queueCount }}人</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 工单处理快捷入口 -->
|
||||
<view class="ticket-shortcuts">
|
||||
<view class="section-title">工单处理</view>
|
||||
<view class="ticket-tabs">
|
||||
<view class="ticket-tab" @click="goToTickets('all')">
|
||||
<text class="tab-icon">📋</text>
|
||||
<text class="tab-text">全部工单</text>
|
||||
<text v-if="ticketCounts.total > 0" class="tab-badge">{{ ticketCounts.total }}</text>
|
||||
</view>
|
||||
<view class="ticket-tab" @click="goToTickets('pending')">
|
||||
<text class="tab-icon">⏳</text>
|
||||
<text class="tab-text">待处理</text>
|
||||
<text v-if="ticketCounts.pending > 0" class="tab-badge alert">{{ ticketCounts.pending }}</text>
|
||||
</view>
|
||||
<view class="ticket-tab" @click="goToTickets('processing')">
|
||||
<text class="tab-icon">🔄</text>
|
||||
<text class="tab-text">处理中</text>
|
||||
<text v-if="ticketCounts.processing > 0" class="tab-badge">{{ ticketCounts.processing }}</text>
|
||||
</view>
|
||||
<view class="ticket-tab" @click="goToTickets('completed')">
|
||||
<text class="tab-icon">✅</text>
|
||||
<text class="tab-text">已完成</text>
|
||||
<text v-if="ticketCounts.completed > 0" class="tab-badge">{{ ticketCounts.completed }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日服务数据 -->
|
||||
<view class="today-stats">
|
||||
<view class="section-title">今日服务</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ todayStats.tickets }}</text>
|
||||
<text class="stat-label">处理工单</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ todayStats.satisfaction }}%</text>
|
||||
<text class="stat-label">满意度</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ todayStats.avgTime }}min</text>
|
||||
<text class="stat-label">平均响应</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ todayStats.onlineTime }}h</text>
|
||||
<text class="stat-label">在线时长</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 当前处理工单 -->
|
||||
<view v-if="currentTicket" class="current-ticket">
|
||||
<view class="section-title">当前处理</view>
|
||||
<view class="ticket-card">
|
||||
<view class="ticket-header">
|
||||
<text class="ticket-id">工单 #{{ currentTicket.id.slice(-6) }}</text>
|
||||
<text class="ticket-priority" :class="'priority-' + currentTicket.priority">
|
||||
{{ getPriorityText(currentTicket.priority) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="ticket-content">
|
||||
<text class="ticket-title">{{ currentTicket.title }}</text>
|
||||
<text class="ticket-desc">{{ currentTicket.description }}</text>
|
||||
</view>
|
||||
<view class="ticket-user">
|
||||
<text class="user-info">客户: {{ currentTicket.user_name }}</text>
|
||||
<text class="created-time">{{ formatTime(currentTicket.created_at) }}</text>
|
||||
</view>
|
||||
<view class="ticket-actions">
|
||||
<button class="action-btn" @click="contactUser">联系客户</button>
|
||||
<button class="action-btn primary" @click="viewTicketDetail">查看详情</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近处理工单 -->
|
||||
<view class="recent-tickets">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最近处理</text>
|
||||
<text class="view-all" @click="goToTickets('all')">查看全部 ></text>
|
||||
</view>
|
||||
<view v-if="recentTickets.length > 0" class="ticket-list">
|
||||
<view v-for="ticket in recentTickets" :key="ticket.id" class="ticket-item" @click="viewTicketDetail(ticket.id)">
|
||||
<view class="ticket-info">
|
||||
<text class="ticket-title">{{ ticket.title }}</text>
|
||||
<text class="ticket-status" :class="'status-' + ticket.status">{{ getTicketStatusText(ticket.status) }}</text>
|
||||
</view>
|
||||
<view class="ticket-meta">
|
||||
<text class="meta-text">{{ ticket.user_name }}</text>
|
||||
<text class="meta-time">{{ formatTime(ticket.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="no-data">
|
||||
<text class="no-data-text">暂无最近工单</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 服务评价统计 -->
|
||||
<view class="rating-stats">
|
||||
<view class="section-header">
|
||||
<text class="section-title">评价统计</text>
|
||||
<text class="view-more" @click="goToRatings">详细评价 ></text>
|
||||
</view>
|
||||
<view class="rating-chart">
|
||||
<view class="rating-overview">
|
||||
<text class="overall-rating">{{ serviceRating }}</text>
|
||||
<text class="rating-label">综合评分</text>
|
||||
<view class="star-rating">
|
||||
<text v-for="i in 5" :key="i" class="star" :class="{ filled: i <= Math.floor(serviceRating) }">★</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="rating-breakdown">
|
||||
<view v-for="item in ratingBreakdown" :key="item.score" class="rating-row">
|
||||
<text class="score-label">{{ item.score }}星</text>
|
||||
<view class="score-bar">
|
||||
<view class="score-fill" :style="{ width: item.percentage + '%' }"></view>
|
||||
</view>
|
||||
<text class="score-count">{{ item.count }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 知识库快捷访问 -->
|
||||
<view class="knowledge-base">
|
||||
<view class="section-title">知识库</view>
|
||||
<view class="kb-grid">
|
||||
<view class="kb-item" @click="goToKnowledge('common')">
|
||||
<text class="kb-icon">📖</text>
|
||||
<text class="kb-label">常见问题</text>
|
||||
</view>
|
||||
<view class="kb-item" @click="goToKnowledge('order')">
|
||||
<text class="kb-icon">📋</text>
|
||||
<text class="kb-label">订单问题</text>
|
||||
</view>
|
||||
<view class="kb-item" @click="goToKnowledge('payment')">
|
||||
<text class="kb-icon">💳</text>
|
||||
<text class="kb-label">支付问题</text>
|
||||
</view>
|
||||
<view class="kb-item" @click="goToKnowledge('delivery')">
|
||||
<text class="kb-icon">🚚</text>
|
||||
<text class="kb-label">配送问题</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能菜单 -->
|
||||
<view class="function-menu">
|
||||
<view class="menu-group">
|
||||
<view class="menu-item" @click="goToRatings">
|
||||
<text class="menu-icon">⭐</text>
|
||||
<text class="menu-label">评价管理</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goToTraining">
|
||||
<text class="menu-icon">📚</text>
|
||||
<text class="menu-label">培训资料</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goToSchedule">
|
||||
<text class="menu-icon">📅</text>
|
||||
<text class="menu-label">排班管理</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-group">
|
||||
<view class="menu-item" @click="goToHelp">
|
||||
<text class="menu-icon">❓</text>
|
||||
<text class="menu-label">帮助中心</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goToFeedback">
|
||||
<text class="menu-icon">💬</text>
|
||||
<text class="menu-label">意见反馈</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import type { UserType, ApiResponseType } from '@/types/mall-types'
|
||||
|
||||
// 工单类型定义
|
||||
type TicketType = {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
user_name: string
|
||||
priority: number
|
||||
status: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const serviceInfo = ref({
|
||||
id: '',
|
||||
phone: '',
|
||||
nickname: '客服专员',
|
||||
avatar_url: '',
|
||||
work_id: 'CS001'
|
||||
} as UserType & { work_id: string })
|
||||
|
||||
const onlineStatus = ref(1) // 1: 在线, 0: 离线
|
||||
const queueCount = ref(3)
|
||||
const serviceRating = ref(4.7)
|
||||
|
||||
const ticketCounts = ref({
|
||||
total: 0,
|
||||
pending: 0,
|
||||
processing: 0,
|
||||
completed: 0
|
||||
})
|
||||
|
||||
const todayStats = ref({
|
||||
tickets: 28,
|
||||
satisfaction: 96.5,
|
||||
avgTime: 8.5,
|
||||
onlineTime: 7.5
|
||||
})
|
||||
|
||||
const currentTicket = ref({
|
||||
id: 'ticket001',
|
||||
title: '订单配送问题咨询',
|
||||
description: '我的订单已经发货3天了,但是物流信息一直没有更新,请帮我查看一下具体情况。',
|
||||
user_name: '张先生',
|
||||
priority: 2,
|
||||
status: 2,
|
||||
created_at: '2024-12-01 14:30:00'
|
||||
} as TicketType)
|
||||
|
||||
const recentTickets = ref([] as Array<TicketType>)
|
||||
|
||||
const ratingBreakdown = ref([
|
||||
{ score: 5, percentage: 68, count: 156 },
|
||||
{ score: 4, percentage: 22, count: 51 },
|
||||
{ score: 3, percentage: 8, count: 18 },
|
||||
{ score: 2, percentage: 2, count: 4 },
|
||||
{ score: 1, percentage: 0, count: 0 }
|
||||
])
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadServiceInfo()
|
||||
loadTicketCounts()
|
||||
loadRecentTickets()
|
||||
})
|
||||
|
||||
// 方法
|
||||
function loadServiceInfo() {
|
||||
// 模拟加载客服信息
|
||||
serviceInfo.value = {
|
||||
id: 'service001',
|
||||
phone: '13666666666',
|
||||
email: 'service@mall.com',
|
||||
nickname: '小王客服',
|
||||
avatar_url: '/static/service-avatar.png',
|
||||
gender: 1,
|
||||
user_type: 2,
|
||||
status: 1,
|
||||
created_at: '2024-01-01',
|
||||
work_id: 'CS001'
|
||||
}
|
||||
}
|
||||
|
||||
function loadTicketCounts() {
|
||||
// 模拟加载工单统计
|
||||
ticketCounts.value = {
|
||||
total: 45,
|
||||
pending: 8,
|
||||
processing: 3,
|
||||
completed: 34
|
||||
}
|
||||
}
|
||||
|
||||
function loadRecentTickets() {
|
||||
// 模拟加载最近工单
|
||||
recentTickets.value = [
|
||||
{
|
||||
id: 'ticket002',
|
||||
title: '退款申请处理',
|
||||
description: '申请退款',
|
||||
user_name: '李女士',
|
||||
priority: 1,
|
||||
status: 3,
|
||||
created_at: '2024-12-01 13:00:00'
|
||||
},
|
||||
{
|
||||
id: 'ticket003',
|
||||
title: '商品质量问题',
|
||||
description: '商品质量投诉',
|
||||
user_name: '王先生',
|
||||
priority: 3,
|
||||
status: 3,
|
||||
created_at: '2024-12-01 11:30:00'
|
||||
},
|
||||
{
|
||||
id: 'ticket004',
|
||||
title: '账户密码重置',
|
||||
description: '忘记密码需要重置',
|
||||
user_name: '赵女士',
|
||||
priority: 1,
|
||||
status: 3,
|
||||
created_at: '2024-12-01 10:15:00'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function getServiceStatus(): string {
|
||||
return onlineStatus.value === 1 ? '在线服务中' : '离线状态'
|
||||
}
|
||||
|
||||
function getPriorityText(priority: number): string {
|
||||
const priorityMap = {
|
||||
1: '低',
|
||||
2: '中',
|
||||
3: '高',
|
||||
4: '紧急'
|
||||
}
|
||||
return priorityMap[priority] || '普通'
|
||||
}
|
||||
|
||||
function getTicketStatusText(status: number): string {
|
||||
const statusMap = {
|
||||
1: '待处理',
|
||||
2: '处理中',
|
||||
3: '已完成',
|
||||
4: '已关闭'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
function formatTime(dateStr: string): string {
|
||||
const date = new Date(dateStr)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60))
|
||||
|
||||
if (hours < 1) {
|
||||
return '刚刚'
|
||||
} else if (hours < 24) {
|
||||
return `${hours}小时前`
|
||||
} else {
|
||||
return `${Math.floor(hours / 24)}天前`
|
||||
}
|
||||
}
|
||||
|
||||
// 交互方法
|
||||
function toggleOnlineStatus() {
|
||||
onlineStatus.value = onlineStatus.value === 1 ? 0 : 1
|
||||
|
||||
uni.showToast({
|
||||
title: onlineStatus.value === 1 ? '已上线服务' : '已下线',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
function contactUser() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['发送消息', '拨打电话'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
// 打开聊天界面
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/service/chat'
|
||||
})
|
||||
} else if (res.tapIndex === 1) {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: '13888888888'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function viewTicketDetail(ticketId: string = '') {
|
||||
const id = ticketId || currentTicket.value?.id || ''
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/service/ticket-detail?id=${id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 导航方法
|
||||
function editProfile() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/service/profile-edit'
|
||||
})
|
||||
}
|
||||
|
||||
function goToSettings() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/service/settings'
|
||||
})
|
||||
}
|
||||
|
||||
function goToTickets(type: string) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/service/tickets?type=${type}`
|
||||
})
|
||||
}
|
||||
|
||||
function goToRatings() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/service/ratings'
|
||||
})
|
||||
}
|
||||
|
||||
function goToKnowledge(category: string) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/service/knowledge?category=${category}`
|
||||
})
|
||||
}
|
||||
|
||||
function goToTraining() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/service/training'
|
||||
})
|
||||
}
|
||||
|
||||
function goToSchedule() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/service/schedule'
|
||||
})
|
||||
}
|
||||
|
||||
function goToHelp() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/common/help'
|
||||
})
|
||||
}
|
||||
|
||||
function goToFeedback() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/common/feedback'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.service-profile {
|
||||
padding: 0 0 120rpx 0;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 40rpx 30rpx;
|
||||
background: linear-gradient(135deg, #00cec9 0%, #00b894 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.service-avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 60rpx;
|
||||
margin-right: 30rpx;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.service-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.service-status {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.service-stats {
|
||||
display: flex;
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
font-size: 36rpx;
|
||||
color: white;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.online-status, .ticket-shortcuts, .today-stats, .current-ticket, .recent-tickets, .rating-stats, .knowledge-base, .function-menu {
|
||||
margin: 20rpx 30rpx;
|
||||
background: white;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.view-all, .view-more {
|
||||
font-size: 24rpx;
|
||||
color: #00cec9;
|
||||
}
|
||||
|
||||
.status-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.status-toggle {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
width: 100rpx;
|
||||
height: 50rpx;
|
||||
background: #ddd;
|
||||
border-radius: 25rpx;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.toggle-switch.active {
|
||||
background: #00cec9;
|
||||
}
|
||||
|
||||
.toggle-handle {
|
||||
position: absolute;
|
||||
top: 5rpx;
|
||||
left: 5rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.toggle-switch.active .toggle-handle {
|
||||
left: 55rpx;
|
||||
}
|
||||
|
||||
.queue-info {
|
||||
padding: 15rpx 20rpx;
|
||||
background: #e8f8f5;
|
||||
border-radius: 15rpx;
|
||||
}
|
||||
|
||||
.queue-text {
|
||||
font-size: 24rpx;
|
||||
color: #00b894;
|
||||
}
|
||||
|
||||
.ticket-tabs {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.ticket-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 48rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tab-badge {
|
||||
position: absolute;
|
||||
top: -10rpx;
|
||||
right: 20rpx;
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 10rpx;
|
||||
min-width: 30rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tab-badge.alert {
|
||||
background: #ff4757;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 150rpx;
|
||||
padding: 20rpx;
|
||||
background: #e8f8f5;
|
||||
border-radius: 15rpx;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #00cec9;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.ticket-card {
|
||||
padding: 25rpx;
|
||||
background: #e8f8f5;
|
||||
border-radius: 15rpx;
|
||||
border-left: 6rpx solid #00cec9;
|
||||
}
|
||||
|
||||
.ticket-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.ticket-id {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ticket-priority {
|
||||
font-size: 22rpx;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #fed330;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.priority-1 {
|
||||
background: #2ed573;
|
||||
}
|
||||
|
||||
.priority-2 {
|
||||
background: #fed330;
|
||||
}
|
||||
|
||||
.priority-3 {
|
||||
background: #ff6348;
|
||||
}
|
||||
|
||||
.priority-4 {
|
||||
background: #ff3838;
|
||||
}
|
||||
|
||||
.ticket-content {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.ticket-title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.ticket-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ticket-user {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.created-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.ticket-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
border-radius: 15rpx;
|
||||
font-size: 26rpx;
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #00cec9;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ticket-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.ticket-item {
|
||||
padding: 25rpx;
|
||||
background: #f8f9ff;
|
||||
border-radius: 15rpx;
|
||||
border-left: 6rpx solid #00cec9;
|
||||
}
|
||||
|
||||
.ticket-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.ticket-title {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ticket-status {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 15rpx;
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.status-3 {
|
||||
background: #e8f5e8;
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
.ticket-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.meta-time {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.rating-chart {
|
||||
display: flex;
|
||||
gap: 40rpx;
|
||||
}
|
||||
|
||||
.rating-overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.overall-rating {
|
||||
font-size: 72rpx;
|
||||
font-weight: bold;
|
||||
color: #00cec9;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.rating-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.star-rating {
|
||||
display: flex;
|
||||
gap: 5rpx;
|
||||
}
|
||||
|
||||
.star {
|
||||
font-size: 32rpx;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.star.filled {
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.rating-breakdown {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.rating-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.score-label {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
width: 60rpx;
|
||||
}
|
||||
|
||||
.score-bar {
|
||||
flex: 1;
|
||||
height: 20rpx;
|
||||
background: #f0f0f0;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.score-fill {
|
||||
height: 100%;
|
||||
background: #00cec9;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.score-count {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
width: 60rpx;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.kb-grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.kb-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 140rpx;
|
||||
padding: 25rpx 15rpx;
|
||||
background: #e8f8f5;
|
||||
border-radius: 15rpx;
|
||||
}
|
||||
|
||||
.kb-icon {
|
||||
font-size: 48rpx;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.kb-label {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.menu-group {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.menu-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 25rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.menu-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 36rpx;
|
||||
width: 60rpx;
|
||||
margin-right: 25rpx;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.no-data-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
919
pages/mall/service/ticket-detail.uvue
Normal file
919
pages/mall/service/ticket-detail.uvue
Normal file
@@ -0,0 +1,919 @@
|
||||
<!-- 客服端 - 工单详情页 -->
|
||||
<template>
|
||||
<view class="ticket-detail-page">
|
||||
<!-- 工单状态 -->
|
||||
<view class="ticket-status">
|
||||
<view class="status-header">
|
||||
<text class="ticket-id">工单 #{{ ticket.id }}</text>
|
||||
<view class="status-badge" :class="getStatusClass()">{{ getStatusText() }}</view>
|
||||
</view>
|
||||
<text class="ticket-title">{{ ticket.title }}</text>
|
||||
<view class="ticket-meta">
|
||||
<text class="meta-item">{{ getCategoryText() }}</text>
|
||||
<text class="meta-item">{{ getPriorityText() }}</text>
|
||||
<text class="meta-item">{{ formatTime(ticket.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<view class="user-info">
|
||||
<view class="section-title">用户信息</view>
|
||||
<view class="user-card">
|
||||
<image :src="user.avatar_url || '/static/default-avatar.png'" class="user-avatar" />
|
||||
<view class="user-details">
|
||||
<text class="user-name">{{ user.nickname || user.phone }}</text>
|
||||
<text class="user-contact">{{ user.phone }}</text>
|
||||
<text class="user-email">{{ user.email || '未设置邮箱' }}</text>
|
||||
</view>
|
||||
<view class="user-actions">
|
||||
<button class="action-btn call" @click="callUser">📞</button>
|
||||
<button class="action-btn message" @click="sendMessage">💬</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 工单内容 -->
|
||||
<view class="ticket-content">
|
||||
<view class="section-title">问题描述</view>
|
||||
<text class="content-text">{{ ticket.description }}</text>
|
||||
|
||||
<view v-if="ticket.attachments && ticket.attachments.length > 0" class="attachments">
|
||||
<text class="attachment-title">相关附件</text>
|
||||
<view class="attachment-list">
|
||||
<view v-for="(attachment, index) in ticket.attachments" :key="index"
|
||||
class="attachment-item" @click="previewAttachment(attachment)">
|
||||
<text class="attachment-icon">{{ getAttachmentIcon(attachment) }}</text>
|
||||
<text class="attachment-name">{{ getAttachmentName(attachment) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 处理记录 -->
|
||||
<view class="ticket-logs">
|
||||
<view class="section-title">处理记录</view>
|
||||
<view v-if="ticketLogs.length === 0" class="empty-logs">
|
||||
<text class="empty-text">暂无处理记录</text>
|
||||
</view>
|
||||
|
||||
<view v-for="log in ticketLogs" :key="log.id" class="log-item">
|
||||
<view class="log-header">
|
||||
<text class="log-operator">{{ log.operator_name }}</text>
|
||||
<text class="log-time">{{ formatTime(log.created_at) }}</text>
|
||||
</view>
|
||||
<text class="log-action">{{ getLogActionText(log.action) }}</text>
|
||||
<text v-if="log.content" class="log-content">{{ log.content }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 相关订单 -->
|
||||
<view v-if="relatedOrder" class="related-order">
|
||||
<view class="section-title">相关订单</view>
|
||||
<view class="order-card" @click="viewOrderDetail">
|
||||
<view class="order-header">
|
||||
<text class="order-no">{{ relatedOrder.order_no }}</text>
|
||||
<text class="order-status">{{ getOrderStatusText(relatedOrder.status) }}</text>
|
||||
</view>
|
||||
<view class="order-info">
|
||||
<text class="order-amount">¥{{ relatedOrder.actual_amount }}</text>
|
||||
<text class="order-time">{{ formatTime(relatedOrder.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 解决方案建议 -->
|
||||
<view class="solution-suggestions">
|
||||
<view class="section-title">解决方案建议</view>
|
||||
<view v-for="suggestion in suggestions" :key="suggestion.id"
|
||||
class="suggestion-item" @click="applySuggestion(suggestion)">
|
||||
<text class="suggestion-title">{{ suggestion.title }}</text>
|
||||
<text class="suggestion-desc">{{ suggestion.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 处理操作 -->
|
||||
<view class="ticket-actions">
|
||||
<view class="section-title">处理操作</view>
|
||||
|
||||
<view class="quick-replies">
|
||||
<text class="quick-title">快速回复</text>
|
||||
<view class="reply-tags">
|
||||
<text v-for="reply in quickReplies" :key="reply"
|
||||
class="reply-tag" @click="selectQuickReply(reply)">{{ reply }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="response-form">
|
||||
<textarea v-model="responseContent"
|
||||
class="response-input"
|
||||
placeholder="请输入处理内容或回复信息..."
|
||||
:maxlength="500"></textarea>
|
||||
|
||||
<view class="form-actions">
|
||||
<button class="attach-btn" @click="addAttachment">📎 添加附件</button>
|
||||
<button class="template-btn" @click="useTemplate">📝 使用模板</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-actions">
|
||||
<button v-if="ticket.status === 'open'" class="action-btn primary" @click="processTicket">处理工单</button>
|
||||
<button v-if="ticket.status === 'processing'" class="action-btn success" @click="resolveTicket">解决工单</button>
|
||||
<button v-if="ticket.status === 'processing'" class="action-btn warning" @click="escalateTicket">升级工单</button>
|
||||
<button class="action-btn secondary" @click="addNote">添加备注</button>
|
||||
<button v-if="ticket.status !== 'closed'" class="action-btn danger" @click="closeTicket">关闭工单</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { UserType, OrderType } from '@/types/mall-types.uts'
|
||||
|
||||
type TicketType = {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
category: string
|
||||
priority: string
|
||||
status: string
|
||||
user_id: string
|
||||
assigned_to: string
|
||||
attachments: Array<string>
|
||||
created_at: string
|
||||
}
|
||||
|
||||
type TicketLogType = {
|
||||
id: string
|
||||
ticket_id: string
|
||||
action: string
|
||||
content: string
|
||||
operator_name: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
type SuggestionType = {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
template: string
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
ticket: {
|
||||
id: '',
|
||||
title: '',
|
||||
description: '',
|
||||
category: '',
|
||||
priority: '',
|
||||
status: '',
|
||||
user_id: '',
|
||||
assigned_to: '',
|
||||
attachments: [],
|
||||
created_at: ''
|
||||
} as TicketType,
|
||||
user: {
|
||||
id: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
nickname: '',
|
||||
avatar_url: '',
|
||||
gender: 0,
|
||||
user_type: 0,
|
||||
status: 0,
|
||||
created_at: ''
|
||||
} as UserType,
|
||||
ticketLogs: [] as Array<TicketLogType>,
|
||||
relatedOrder: null as OrderType | null,
|
||||
suggestions: [] as Array<SuggestionType>,
|
||||
quickReplies: [] as Array<string>,
|
||||
responseContent: ''
|
||||
}
|
||||
},
|
||||
onLoad(options: any) {
|
||||
const ticketId = options.ticketId as string
|
||||
if (ticketId) {
|
||||
this.loadTicketDetail(ticketId)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadTicketDetail(ticketId: string) {
|
||||
// 模拟加载工单详情数据
|
||||
this.ticket = {
|
||||
id: ticketId,
|
||||
title: '订单支付问题',
|
||||
description: '我在支付订单时遇到了问题,支付页面一直加载不出来,尝试了多次都无法完成支付。订单号是ORD202401150001,希望能尽快解决。',
|
||||
category: 'payment',
|
||||
priority: 'high',
|
||||
status: 'processing',
|
||||
user_id: 'user_001',
|
||||
assigned_to: 'service_001',
|
||||
attachments: ['/static/screenshot1.jpg', '/static/log.txt'],
|
||||
created_at: '2024-01-15T10:30:00'
|
||||
}
|
||||
|
||||
this.user = {
|
||||
id: 'user_001',
|
||||
phone: '13800138000',
|
||||
email: 'user@example.com',
|
||||
nickname: '张三',
|
||||
avatar_url: '/static/avatar1.jpg',
|
||||
gender: 1,
|
||||
user_type: 1,
|
||||
status: 1,
|
||||
created_at: '2023-06-15T10:30:00'
|
||||
}
|
||||
|
||||
this.ticketLogs = [
|
||||
{
|
||||
id: 'log_001',
|
||||
ticket_id: ticketId,
|
||||
action: 'created',
|
||||
content: '工单已创建',
|
||||
operator_name: '系统',
|
||||
created_at: '2024-01-15T10:30:00'
|
||||
},
|
||||
{
|
||||
id: 'log_002',
|
||||
ticket_id: ticketId,
|
||||
action: 'assigned',
|
||||
content: '工单已分配给客服小王',
|
||||
operator_name: '系统',
|
||||
created_at: '2024-01-15T10:35:00'
|
||||
},
|
||||
{
|
||||
id: 'log_003',
|
||||
ticket_id: ticketId,
|
||||
action: 'responded',
|
||||
content: '已联系用户确认问题详情,正在排查支付系统。',
|
||||
operator_name: '客服小王',
|
||||
created_at: '2024-01-15T11:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
if (this.ticket.category === 'payment') {
|
||||
this.relatedOrder = {
|
||||
id: 'order_001',
|
||||
order_no: 'ORD202401150001',
|
||||
user_id: 'user_001',
|
||||
merchant_id: 'merchant_001',
|
||||
status: 1,
|
||||
total_amount: 299.98,
|
||||
discount_amount: 30.00,
|
||||
delivery_fee: 8.00,
|
||||
actual_amount: 277.98,
|
||||
payment_method: 1,
|
||||
payment_status: 0,
|
||||
delivery_address: {},
|
||||
created_at: '2024-01-15T10:25:00'
|
||||
}
|
||||
}
|
||||
|
||||
this.suggestions = [
|
||||
{
|
||||
id: 'sug_001',
|
||||
title: '检查支付方式',
|
||||
description: '建议用户更换支付方式或检查账户余额',
|
||||
template: '您好,建议您尝试更换支付方式,或检查您的账户余额是否充足。如果问题仍然存在,请联系我们的技术支持。'
|
||||
},
|
||||
{
|
||||
id: 'sug_002',
|
||||
title: '清除缓存',
|
||||
description: '指导用户清除浏览器缓存后重试',
|
||||
template: '您好,请尝试清除浏览器缓存后重新进行支付。具体操作:设置 > 清除浏览数据 > 清除缓存和Cookie。'
|
||||
}
|
||||
]
|
||||
|
||||
this.quickReplies = [
|
||||
'感谢您的反馈,我们正在处理',
|
||||
'问题已确认,预计30分钟内解决',
|
||||
'请提供更多详细信息',
|
||||
'问题已解决,请您确认',
|
||||
'如有其他问题,请随时联系我们'
|
||||
]
|
||||
},
|
||||
|
||||
getStatusClass(): string {
|
||||
const classes: Record<string, string> = {
|
||||
open: 'status-open',
|
||||
processing: 'status-processing',
|
||||
resolved: 'status-resolved',
|
||||
closed: 'status-closed'
|
||||
}
|
||||
return classes[this.ticket.status] || 'status-open'
|
||||
},
|
||||
|
||||
getStatusText(): string {
|
||||
const statusTexts: Record<string, string> = {
|
||||
open: '待处理',
|
||||
processing: '处理中',
|
||||
resolved: '已解决',
|
||||
closed: '已关闭'
|
||||
}
|
||||
return statusTexts[this.ticket.status] || '未知'
|
||||
},
|
||||
|
||||
getCategoryText(): string {
|
||||
const categories: Record<string, string> = {
|
||||
payment: '支付问题',
|
||||
order: '订单问题',
|
||||
delivery: '配送问题',
|
||||
product: '商品问题',
|
||||
account: '账户问题',
|
||||
other: '其他问题'
|
||||
}
|
||||
return categories[this.ticket.category] || '其他'
|
||||
},
|
||||
|
||||
getPriorityText(): string {
|
||||
const priorities: Record<string, string> = {
|
||||
low: '低优先级',
|
||||
medium: '中优先级',
|
||||
high: '高优先级',
|
||||
urgent: '紧急'
|
||||
}
|
||||
return priorities[this.ticket.priority] || '普通'
|
||||
},
|
||||
|
||||
getOrderStatusText(status: number): string {
|
||||
const statusTexts = ['异常', '待支付', '待发货', '待收货', '已完成', '已取消']
|
||||
return statusTexts[status] || '未知'
|
||||
},
|
||||
|
||||
getLogActionText(action: string): string {
|
||||
const actions: Record<string, string> = {
|
||||
created: '创建工单',
|
||||
assigned: '分配工单',
|
||||
responded: '回复用户',
|
||||
escalated: '升级工单',
|
||||
resolved: '解决工单',
|
||||
closed: '关闭工单'
|
||||
}
|
||||
return actions[action] || action
|
||||
},
|
||||
|
||||
getAttachmentIcon(attachment: string): string {
|
||||
const ext = attachment.split('.').pop()?.toLowerCase()
|
||||
const icons: Record<string, string> = {
|
||||
jpg: '🖼️',
|
||||
jpeg: '🖼️',
|
||||
png: '🖼️',
|
||||
gif: '🖼️',
|
||||
pdf: '📄',
|
||||
doc: '📝',
|
||||
docx: '📝',
|
||||
txt: '📄',
|
||||
zip: '📦'
|
||||
}
|
||||
return icons[ext || ''] || '📎'
|
||||
},
|
||||
|
||||
getAttachmentName(attachment: string): string {
|
||||
return attachment.split('/').pop() || '未知文件'
|
||||
},
|
||||
|
||||
formatTime(timeStr: string): string {
|
||||
return timeStr.replace('T', ' ').split('.')[0]
|
||||
},
|
||||
|
||||
callUser() {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: this.user.phone
|
||||
})
|
||||
},
|
||||
|
||||
sendMessage() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/service/chat?userId=${this.user.id}&ticketId=${this.ticket.id}`
|
||||
})
|
||||
},
|
||||
|
||||
previewAttachment(attachment: string) {
|
||||
uni.previewImage({
|
||||
urls: [attachment],
|
||||
current: attachment
|
||||
})
|
||||
},
|
||||
|
||||
viewOrderDetail() {
|
||||
if (this.relatedOrder) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/service/order-detail?orderId=${this.relatedOrder.id}`
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
applySuggestion(suggestion: SuggestionType) {
|
||||
this.responseContent = suggestion.template
|
||||
},
|
||||
|
||||
selectQuickReply(reply: string) {
|
||||
this.responseContent = reply
|
||||
},
|
||||
|
||||
addAttachment() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
uni.showToast({
|
||||
title: '附件添加成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
useTemplate() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/service/response-templates'
|
||||
})
|
||||
},
|
||||
|
||||
processTicket() {
|
||||
if (!this.responseContent.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入处理内容',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.ticket.status = 'processing'
|
||||
this.ticketLogs.push({
|
||||
id: 'log_' + Date.now(),
|
||||
ticket_id: this.ticket.id,
|
||||
action: 'responded',
|
||||
content: this.responseContent,
|
||||
operator_name: '当前客服',
|
||||
created_at: new Date().toISOString()
|
||||
})
|
||||
|
||||
this.responseContent = ''
|
||||
uni.showToast({
|
||||
title: '处理成功',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
|
||||
resolveTicket() {
|
||||
if (!this.responseContent.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入解决方案',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.ticket.status = 'resolved'
|
||||
this.ticketLogs.push({
|
||||
id: 'log_' + Date.now(),
|
||||
ticket_id: this.ticket.id,
|
||||
action: 'resolved',
|
||||
content: this.responseContent,
|
||||
operator_name: '当前客服',
|
||||
created_at: new Date().toISOString()
|
||||
})
|
||||
|
||||
this.responseContent = ''
|
||||
uni.showToast({
|
||||
title: '工单已解决',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
|
||||
escalateTicket() {
|
||||
uni.showModal({
|
||||
title: '升级工单',
|
||||
content: '确定要将此工单升级到上级处理吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.ticketLogs.push({
|
||||
id: 'log_' + Date.now(),
|
||||
ticket_id: this.ticket.id,
|
||||
action: 'escalated',
|
||||
content: '工单已升级到上级处理',
|
||||
operator_name: '当前客服',
|
||||
created_at: new Date().toISOString()
|
||||
})
|
||||
|
||||
uni.showToast({
|
||||
title: '工单已升级',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
addNote() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/service/add-note?ticketId=${this.ticket.id}`
|
||||
})
|
||||
},
|
||||
|
||||
closeTicket() {
|
||||
uni.showModal({
|
||||
title: '关闭工单',
|
||||
content: '确定要关闭此工单吗?关闭后将无法继续处理。',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.ticket.status = 'closed'
|
||||
this.ticketLogs.push({
|
||||
id: 'log_' + Date.now(),
|
||||
ticket_id: this.ticket.id,
|
||||
action: 'closed',
|
||||
content: '工单已关闭',
|
||||
operator_name: '当前客服',
|
||||
created_at: new Date().toISOString()
|
||||
})
|
||||
|
||||
uni.showToast({
|
||||
title: '工单已关闭',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.ticket-detail-page {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 200rpx;
|
||||
}
|
||||
|
||||
.ticket-status, .user-info, .ticket-content, .ticket-logs, .related-order, .solution-suggestions, .ticket-actions {
|
||||
background-color: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.status-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.ticket-id {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.status-open {
|
||||
background-color: #ffa726;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background-color: #2196f3;
|
||||
}
|
||||
|
||||
.status-resolved {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.status-closed {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.ticket-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ticket-meta {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
background-color: #f0f0f0;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50rpx;
|
||||
margin-right: 25rpx;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.user-contact, .user-email {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.action-btn.call, .action-btn.message {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
background-color: #f0f8ff;
|
||||
border: none;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.content-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.attachments {
|
||||
margin-top: 25rpx;
|
||||
}
|
||||
|
||||
.attachment-title {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15rpx;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8rpx;
|
||||
min-width: 200rpx;
|
||||
}
|
||||
|
||||
.attachment-icon {
|
||||
font-size: 24rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.empty-logs {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
padding: 25rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.log-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.log-operator {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.log-action {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
background-color: #f8f9fa;
|
||||
padding: 15rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
padding: 25rpx;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.order-no {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.order-status {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.order-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.order-amount {
|
||||
font-size: 24rpx;
|
||||
color: #ff4444;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.order-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: 25rpx;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 10rpx;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.suggestion-title {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.suggestion-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.quick-replies {
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.quick-title {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.reply-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.reply-tag {
|
||||
padding: 12rpx 20rpx;
|
||||
background-color: #e3f2fd;
|
||||
color: #1976d2;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.response-form {
|
||||
margin-top: 25rpx;
|
||||
}
|
||||
|
||||
.response-input {
|
||||
width: 100%;
|
||||
min-height: 200rpx;
|
||||
padding: 20rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 10rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.4;
|
||||
background-color: #fafafa;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.attach-btn, .template-btn {
|
||||
padding: 15rpx 25rpx;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #fff;
|
||||
padding: 25rpx 30rpx;
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
gap: 15rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bottom-actions .action-btn {
|
||||
flex: 1;
|
||||
min-width: 120rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 24rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background-color: #2196f3;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-btn.success {
|
||||
background-color: #4caf50;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-btn.warning {
|
||||
background-color: #ffa726;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background-color: #e0e0e0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
background-color: #ff4444;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user