Files
akmon/pages/mall/service/profile.uvue
2026-01-20 08:04:15 +08:00

998 lines
21 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 客服端 - 个人中心 -->
<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>