微信修改
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #fff 0%, #f0f1f6 60%, #f0f1f6 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 176rpx;
|
||||
padding-top: 88rpx;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 24rpx;
|
||||
padding-right: 24rpx;
|
||||
box-sizing: border-box;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-left: 4rpx solid #222;
|
||||
border-bottom: 4rpx solid #222;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-left: 16rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 12rpx 20rpx;
|
||||
background: rgba(255,255,255,0.9);
|
||||
border-radius: 32rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #173294;
|
||||
}
|
||||
|
||||
.meeting-btn {
|
||||
background: linear-gradient(90deg, #52c41a 0%, #73d13d 100%);
|
||||
}
|
||||
|
||||
.meeting-btn .action-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.chat-area {
|
||||
flex: 1;
|
||||
margin-top: 176rpx;
|
||||
padding: 24rpx;
|
||||
padding-bottom: 180rpx;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.msg {
|
||||
display: flex;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.msg.ai {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.msg.user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.message-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.other-row {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.self-row {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(145deg, #e8f7ff 0%, #c5e4ff 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.self-avatar {
|
||||
background: linear-gradient(145deg, #e9f1ff 0%, #c5d9ff 100%);
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #173294;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 480rpx;
|
||||
}
|
||||
|
||||
.self-row .message-content {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.sender-name {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
max-width: 480rpx;
|
||||
padding: 18rpx 20rpx;
|
||||
border-radius: 18rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.other-bubble {
|
||||
background: #fff;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.self-bubble {
|
||||
background: #e9f1ff;
|
||||
color: #173294;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #f0f1f6;
|
||||
padding: 20rpx 24rpx 40rpx;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 50rpx;
|
||||
padding: 10rpx 96rpx 10rpx 18rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
position: absolute;
|
||||
right: 12rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
border: 2rpx solid #8dbbff;
|
||||
background: #e9f1ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.send-text {
|
||||
font-size: 24rpx;
|
||||
color: #4b87ff;
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<!-- #ifdef APP -->
|
||||
<scroll-view style="flex:1">
|
||||
<!-- #endif -->
|
||||
<view class="page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<text class="nav-back-icon">←</text>
|
||||
</view>
|
||||
<text class="nav-title">{{ roomName }}</text>
|
||||
<view class="nav-actions">
|
||||
<view class="action-btn" @tap="handleWorkcaseAction">
|
||||
<text class="action-text">{{ workcaseId ? '查看工单' : '创建工单' }}</text>
|
||||
</view>
|
||||
<view class="action-btn meeting-btn" @tap="startMeeting">
|
||||
<text class="action-text">发起会议</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天消息区域 -->
|
||||
<scroll-view class="chat-area" scroll-y="true" :scroll-top="scrollTop"
|
||||
:style="{ marginTop: headerTotalHeight + 'px' }">
|
||||
<view class="message-list">
|
||||
<view class="message-item" v-for="(msg, index) in messages" :key="index"
|
||||
:class="msg.senderType === 'guest' ? 'self' : 'other'">
|
||||
<!-- 对方消息(左侧) -->
|
||||
<view class="message-row other-row" v-if="msg.senderType !== 'guest'">
|
||||
<view class="avatar">
|
||||
<text class="avatar-text">{{ msg.senderName?.charAt(0) || '客' }}</text>
|
||||
</view>
|
||||
<view class="message-content">
|
||||
<text class="sender-name">{{ msg.senderName || '客服' }}</text>
|
||||
<view class="bubble other-bubble">
|
||||
<text class="message-text">{{ msg.content }}</text>
|
||||
</view>
|
||||
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 自己消息(右侧) -->
|
||||
<view class="message-row self-row" v-else>
|
||||
<view class="message-content">
|
||||
<view class="bubble self-bubble">
|
||||
<text class="message-text">{{ msg.content }}</text>
|
||||
</view>
|
||||
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
|
||||
</view>
|
||||
<view class="avatar self-avatar">
|
||||
<text class="avatar-text">我</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部输入区 -->
|
||||
<view class="footer">
|
||||
<view class="input-row">
|
||||
<input class="chat-input" v-model="inputText" placeholder="输入消息..."
|
||||
@confirm="sendMessage" />
|
||||
<view class="send-btn" @tap="sendMessage">
|
||||
<text class="send-text">发送</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 工单创建弹窗 -->
|
||||
<WorkcaseCreator v-if="showWorkcaseCreator" :show="showWorkcaseCreator"
|
||||
@close="hideCreator" @success="onWorkcaseCreated" />
|
||||
</view>
|
||||
<!-- #ifdef APP -->
|
||||
</scroll-view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, onMounted } from 'vue'
|
||||
import WorkcaseCreator from '@/components/WorkcaseCreator/WorkcaseCreator.uvue'
|
||||
import type { ChatRoomMessageVO } from '@/types/workcase'
|
||||
|
||||
// 响应式数据
|
||||
const headerPaddingTop = ref<number>(44)
|
||||
const headerTotalHeight = ref<number>(88)
|
||||
const roomId = ref<string>('')
|
||||
const workcaseId = ref<string>('')
|
||||
const roomName = ref<string>('聊天室')
|
||||
const inputText = ref<string>('')
|
||||
const scrollTop = ref<number>(0)
|
||||
const showWorkcaseCreator = ref<boolean>(false)
|
||||
|
||||
// 消息列表
|
||||
const messages = ref<ChatRoomMessageVO[]>([
|
||||
{
|
||||
messageId: '1',
|
||||
roomId: 'room001',
|
||||
senderId: 'agent001',
|
||||
senderType: 'agent',
|
||||
senderName: '客服小张',
|
||||
content: '您好,我是客服小张,请问有什么可以帮助您的?',
|
||||
sendTime: '2024-12-17 16:00:00'
|
||||
},
|
||||
{
|
||||
messageId: '2',
|
||||
roomId: 'room001',
|
||||
senderId: 'guest001',
|
||||
senderType: 'guest',
|
||||
senderName: '李经理',
|
||||
content: '我们的设备出现了控制系统故障,无法正常启动',
|
||||
sendTime: '2024-12-17 16:02:00'
|
||||
},
|
||||
{
|
||||
messageId: '3',
|
||||
roomId: 'room001',
|
||||
senderId: 'agent001',
|
||||
senderType: 'agent',
|
||||
senderName: '客服小张',
|
||||
content: '好的,请问是哪个型号的设备?能否提供一下设备序列号?',
|
||||
sendTime: '2024-12-17 16:03:00'
|
||||
},
|
||||
{
|
||||
messageId: '4',
|
||||
roomId: 'room001',
|
||||
senderId: 'guest001',
|
||||
senderType: 'guest',
|
||||
senderName: '李经理',
|
||||
content: '型号是TH-500GF,序列号是TH20230501001',
|
||||
sendTime: '2024-12-17 16:05:00'
|
||||
},
|
||||
{
|
||||
messageId: '5',
|
||||
roomId: 'room001',
|
||||
senderId: 'agent001',
|
||||
senderType: 'agent',
|
||||
senderName: '客服小张',
|
||||
content: '好的,我已经记录了您的问题。建议您创建一个工单,我们会安排工程师尽快上门处理。',
|
||||
sendTime: '2024-12-17 16:08:00'
|
||||
}
|
||||
])
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
|
||||
headerPaddingTop.value = menuButtonInfo.top
|
||||
headerTotalHeight.value = menuButtonInfo.bottom + 8
|
||||
} catch (e) {
|
||||
headerPaddingTop.value = res.statusBarHeight || 44
|
||||
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
|
||||
}
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
headerPaddingTop.value = res.statusBarHeight || 44
|
||||
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
|
||||
// #endif
|
||||
}
|
||||
})
|
||||
|
||||
// 获取页面参数
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1] as any
|
||||
if (currentPage && currentPage.options) {
|
||||
roomId.value = currentPage.options.roomId || ''
|
||||
workcaseId.value = currentPage.options.workcaseId || ''
|
||||
}
|
||||
|
||||
loadChatRoom()
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
// 加载聊天室
|
||||
function loadChatRoom() {
|
||||
console.log('加载聊天室:', roomId.value)
|
||||
// TODO: 调用 workcaseChatAPI.getChatRoomById() 获取聊天室信息
|
||||
// TODO: 调用 workcaseChatAPI.getChatMessagePage() 获取消息列表
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(time?: string): string {
|
||||
if (!time) return ''
|
||||
const date = new Date(time)
|
||||
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
function sendMessage() {
|
||||
const text = inputText.value.trim()
|
||||
if (!text) return
|
||||
|
||||
const newMsg: ChatRoomMessageVO = {
|
||||
messageId: Date.now().toString(),
|
||||
roomId: roomId.value,
|
||||
senderId: 'guest001',
|
||||
senderType: 'guest',
|
||||
senderName: '我',
|
||||
content: text,
|
||||
sendTime: new Date().toISOString()
|
||||
}
|
||||
|
||||
messages.value.push(newMsg)
|
||||
inputText.value = ''
|
||||
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
// TODO: 调用 workcaseChatAPI.sendMessage() 发送消息
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
function scrollToBottom() {
|
||||
scrollTop.value = 999999
|
||||
}
|
||||
|
||||
// 处理工单操作
|
||||
function handleWorkcaseAction() {
|
||||
if (workcaseId.value) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/workcase/workcaseDetail/workcaseDetail?workcaseId=${workcaseId.value}`
|
||||
})
|
||||
} else {
|
||||
showWorkcaseCreator.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏工单创建器
|
||||
function hideCreator() {
|
||||
showWorkcaseCreator.value = false
|
||||
}
|
||||
|
||||
// 工单创建成功
|
||||
function onWorkcaseCreated(data: any) {
|
||||
hideCreator()
|
||||
workcaseId.value = data.workcaseId || 'new-workcase'
|
||||
uni.showToast({
|
||||
title: '工单创建成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
// 发起会议
|
||||
function startMeeting() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/meeting/Meeting/Meeting?roomId=${roomId.value}&workcaseId=${workcaseId.value}`
|
||||
})
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./chatRoom.scss";
|
||||
</style>
|
||||
@@ -0,0 +1,229 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f4f5f7;
|
||||
}
|
||||
|
||||
.nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 176rpx;
|
||||
padding-top: 88rpx;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding-left: 24rpx;
|
||||
padding-right: 24rpx;
|
||||
box-sizing: border-box;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-left: 4rpx solid #222;
|
||||
border-bottom: 4rpx solid #222;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding-right: 174rpx;
|
||||
}
|
||||
|
||||
.nav-capsule {
|
||||
width: 174rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 176rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
padding-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.room-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.room-avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(145deg, #e8f7ff 0%, #c5e4ff 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #173294;
|
||||
}
|
||||
|
||||
.room-info {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.room-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.room-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.room-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.room-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.last-message {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.unread-badge {
|
||||
min-width: 36rpx;
|
||||
height: 36rpx;
|
||||
padding: 0 12rpx;
|
||||
background: #ff4d4f;
|
||||
border-radius: 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.room-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
margin-left: 20rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background: #e7f7ea;
|
||||
}
|
||||
|
||||
.status-active .status-dot {
|
||||
background: #3abe59;
|
||||
}
|
||||
|
||||
.status-active .status-text {
|
||||
color: #3abe59;
|
||||
}
|
||||
|
||||
.status-waiting {
|
||||
background: #fff7e6;
|
||||
}
|
||||
|
||||
.status-waiting .status-dot {
|
||||
background: #faad14;
|
||||
}
|
||||
|
||||
.status-waiting .status-text {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.status-closed {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.status-closed .status-dot {
|
||||
background: #999;
|
||||
}
|
||||
|
||||
.status-closed .status-text {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 160rpx 40rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 96rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #222;
|
||||
font-weight: 500;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<!-- #ifdef APP -->
|
||||
<scroll-view style="flex:1">
|
||||
<!-- #endif -->
|
||||
<view class="page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<text class="nav-back-icon">←</text>
|
||||
</view>
|
||||
<text class="nav-title">我的聊天室</text>
|
||||
<view class="nav-capsule"></view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天室列表 -->
|
||||
<scroll-view class="list" scroll-y="true" :style="{ marginTop: headerTotalHeight + 'px' }">
|
||||
<view class="room-card" v-for="(room, index) in chatRooms" :key="index" @tap="enterRoom(room)">
|
||||
<view class="room-avatar">
|
||||
<text class="avatar-text">{{ room.guestName?.charAt(0) || '客' }}</text>
|
||||
</view>
|
||||
<view class="room-info">
|
||||
<view class="room-header">
|
||||
<text class="room-name">{{ room.roomName || '聊天室' }}</text>
|
||||
<text class="room-time">{{ formatTime(room.lastMessageTime) }}</text>
|
||||
</view>
|
||||
<view class="room-footer">
|
||||
<text class="last-message">{{ room.lastMessage || '暂无消息' }}</text>
|
||||
<view class="unread-badge" v-if="room.unreadCount && room.unreadCount > 0">
|
||||
<text class="badge-text">{{ room.unreadCount > 99 ? '99+' : room.unreadCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="room-status" :class="getStatusClass(room.status)">
|
||||
<text class="status-dot"></text>
|
||||
<text class="status-text">{{ getStatusText(room.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="chatRooms.length === 0">
|
||||
<text class="empty-icon">💬</text>
|
||||
<text class="empty-text">暂无聊天室</text>
|
||||
<text class="empty-hint">点击"联系人工"创建新聊天室</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- #ifdef APP -->
|
||||
</scroll-view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { ChatRoomVO } from '@/types/workcase'
|
||||
|
||||
// 响应式数据
|
||||
const headerPaddingTop = ref<number>(44)
|
||||
const headerTotalHeight = ref<number>(88)
|
||||
|
||||
// 聊天室列表
|
||||
const chatRooms = ref<ChatRoomVO[]>([
|
||||
{
|
||||
roomId: 'room001',
|
||||
roomName: '控制系统故障咨询',
|
||||
guestId: '1',
|
||||
guestName: '李经理',
|
||||
status: 'active',
|
||||
lastMessage: '好的,工程师会尽快联系您',
|
||||
lastMessageTime: '2024-12-17 16:30:00',
|
||||
unreadCount: 2,
|
||||
workcaseId: 'TH20241217001'
|
||||
},
|
||||
{
|
||||
roomId: 'room002',
|
||||
roomName: '设备维修咨询',
|
||||
guestId: '2',
|
||||
guestName: '王工',
|
||||
status: 'closed',
|
||||
lastMessage: '问题已解决,感谢您的咨询',
|
||||
lastMessageTime: '2024-12-16 14:20:00',
|
||||
unreadCount: 0
|
||||
},
|
||||
{
|
||||
roomId: 'room003',
|
||||
roomName: '新设备安装咨询',
|
||||
guestId: '3',
|
||||
guestName: '张总',
|
||||
status: 'waiting',
|
||||
lastMessage: '您好,请问有什么可以帮助您的?',
|
||||
lastMessageTime: '2024-12-17 10:15:00',
|
||||
unreadCount: 0
|
||||
}
|
||||
])
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
|
||||
headerPaddingTop.value = menuButtonInfo.top
|
||||
headerTotalHeight.value = menuButtonInfo.bottom + 8
|
||||
} catch (e) {
|
||||
headerPaddingTop.value = res.statusBarHeight || 44
|
||||
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
|
||||
}
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
headerPaddingTop.value = res.statusBarHeight || 44
|
||||
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
|
||||
// #endif
|
||||
}
|
||||
})
|
||||
|
||||
loadChatRooms()
|
||||
})
|
||||
|
||||
// 加载聊天室列表
|
||||
function loadChatRooms() {
|
||||
console.log('加载聊天室列表')
|
||||
// TODO: 调用 workcaseChatAPI.getChatRoomPage() 获取数据
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(time?: string): string {
|
||||
if (!time) return ''
|
||||
const date = new Date(time)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
|
||||
if (diff < 60000) return '刚刚'
|
||||
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'
|
||||
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'
|
||||
if (diff < 172800000) return '昨天'
|
||||
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`
|
||||
}
|
||||
|
||||
// 获取状态样式类
|
||||
function getStatusClass(status?: string): string {
|
||||
switch (status) {
|
||||
case 'active': return 'status-active'
|
||||
case 'waiting': return 'status-waiting'
|
||||
case 'closed': return 'status-closed'
|
||||
default: return 'status-waiting'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status?: string): string {
|
||||
switch (status) {
|
||||
case 'active': return '进行中'
|
||||
case 'waiting': return '等待中'
|
||||
case 'closed': return '已关闭'
|
||||
default: return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 进入聊天室
|
||||
function enterRoom(room: ChatRoomVO) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/chatRoom/chatRoom/chatRoom?roomId=${room.roomId}&workcaseId=${room.workcaseId || ''}`
|
||||
})
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./chatRoomList.scss";
|
||||
</style>
|
||||
@@ -19,27 +19,32 @@
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
// background: linear-gradient(180deg, rgba(235, 245, 255, 0.8) 0%, rgba(255, 255, 255, 0.95) 100%);
|
||||
// backdrop-filter: blur(10px);
|
||||
z-index: 100;
|
||||
box-sizing: border-box;
|
||||
// paddingTop和height通过JS动态设置
|
||||
|
||||
// 小程序需要为右侧胶囊按钮留出空间
|
||||
/* #ifdef MP-WEIXIN */
|
||||
padding-right: 100px; // 为胶囊按钮留出空间
|
||||
padding-right: 100px;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px; // 调整字体大小以适配胶囊按钮高度
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #000000;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.workcase-btn {
|
||||
@@ -47,16 +52,21 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
height: 32px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(10px);
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
height: 28px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0; // 防止按钮被压缩
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.workcase-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
@@ -65,11 +75,114 @@
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
color: #000000;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// 欢迎区域(机器人+浮动标签)
|
||||
.hero {
|
||||
position: relative;
|
||||
height: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 80px;
|
||||
}
|
||||
|
||||
.rings {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ring {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(100, 180, 255, 0.15);
|
||||
}
|
||||
|
||||
.r1 { width: 260px; height: 260px; }
|
||||
.r2 { width: 200px; height: 200px; border-color: rgba(100, 180, 255, 0.2); }
|
||||
.r3 { width: 150px; height: 150px; border-color: rgba(100, 180, 255, 0.25); }
|
||||
.r4 { width: 110px; height: 110px; border-color: rgba(100, 180, 255, 0.35); }
|
||||
|
||||
.robot {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.robot-face {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: linear-gradient(145deg, #e8f7ff 0%, #c5e4ff 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
box-shadow: 0 10px 40px rgba(180, 220, 255, 0.5), inset 0 0 20px rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.eye {
|
||||
width: 14px;
|
||||
height: 24px;
|
||||
background: linear-gradient(180deg, #7ec1ff 0%, #1846ff 100%);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.float-tag {
|
||||
position: absolute;
|
||||
padding: 6px 12px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid rgba(200, 220, 255, 0.5);
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.t1 { right: 20px; top: 40px; }
|
||||
.t2 { left: 20px; top: 80px; }
|
||||
.t3 { right: 30px; bottom: 50px; }
|
||||
|
||||
.greeting {
|
||||
text-align: left;
|
||||
padding: 0 30px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.greeting-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #1d72d3;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.greeting-sub {
|
||||
font-size: 15px;
|
||||
color: #a2a9b7;
|
||||
}
|
||||
|
||||
// AI初始消息
|
||||
.ai-initial-msg {
|
||||
background: #fff;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
margin: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.ai-msg-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
// 聊天消息区域
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
@@ -216,132 +329,117 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
// background: #FFFFFF;
|
||||
// background: rgba(240, 241, 246, 0.95);
|
||||
padding: 12px 16px;
|
||||
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||||
// box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
// 第一行容器
|
||||
.top-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
// 快捷按钮横向滚动
|
||||
.quick-scroll {
|
||||
white-space: nowrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
// 主要操作按钮
|
||||
.main-actions {
|
||||
display: flex;
|
||||
.quick-list {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
height: 30px;
|
||||
padding: 0 20px;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #5B8FF9;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: #F7F8FA;
|
||||
color: #1F2329;
|
||||
border: 1px solid #E5E6EB;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// 竖向分隔线
|
||||
.divider-line {
|
||||
width: 1px;
|
||||
height: 30px;
|
||||
background: #E5E6EB;
|
||||
margin: 0 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 快速问题区域
|
||||
.quick-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
// width: 100%;
|
||||
height: 30px;
|
||||
background: #F7F8FA;
|
||||
border: 1px solid #E5E6EB;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
flex-basis: auto;
|
||||
align-content: stretch;
|
||||
min-height: 0px;
|
||||
min-width: 0px;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 10px 24px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.quick-btn.has-icon {
|
||||
// background: linear-gradient(90deg, #173294 0%, #4a6fd9 100%);
|
||||
border: none;
|
||||
padding: 10px 24px;
|
||||
}
|
||||
|
||||
.quick-btn.has-icon .quick-text {
|
||||
// color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
font-size: 14px;
|
||||
margin-right: 6px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.quick-divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: #d0d5dd;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.quick-text {
|
||||
font-size: 13px;
|
||||
color: #646A73;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 输入区域
|
||||
.input-section {
|
||||
.chat-input-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 24px;
|
||||
padding: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
padding: 0 50px 0 16px;
|
||||
background: #F7F8FA;
|
||||
border: 1px solid #E5E6EB;
|
||||
border-radius: 20px;
|
||||
padding: 0 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
color: #1F2329;
|
||||
box-sizing: border-box;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.chat-input::placeholder {
|
||||
color: #8F959E;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
border: 2px solid;
|
||||
.send-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(135deg, #e9f1ff 0%, #d4e4ff 100%);
|
||||
border: 1px solid #8dbbff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
.send-icon {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: #000000;
|
||||
line-height: 1;
|
||||
color: #4b87ff;
|
||||
}
|
||||
|
||||
@@ -3,18 +3,48 @@
|
||||
<!-- 顶部标题栏 -->
|
||||
<view class="header" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
|
||||
<text class="title">泰豪小电</text>
|
||||
<button class="workcase-btn" @tap="goToWorkList">
|
||||
<image class="btn-icon" src="/static/imgs/case.svg" />
|
||||
<text class="btn-text">我的工单</text>
|
||||
</button>
|
||||
<view class="header-right">
|
||||
<button class="workcase-btn" @tap="switchMockUser" v-if="isMockMode">
|
||||
<text class="btn-text">切换</text>
|
||||
</button>
|
||||
<button class="workcase-btn" @tap="goToChatRoomList">
|
||||
<text class="btn-text">聊天室</text>
|
||||
</button>
|
||||
<button class="workcase-btn" @tap="goToWorkList">
|
||||
<image class="btn-icon" src="/static/imgs/case.svg" />
|
||||
<text class="btn-text">工单</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 欢迎区域(机器人+浮动标签) -->
|
||||
<view class="hero" v-if="messages.length === 0">
|
||||
<view class="rings">
|
||||
<view class="ring r1"></view>
|
||||
<view class="ring r2"></view>
|
||||
<view class="ring r3"></view>
|
||||
<view class="ring r4"></view>
|
||||
</view>
|
||||
<view class="robot">
|
||||
<view class="robot-face">
|
||||
<view class="eye left"></view>
|
||||
<view class="eye right"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="float-tag t1">查询质保状态</view>
|
||||
<view class="float-tag t2">发动机无法启动</view>
|
||||
<view class="float-tag t3">申请上门维修</view>
|
||||
</view>
|
||||
|
||||
<view class="greeting" v-if="messages.length === 0">
|
||||
<text class="greeting-title">Hi~ 有什么可以帮您!</text>
|
||||
<text class="greeting-sub">泰豪小电为您服务:)</text>
|
||||
</view>
|
||||
|
||||
<!-- 聊天消息区域 -->
|
||||
<scroll-view class="chat-messages" scroll-y="true" :scroll-top="scrollTop" scroll-with-animation="true">
|
||||
<!-- 默认欢迎界面 -->
|
||||
<view class="welcome-container" v-if="messages.length === 0">
|
||||
<image class="welcome-image" src="/static/imgs/defaultchat.png" />
|
||||
<text class="welcome-text-primary">Hi~ 有什么可以帮您!</text>
|
||||
<text class="welcome-text-secondary">泰豪小电为您服务:)</text>
|
||||
<scroll-view class="chat-messages" scroll-y="true" :scroll-top="scrollTop" scroll-with-animation="true" :class="{ started: messages.length > 0 }">
|
||||
<!-- AI初始消息 -->
|
||||
<view class="ai-initial-msg" v-if="messages.length === 0">
|
||||
<text class="ai-msg-text">您好,我是泰豪小电智能客服。请描述您的问题,我会尽力协助。</text>
|
||||
</view>
|
||||
<!-- 聊天消息列表 -->
|
||||
<view class="messages-list" v-else>
|
||||
@@ -44,34 +74,33 @@
|
||||
</scroll-view>
|
||||
<!-- 底部操作区域 -->
|
||||
<view class="bottom-area">
|
||||
<!-- 第一行:按钮和快速问题 -->
|
||||
<view class="top-row">
|
||||
<view class="main-actions">
|
||||
<button class="action-btn primary" @tap="contactHuman">
|
||||
<text class="action-text">联系人工</text>
|
||||
</button>
|
||||
<button class="action-btn secondary" @tap="showCreator">
|
||||
<text class="action-text">创建工单</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 竖向分隔线 -->
|
||||
<view class="divider-line"></view>
|
||||
|
||||
<!-- 快速问题 -->
|
||||
<view class="quick-section">
|
||||
<button class="quick-btn" @tap="handleQuickQuestion">
|
||||
<!-- 快捷按钮横向滚动 -->
|
||||
<scroll-view class="quick-scroll" scroll-x="true">
|
||||
<view class="quick-list">
|
||||
<view class="quick-btn has-icon" @tap="contactHuman">
|
||||
<text class="quick-icon">☎</text>
|
||||
<text class="quick-text">联系人工</text>
|
||||
</view>
|
||||
<view class="quick-btn has-icon" @tap="showCreator">
|
||||
<text class="quick-icon">⊕</text>
|
||||
<text class="quick-text">创建工单</text>
|
||||
</view>
|
||||
<view class="quick-divider"></view>
|
||||
<view class="quick-btn" @tap="handleQuickQuestion('查询质保状态')">
|
||||
<text class="quick-text">查询质保状态</text>
|
||||
</button>
|
||||
</view>
|
||||
<view class="quick-btn" @tap="handleQuickQuestion('发动机无法启动')">
|
||||
<text class="quick-text">发动机无法启动</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<view class="input-section">
|
||||
<input class="chat-input" v-model="inputText" placeholder="输入问题来问我~" @confirm="sendMessage" />
|
||||
<button class="add-btn" @tap="showUploadOptions">
|
||||
<text class="add-icon">+</text>
|
||||
</button>
|
||||
<view class="chat-input-wrap">
|
||||
<input class="chat-input" v-model="inputText" placeholder="输入问题 来问问我~" @confirm="sendMessage" />
|
||||
<view class="send-btn" @tap="sendMessage">
|
||||
<text class="send-icon">➤</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 工单创建弹窗 -->
|
||||
@@ -111,8 +140,85 @@
|
||||
const headerPaddingTop = ref<number>(44) // header顶部padding,默认44px
|
||||
const headerTotalHeight = ref<number>(76) // header总高度,默认76px
|
||||
|
||||
// 用户信息
|
||||
const userInfo = ref({
|
||||
wechatId: '',
|
||||
username: '',
|
||||
phone: ''
|
||||
})
|
||||
const isMockMode = ref(true) // 开发环境mock模式
|
||||
|
||||
// 初始化用户信息
|
||||
function initUserInfo() {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 正式环境:从微信获取用户信息
|
||||
// wx.login({
|
||||
// success: (loginRes) => {
|
||||
// // 使用code换取openid等信息
|
||||
// console.log('微信登录code:', loginRes.code)
|
||||
// }
|
||||
// })
|
||||
// #endif
|
||||
|
||||
// 开发环境:使用mock数据
|
||||
if (isMockMode.value) {
|
||||
userInfo.value = {
|
||||
wechatId: '17857100375',
|
||||
username: '测试用户',
|
||||
phone: '17857100375'
|
||||
}
|
||||
doIdentify()
|
||||
}
|
||||
}
|
||||
|
||||
// 切换mock用户(开发调试用)
|
||||
function switchMockUser() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['员工 (17857100375)', '访客 (17857100376)'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
userInfo.value = { wechatId: '17857100375', username: '员工用户', phone: '17857100375' }
|
||||
} else {
|
||||
userInfo.value = { wechatId: '17857100376', username: '访客用户', phone: '17857100376' }
|
||||
}
|
||||
doIdentify()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 调用identify接口
|
||||
function doIdentify() {
|
||||
uni.showLoading({ title: '登录中...' })
|
||||
uni.request({
|
||||
url: 'http://localhost:8180/urban-lifeline/system/guest/identify',
|
||||
method: 'POST',
|
||||
header: { 'Content-Type': 'application/json' },
|
||||
data: { wechatId: userInfo.value.wechatId, phone: userInfo.value.phone },
|
||||
success: (res : any) => {
|
||||
uni.hideLoading()
|
||||
if (res.statusCode === 200 && res.data?.success) {
|
||||
const loginDomain = res.data.data
|
||||
uni.setStorageSync('token', loginDomain.token || '')
|
||||
uni.setStorageSync('userInfo', JSON.stringify(loginDomain.user))
|
||||
uni.setStorageSync('wechatId', userInfo.value.wechatId)
|
||||
console.log('identify成功:', loginDomain)
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
} else {
|
||||
console.error('identify失败:', res.data?.message)
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
console.error('identify请求失败:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 初始化用户信息
|
||||
initUserInfo()
|
||||
|
||||
// 设置页面标题
|
||||
uni.setNavigationBarTitle({
|
||||
title: '智能助手'
|
||||
@@ -268,7 +374,14 @@
|
||||
// 跳转到工单列表
|
||||
function goToWorkList() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/workcase/list'
|
||||
url: '/pages/workcase/workcaseList/workcaseList'
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转到聊天室列表
|
||||
function goToChatRoomList() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/chatRoom/chatRoomList/chatRoomList'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -295,9 +408,9 @@
|
||||
}
|
||||
|
||||
// 处理快速问题
|
||||
function handleQuickQuestion() {
|
||||
addMessage('user', '查询质保状态')
|
||||
simulateAIResponse('查询质保状态')
|
||||
function handleQuickQuestion(question : string) {
|
||||
addMessage('user', question)
|
||||
simulateAIResponse(question)
|
||||
}
|
||||
|
||||
// 显示上传选项
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f4f5f7;
|
||||
}
|
||||
|
||||
.nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 176rpx;
|
||||
padding-top: 88rpx;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-left: 24rpx;
|
||||
padding-right: 24rpx;
|
||||
box-sizing: border-box;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-left: 4rpx solid #222;
|
||||
border-bottom: 4rpx solid #222;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.nav-capsule {
|
||||
width: 174rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.meeting-container {
|
||||
margin-top: 176rpx;
|
||||
padding: 48rpx 32rpx;
|
||||
min-height: calc(100vh - 176rpx);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.meeting-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 40rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.meeting-icon {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(145deg, #e8f7ff 0%, #c5e4ff 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 48rpx;
|
||||
box-shadow: 0 10rpx 40rpx rgba(180,220,255,0.5);
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 96rpx;
|
||||
}
|
||||
|
||||
.meeting-name {
|
||||
font-size: 44rpx;
|
||||
font-weight: 900;
|
||||
color: #1d72d3;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.meeting-desc {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-bottom: 64rpx;
|
||||
}
|
||||
|
||||
.meeting-actions {
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
|
||||
.join-btn {
|
||||
height: 96rpx;
|
||||
padding: 0 60rpx;
|
||||
background: linear-gradient(90deg, #173294 0%, #4a6fd9 100%);
|
||||
border-radius: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.join-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.meeting-tips {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
padding: 32rpx;
|
||||
background: #f5f8ff;
|
||||
border-radius: 16rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.in-meeting {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.meeting-webview {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.meeting-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 48rpx;
|
||||
padding: 32rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 24rpx 40rpx;
|
||||
background: #f5f8ff;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.control-btn.active {
|
||||
background: #fff7e6;
|
||||
}
|
||||
|
||||
.leave-btn {
|
||||
background: #fff1f0;
|
||||
}
|
||||
|
||||
.control-icon {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<!-- #ifdef APP -->
|
||||
<scroll-view style="flex:1">
|
||||
<!-- #endif -->
|
||||
<view class="page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<text class="nav-back-icon">←</text>
|
||||
</view>
|
||||
<text class="nav-title">视频会议</text>
|
||||
<view class="nav-capsule"></view>
|
||||
</view>
|
||||
|
||||
<!-- 会议内容区 -->
|
||||
<view class="meeting-container" :style="{ marginTop: headerTotalHeight + 'px' }">
|
||||
<!-- 会议信息 -->
|
||||
<view class="meeting-info" v-if="!isInMeeting">
|
||||
<view class="meeting-icon">
|
||||
<text class="icon-text">📹</text>
|
||||
</view>
|
||||
<text class="meeting-name">{{ meetingName || '视频会议' }}</text>
|
||||
<text class="meeting-desc">与客服进行实时视频沟通</text>
|
||||
|
||||
<view class="meeting-actions">
|
||||
<view class="join-btn" @tap="joinMeeting">
|
||||
<text class="join-text">加入会议</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="meeting-tips">
|
||||
<text class="tip-item">• 请确保网络连接稳定</text>
|
||||
<text class="tip-item">• 允许摄像头和麦克风权限</text>
|
||||
<text class="tip-item">• 建议在安静环境下进行会议</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 会议中状态 -->
|
||||
<view class="in-meeting" v-else>
|
||||
<!-- Jitsi Meet iframe 容器 -->
|
||||
<web-view v-if="iframeUrl" :src="iframeUrl" class="meeting-webview"></web-view>
|
||||
|
||||
<!-- 会议控制栏 -->
|
||||
<view class="meeting-controls">
|
||||
<view class="control-btn" :class="{ active: isMuted }" @tap="toggleMute">
|
||||
<text class="control-icon">{{ isMuted ? '🔇' : '🔊' }}</text>
|
||||
<text class="control-label">{{ isMuted ? '取消静音' : '静音' }}</text>
|
||||
</view>
|
||||
<view class="control-btn" :class="{ active: isVideoOff }" @tap="toggleVideo">
|
||||
<text class="control-icon">{{ isVideoOff ? '📷' : '📹' }}</text>
|
||||
<text class="control-label">{{ isVideoOff ? '开启视频' : '关闭视频' }}</text>
|
||||
</view>
|
||||
<view class="control-btn leave-btn" @tap="leaveMeeting">
|
||||
<text class="control-icon">📞</text>
|
||||
<text class="control-label">离开会议</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #ifdef APP -->
|
||||
</scroll-view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { VideoMeetingVO } from '@/types/workcase'
|
||||
|
||||
// 响应式数据
|
||||
const headerPaddingTop = ref<number>(44)
|
||||
const headerTotalHeight = ref<number>(88)
|
||||
const roomId = ref<string>('')
|
||||
const workcaseId = ref<string>('')
|
||||
const meetingName = ref<string>('视频会议')
|
||||
const isInMeeting = ref<boolean>(false)
|
||||
const iframeUrl = ref<string>('')
|
||||
const isMuted = ref<boolean>(false)
|
||||
const isVideoOff = ref<boolean>(false)
|
||||
|
||||
// 会议信息
|
||||
const meeting = ref<VideoMeetingVO>({})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
|
||||
headerPaddingTop.value = menuButtonInfo.top
|
||||
headerTotalHeight.value = menuButtonInfo.bottom + 8
|
||||
} catch (e) {
|
||||
headerPaddingTop.value = res.statusBarHeight || 44
|
||||
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
|
||||
}
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
headerPaddingTop.value = res.statusBarHeight || 44
|
||||
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
|
||||
// #endif
|
||||
}
|
||||
})
|
||||
|
||||
// 获取页面参数
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1] as any
|
||||
if (currentPage && currentPage.options) {
|
||||
roomId.value = currentPage.options.roomId || ''
|
||||
workcaseId.value = currentPage.options.workcaseId || ''
|
||||
}
|
||||
|
||||
loadMeetingInfo()
|
||||
})
|
||||
|
||||
// 加载会议信息
|
||||
function loadMeetingInfo() {
|
||||
console.log('加载会议信息:', roomId.value)
|
||||
// TODO: 调用 workcaseChatAPI 获取会议信息
|
||||
}
|
||||
|
||||
// 加入会议
|
||||
function joinMeeting() {
|
||||
uni.showLoading({ title: '正在加入会议...' })
|
||||
|
||||
// 模拟加入会议
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
isInMeeting.value = true
|
||||
// TODO: 实际调用API创建/加入会议,获取iframeUrl
|
||||
// iframeUrl.value = meeting.value.iframeUrl || ''
|
||||
|
||||
uni.showToast({
|
||||
title: '已加入会议',
|
||||
icon: 'success'
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 离开会议
|
||||
function leaveMeeting() {
|
||||
uni.showModal({
|
||||
title: '离开会议',
|
||||
content: '确定要离开当前会议吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
isInMeeting.value = false
|
||||
iframeUrl.value = ''
|
||||
uni.showToast({
|
||||
title: '已离开会议',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 切换静音
|
||||
function toggleMute() {
|
||||
isMuted.value = !isMuted.value
|
||||
// TODO: 调用Jitsi API控制静音
|
||||
}
|
||||
|
||||
// 切换视频
|
||||
function toggleVideo() {
|
||||
isVideoOff.value = !isVideoOff.value
|
||||
// TODO: 调用Jitsi API控制视频
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
if (isInMeeting.value) {
|
||||
uni.showModal({
|
||||
title: '离开会议',
|
||||
content: '返回将离开当前会议,确定吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./Meeting.scss";
|
||||
</style>
|
||||
@@ -1,403 +0,0 @@
|
||||
.detail-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background-color: #FFFFFF;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #F0F0F0;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #FFF3E0;
|
||||
color: #F57C00;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background-color: #E3F2FD;
|
||||
color: #1976D2;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background-color: #E8F5E8;
|
||||
color: #388E3C;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
background-color: #FFEBEE;
|
||||
color: #D32F2F;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 80px;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.priority {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.priority-normal {
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.priority-urgent {
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
.priority-emergency {
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 16px;
|
||||
color: #333333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.image-gallery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.gallery-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1976D2;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background-color: #E0E0E0;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background-color: #1976D2;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-desc {
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
margin-right: 16px;
|
||||
border: 3px solid #FFFFFF;
|
||||
box-shadow: 0 0 0 2px #E0E0E0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dot-create {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
.dot-accept {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
.dot-processing {
|
||||
background-color: #FF9800;
|
||||
}
|
||||
|
||||
.dot-complete {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
flex: 1;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.record-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.record-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
margin: 4px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.record-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.record-operator {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.rating-section {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stars {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.star {
|
||||
font-size: 24px;
|
||||
color: #FFD700;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.rating-text {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
background-color: #FFFFFF;
|
||||
padding: 16px;
|
||||
border-top: 1px solid #E0E0E0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background-color: #1976D2;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background-color: #F0F0F0;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.rating-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px 16px 16px;
|
||||
border-bottom: 1px solid #F0F0F0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 14px;
|
||||
background-color: #F0F0F0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
color: #666666;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.rating-form {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
color: #333333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.star-rating {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.rating-star {
|
||||
font-size: 32px;
|
||||
color: #E0E0E0;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.rating-star.active {
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.rating-textarea {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
padding: 12px;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
padding: 16px;
|
||||
border-top: 1px solid #F0F0F0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-btn.cancel {
|
||||
background-color: #F0F0F0;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.modal-btn.confirm {
|
||||
background-color: #1976D2;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.modal-btn[disabled] {
|
||||
background-color: #CCCCCC;
|
||||
color: #999999;
|
||||
}
|
||||
@@ -1,507 +0,0 @@
|
||||
<template>
|
||||
<view class="detail-container">
|
||||
<scroll-view class="detail-content" scroll-y="true">
|
||||
<!-- 工单基本信息 -->
|
||||
<view class="info-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">基本信息</text>
|
||||
<view class="status-tag" :class="getStatusClass(workcase.status)">
|
||||
<text class="status-text">{{workcase.statusText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="label">工单标题</text>
|
||||
<text class="value">{{workcase.title}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="label">工单编号</text>
|
||||
<text class="value">{{workcase.number}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="label">问题分类</text>
|
||||
<text class="value">{{workcase.category}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="label">紧急程度</text>
|
||||
<text class="value priority" :class="getPriorityClass(workcase.priority)">
|
||||
{{workcase.priority}}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="label">联系方式</text>
|
||||
<text class="value">{{workcase.contact}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="label">创建时间</text>
|
||||
<text class="value">{{formatDateTime(workcase.createTime)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 问题描述 -->
|
||||
<view class="info-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">问题描述</text>
|
||||
</view>
|
||||
<text class="description">{{workcase.description}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 图片附件 -->
|
||||
<view class="info-card" v-if="workcase.images && workcase.images.length > 0">
|
||||
<view class="card-header">
|
||||
<text class="card-title">相关图片</text>
|
||||
</view>
|
||||
<view class="image-gallery">
|
||||
<image
|
||||
class="gallery-image"
|
||||
v-for="(image, index) in workcase.images"
|
||||
:key="index"
|
||||
:src="image"
|
||||
mode="aspectFill"
|
||||
@tap="previewImage(index)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 处理进度 -->
|
||||
<view class="info-card" v-if="workcase.status === 'processing'">
|
||||
<view class="card-header">
|
||||
<text class="card-title">处理进度</text>
|
||||
<text class="progress-text">{{workcase.progress}}%</text>
|
||||
</view>
|
||||
<view class="progress-container">
|
||||
<view class="progress-bar">
|
||||
<view class="progress-fill" :style="'width: ' + workcase.progress + '%'"></view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="progress-desc">{{getProgressDesc(workcase.progress)}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 处理记录 -->
|
||||
<view class="info-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">处理记录</text>
|
||||
</view>
|
||||
<view class="timeline">
|
||||
<view
|
||||
class="timeline-item"
|
||||
v-for="(record, index) in workcase.records"
|
||||
:key="index"
|
||||
>
|
||||
<view class="timeline-dot" :class="getRecordDotClass(record.type)"></view>
|
||||
<view class="timeline-content">
|
||||
<text class="record-title">{{record.title}}</text>
|
||||
<text class="record-desc" v-if="record.description">{{record.description}}</text>
|
||||
<view class="record-meta">
|
||||
<text class="record-time">{{formatDateTime(record.time)}}</text>
|
||||
<text class="record-operator">{{record.operator}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 客服评价 -->
|
||||
<view class="info-card" v-if="workcase.rating">
|
||||
<view class="card-header">
|
||||
<text class="card-title">服务评价</text>
|
||||
</view>
|
||||
<view class="rating-section">
|
||||
<view class="stars">
|
||||
<text
|
||||
class="star"
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
:class="{active: i <= workcase.rating.score}"
|
||||
>★</text>
|
||||
</view>
|
||||
<text class="rating-text">{{workcase.rating.comment}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-actions">
|
||||
<button
|
||||
class="action-btn secondary"
|
||||
@tap="contactService"
|
||||
>
|
||||
联系客服
|
||||
</button>
|
||||
<button
|
||||
class="action-btn primary"
|
||||
v-if="workcase.status === 'pending'"
|
||||
@tap="cancelWorkcase"
|
||||
>
|
||||
取消工单
|
||||
</button>
|
||||
<button
|
||||
class="action-btn primary"
|
||||
v-if="workcase.status === 'processing'"
|
||||
@tap="confirmComplete"
|
||||
>
|
||||
确认完成
|
||||
</button>
|
||||
<button
|
||||
class="action-btn primary"
|
||||
v-if="workcase.status === 'completed' && !workcase.rating"
|
||||
@tap="showRating"
|
||||
>
|
||||
服务评价
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 评价弹窗 -->
|
||||
<view class="rating-modal" v-if="showRatingModal">
|
||||
<view class="modal-content">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">服务评价</text>
|
||||
<view class="close-btn" @tap="hideRating">
|
||||
<text class="close-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rating-form">
|
||||
<text class="form-label">请为本次服务打分</text>
|
||||
<view class="star-rating">
|
||||
<text
|
||||
class="rating-star"
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
:class="{active: i <= ratingScore}"
|
||||
@tap="setRating(i)"
|
||||
>★</text>
|
||||
</view>
|
||||
|
||||
<text class="form-label">评价内容</text>
|
||||
<textarea
|
||||
class="rating-textarea"
|
||||
v-model="ratingComment"
|
||||
placeholder="请输入您的评价内容..."
|
||||
maxlength="200"
|
||||
/>
|
||||
<text class="char-count">{{ratingComment.length}}/200</text>
|
||||
</view>
|
||||
|
||||
<view class="modal-actions">
|
||||
<button class="modal-btn cancel" @tap="hideRating">取消</button>
|
||||
<button
|
||||
class="modal-btn confirm"
|
||||
@tap="submitRating"
|
||||
:disabled="ratingScore === 0"
|
||||
>提交</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
// 接口定义
|
||||
interface Workcase {
|
||||
id: string
|
||||
number: string
|
||||
title: string
|
||||
category: string
|
||||
priority: string
|
||||
status: 'pending' | 'processing' | 'completed' | 'cancelled'
|
||||
statusText: string
|
||||
description: string
|
||||
contact: string
|
||||
progress: number
|
||||
createTime: Date
|
||||
updateTime: Date
|
||||
images?: string[]
|
||||
records: ProcessRecord[]
|
||||
rating?: Rating
|
||||
}
|
||||
|
||||
interface ProcessRecord {
|
||||
type: string
|
||||
title: string
|
||||
description?: string
|
||||
time: Date
|
||||
operator: string
|
||||
}
|
||||
|
||||
interface Rating {
|
||||
score: number
|
||||
comment: string
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const workcaseId = ref<string | null>(null)
|
||||
const workcase = ref<Workcase>({} as Workcase)
|
||||
const showRatingModal = ref<boolean>(false)
|
||||
const ratingScore = ref<number>(0)
|
||||
const ratingComment = ref<string>('')
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
workcaseId.value = currentPage.options?.id || '1'
|
||||
loadWorkcaseDetail()
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
async function loadWorkcaseDetail() {
|
||||
try {
|
||||
// 模拟获取工单详情
|
||||
workcase.value = getMockWorkcase()
|
||||
|
||||
// 设置页面标题
|
||||
uni.setNavigationBarTitle({
|
||||
title: workcase.value.title
|
||||
})
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'error'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
|
||||
function getMockWorkcase(): Workcase {
|
||||
return {
|
||||
id: workcaseId.value || '1',
|
||||
number: `WC2024${String(workcaseId.value || '1').padStart(4, '0')}`,
|
||||
title: '小区公园路灯不亮需要维修',
|
||||
category: '设施报修',
|
||||
priority: '紧急',
|
||||
status: 'processing',
|
||||
statusText: '处理中',
|
||||
description: '小区公园内的路灯已经连续三天不亮了,影响居民夜间出行安全。路灯位置在公园主干道上,希望能够尽快派人维修。',
|
||||
contact: '138****5678',
|
||||
progress: 65,
|
||||
createTime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
|
||||
updateTime: new Date(),
|
||||
images: [
|
||||
'/static/workcase1.jpg',
|
||||
'/static/workcase2.jpg'
|
||||
],
|
||||
records: [
|
||||
{
|
||||
type: 'create',
|
||||
title: '工单创建',
|
||||
description: '用户提交工单,问题已记录',
|
||||
time: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
|
||||
operator: '系统'
|
||||
},
|
||||
{
|
||||
type: 'accept',
|
||||
title: '工单受理',
|
||||
description: '客服已受理,安排相关人员处理',
|
||||
time: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000),
|
||||
operator: '客服小王'
|
||||
},
|
||||
{
|
||||
type: 'processing',
|
||||
title: '现场勘查',
|
||||
description: '维修人员已到达现场,正在检查路灯故障原因',
|
||||
time: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000),
|
||||
operator: '维修师傅张三'
|
||||
},
|
||||
{
|
||||
type: 'processing',
|
||||
title: '配件采购',
|
||||
description: '故障原因确认为灯泡损坏,正在采购替换配件',
|
||||
time: new Date(Date.now() - 12 * 60 * 60 * 1000),
|
||||
operator: '维修师傅张三'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态样式
|
||||
function getStatusClass(status: string) {
|
||||
return {
|
||||
'status-pending': status === 'pending',
|
||||
'status-processing': status === 'processing',
|
||||
'status-completed': status === 'completed',
|
||||
'status-cancelled': status === 'cancelled'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取优先级样式
|
||||
function getPriorityClass(priority: string) {
|
||||
return {
|
||||
'priority-normal': priority === '一般',
|
||||
'priority-urgent': priority === '紧急',
|
||||
'priority-emergency': priority === '非常紧急'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取记录点样式
|
||||
function getRecordDotClass(type: string) {
|
||||
return {
|
||||
'dot-create': type === 'create',
|
||||
'dot-accept': type === 'accept',
|
||||
'dot-processing': type === 'processing',
|
||||
'dot-complete': type === 'complete'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取进度描述
|
||||
function getProgressDesc(progress: number): string {
|
||||
if (progress < 25) {
|
||||
return '刚刚开始处理'
|
||||
} else if (progress < 50) {
|
||||
return '正在积极处理中'
|
||||
} else if (progress < 75) {
|
||||
return '处理进展顺利'
|
||||
} else if (progress < 100) {
|
||||
return '即将完成处理'
|
||||
} else {
|
||||
return '处理已完成'
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTime(date: Date): string {
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const hour = String(d.getHours()).padStart(2, '0')
|
||||
const minute = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
function previewImage(index: number) {
|
||||
uni.previewImage({
|
||||
current: index,
|
||||
urls: workcase.value.images || []
|
||||
})
|
||||
}
|
||||
|
||||
// 联系客服
|
||||
function contactService() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['拨打电话', '在线客服'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: '400-123-4567'
|
||||
})
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 取消工单
|
||||
function cancelWorkcase() {
|
||||
uni.showModal({
|
||||
title: '确认取消',
|
||||
content: '确定要取消此工单吗?取消后无法恢复。',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
workcase.value.status = 'cancelled'
|
||||
workcase.value.statusText = '已取消'
|
||||
|
||||
// 添加取消记录
|
||||
workcase.value.records.push({
|
||||
type: 'cancel',
|
||||
title: '工单取消',
|
||||
description: '用户主动取消工单',
|
||||
time: new Date(),
|
||||
operator: '用户'
|
||||
})
|
||||
|
||||
uni.showToast({
|
||||
title: '工单已取消',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 确认完成
|
||||
function confirmComplete() {
|
||||
uni.showModal({
|
||||
title: '确认完成',
|
||||
content: '确认问题已经得到解决?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
workcase.value.status = 'completed'
|
||||
workcase.value.statusText = '已完成'
|
||||
workcase.value.progress = 100
|
||||
|
||||
// 添加完成记录
|
||||
workcase.value.records.push({
|
||||
type: 'complete',
|
||||
title: '工单完成',
|
||||
description: '用户确认问题已解决',
|
||||
time: new Date(),
|
||||
operator: '用户'
|
||||
})
|
||||
|
||||
uni.showToast({
|
||||
title: '工单已完成',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 显示评价弹窗
|
||||
function showRating() {
|
||||
showRatingModal.value = true
|
||||
ratingScore.value = 0
|
||||
ratingComment.value = ''
|
||||
}
|
||||
|
||||
// 隐藏评价弹窗
|
||||
function hideRating() {
|
||||
showRatingModal.value = false
|
||||
}
|
||||
|
||||
// 设置评分
|
||||
function setRating(score: number) {
|
||||
ratingScore.value = score
|
||||
}
|
||||
|
||||
// 提交评价
|
||||
function submitRating() {
|
||||
if (ratingScore.value === 0) {
|
||||
uni.showToast({
|
||||
title: '请选择评分',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
workcase.value.rating = {
|
||||
score: ratingScore.value,
|
||||
comment: ratingComment.value || '用户未填写评价内容'
|
||||
}
|
||||
|
||||
hideRating()
|
||||
|
||||
uni.showToast({
|
||||
title: '评价提交成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './detail.scss';
|
||||
</style>
|
||||
@@ -1,293 +0,0 @@
|
||||
.workcase-list-container {
|
||||
height: 100vh;
|
||||
background-color: #F5F5F5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
background-color: #FFFFFF;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid #E0E0E0;
|
||||
}
|
||||
|
||||
.filter-picker {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.picker-content {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 6px;
|
||||
background-color: #F8F8F8;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
color: #333333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.workcase-list {
|
||||
flex: 1;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background-color: #FFFFFF;
|
||||
margin: 16px 0;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1976D2;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.workcase-card {
|
||||
background-color: #FFFFFF;
|
||||
margin-bottom: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.workcase-title {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #FFF3E0;
|
||||
color: #F57C00;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background-color: #E3F2FD;
|
||||
color: #1976D2;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background-color: #E8F5E8;
|
||||
color: #388E3C;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
background-color: #FFEBEE;
|
||||
color: #D32F2F;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.workcase-id {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.priority {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.priority-normal {
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.priority-urgent {
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
.priority-emergency {
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background-color: #1976D2;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background-color: #F0F0F0;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background-color: #E0E0E0;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background-color: #1976D2;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #999999;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
width: 140px;
|
||||
height: 44px;
|
||||
background-color: #1976D2;
|
||||
color: #FFFFFF;
|
||||
border-radius: 22px;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.create-icon {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.load-text {
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.fab {
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
right: 20px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 28px;
|
||||
background-color: #1976D2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.4);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
color: #FFFFFF;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -1,443 +0,0 @@
|
||||
<template>
|
||||
<view class="workcase-list-container">
|
||||
<!-- 筛选栏 -->
|
||||
<view class="filter-bar">
|
||||
<picker
|
||||
class="filter-picker"
|
||||
:value="statusIndex"
|
||||
:range="statusOptions"
|
||||
@change="onStatusChange"
|
||||
>
|
||||
<view class="picker-content">
|
||||
<text class="picker-text">{{statusOptions[statusIndex]}}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<picker
|
||||
class="filter-picker"
|
||||
:value="categoryIndex"
|
||||
:range="categoryOptions"
|
||||
@change="onCategoryChange"
|
||||
>
|
||||
<view class="picker-content">
|
||||
<text class="picker-text">{{categoryOptions[categoryIndex]}}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 工单列表 -->
|
||||
<scroll-view
|
||||
class="workcase-list"
|
||||
scroll-y="true"
|
||||
@scrolltolower="loadMore"
|
||||
:refresher-enabled="true"
|
||||
@refresherrefresh="onRefresh"
|
||||
:refresher-triggered="isRefreshing"
|
||||
>
|
||||
<!-- 统计信息 -->
|
||||
<view class="stats-card">
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{stats.total}}</text>
|
||||
<text class="stat-label">总工单</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{stats.pending}}</text>
|
||||
<text class="stat-label">待处理</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{stats.processing}}</text>
|
||||
<text class="stat-label">处理中</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{stats.completed}}</text>
|
||||
<text class="stat-label">已完成</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 工单卡片列表 -->
|
||||
<view
|
||||
class="workcase-card"
|
||||
v-for="workcase in displayList"
|
||||
:key="workcase.id"
|
||||
@tap="goToDetail(workcase)"
|
||||
>
|
||||
<view class="card-header">
|
||||
<view class="title-row">
|
||||
<text class="workcase-title">{{workcase.title}}</text>
|
||||
<view class="status-tag" :class="getStatusClass(workcase.status)">
|
||||
<text class="status-text">{{workcase.statusText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="workcase-id">工单编号:{{workcase.number}}</text>
|
||||
</view>
|
||||
|
||||
<view class="card-content">
|
||||
<view class="info-row">
|
||||
<text class="info-label">分类:</text>
|
||||
<text class="info-value">{{workcase.category}}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">紧急程度:</text>
|
||||
<text class="info-value priority" :class="getPriorityClass(workcase.priority)">
|
||||
{{workcase.priority}}
|
||||
</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">创建时间:</text>
|
||||
<text class="info-value">{{formatTime(workcase.createTime)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-description">
|
||||
<text class="description-text">{{workcase.description}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="card-actions" v-if="workcase.status !== 'completed'">
|
||||
<button
|
||||
class="action-btn secondary"
|
||||
v-if="workcase.status === 'pending'"
|
||||
@tap.stop="cancelWorkcase(workcase)"
|
||||
>
|
||||
取消工单
|
||||
</button>
|
||||
<button
|
||||
class="action-btn primary"
|
||||
v-if="workcase.status === 'processing'"
|
||||
@tap.stop="confirmComplete(workcase)"
|
||||
>
|
||||
确认完成
|
||||
</button>
|
||||
<button
|
||||
class="action-btn secondary"
|
||||
@tap.stop="contactService(workcase)"
|
||||
>
|
||||
联系客服
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<view class="progress-bar" v-if="workcase.status === 'processing'">
|
||||
<view class="progress-fill" :style="'width: ' + workcase.progress + '%'"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="displayList.length === 0 && !isLoading">
|
||||
<image class="empty-icon" src="/static/empty-workcase.png" mode="aspectFit" />
|
||||
<text class="empty-text">暂无工单记录</text>
|
||||
<button class="create-btn" @tap="createWorkcase">
|
||||
<text class="create-icon">+</text>
|
||||
<text>创建工单</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view class="load-more" v-if="hasMore">
|
||||
<text class="load-text">{{isLoading ? '加载中...' : '上拉加载更多'}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 悬浮按钮 -->
|
||||
<view class="fab" @tap="createWorkcase">
|
||||
<text class="fab-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
// 接口定义
|
||||
interface Workcase {
|
||||
id: number
|
||||
number: string
|
||||
title: string
|
||||
description: string
|
||||
category: string
|
||||
priority: string
|
||||
status: 'pending' | 'processing' | 'completed' | 'cancelled'
|
||||
statusText: string
|
||||
progress: number
|
||||
createTime: Date
|
||||
updateTime: Date
|
||||
}
|
||||
|
||||
interface Stats {
|
||||
total: number
|
||||
pending: number
|
||||
processing: number
|
||||
completed: number
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const workcaseList = ref<Workcase[]>([])
|
||||
const displayList = ref<Workcase[]>([])
|
||||
const statusOptions = ref<string[]>(['全部状态', '待处理', '处理中', '已完成', '已取消'])
|
||||
const statusIndex = ref<number>(0)
|
||||
const categoryOptions = ref<string[]>(['全部分类', '设施报修', '环境卫生', '交通问题', '安全隐患', '其他问题'])
|
||||
const categoryIndex = ref<number>(0)
|
||||
const stats = ref<Stats>({
|
||||
total: 0,
|
||||
pending: 0,
|
||||
processing: 0,
|
||||
completed: 0
|
||||
})
|
||||
const isLoading = ref<boolean>(false)
|
||||
const isRefreshing = ref<boolean>(false)
|
||||
const hasMore = ref<boolean>(true)
|
||||
const page = ref<number>(1)
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
// 模拟数据
|
||||
const mockData = generateMockData()
|
||||
workcaseList.value = mockData
|
||||
updateDisplayList()
|
||||
updateStats()
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成模拟数据
|
||||
function generateMockData(): Workcase[] {
|
||||
const categories = ['设施报修', '环境卫生', '交通问题', '安全隐患', '其他问题']
|
||||
const priorities = ['一般', '紧急', '非常紧急']
|
||||
const statuses: ('pending' | 'processing' | 'completed' | 'cancelled')[] = ['pending', 'processing', 'completed', 'cancelled']
|
||||
const statusTexts = {
|
||||
pending: '待处理',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消'
|
||||
}
|
||||
|
||||
const mockList: Workcase[] = []
|
||||
for (let i = 1; i <= 15; i++) {
|
||||
const status = statuses[Math.floor(Math.random() * statuses.length)]
|
||||
mockList.push({
|
||||
id: i,
|
||||
number: `WC${new Date().getFullYear()}${String(i).padStart(4, '0')}`,
|
||||
title: `测试工单${i}`,
|
||||
description: `这是一个测试工单的描述内容,描述了具体的问题情况...`,
|
||||
category: categories[Math.floor(Math.random() * categories.length)],
|
||||
priority: priorities[Math.floor(Math.random() * priorities.length)],
|
||||
status: status,
|
||||
statusText: statusTexts[status],
|
||||
progress: status === 'processing' ? Math.floor(Math.random() * 80) + 10 : 0,
|
||||
createTime: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000),
|
||||
updateTime: new Date()
|
||||
})
|
||||
}
|
||||
return mockList
|
||||
}
|
||||
|
||||
// 更新显示列表
|
||||
function updateDisplayList() {
|
||||
let filtered = [...workcaseList.value]
|
||||
|
||||
// 状态筛选
|
||||
if (statusIndex.value > 0) {
|
||||
const statusMap: Record<number, string> = {
|
||||
1: 'pending',
|
||||
2: 'processing',
|
||||
3: 'completed',
|
||||
4: 'cancelled'
|
||||
}
|
||||
filtered = filtered.filter(item => item.status === statusMap[statusIndex.value])
|
||||
}
|
||||
|
||||
// 分类筛选
|
||||
if (categoryIndex.value > 0) {
|
||||
const category = categoryOptions.value[categoryIndex.value]
|
||||
filtered = filtered.filter(item => item.category === category)
|
||||
}
|
||||
|
||||
displayList.value = filtered
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
function updateStats() {
|
||||
stats.value = {
|
||||
total: workcaseList.value.length,
|
||||
pending: workcaseList.value.filter(item => item.status === 'pending').length,
|
||||
processing: workcaseList.value.filter(item => item.status === 'processing').length,
|
||||
completed: workcaseList.value.filter(item => item.status === 'completed').length
|
||||
}
|
||||
}
|
||||
|
||||
// 状态筛选改变
|
||||
function onStatusChange(e: any) {
|
||||
statusIndex.value = e.detail.value
|
||||
updateDisplayList()
|
||||
}
|
||||
|
||||
// 分类筛选改变
|
||||
function onCategoryChange(e: any) {
|
||||
categoryIndex.value = e.detail.value
|
||||
updateDisplayList()
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
function onRefresh() {
|
||||
isRefreshing.value = true
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
function loadMore() {
|
||||
if (!hasMore.value || isLoading.value) return
|
||||
|
||||
page.value++
|
||||
// 模拟加载更多
|
||||
setTimeout(() => {
|
||||
if (page.value > 3) {
|
||||
hasMore.value = false
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 跳转到详情页
|
||||
function goToDetail(workcase: Workcase) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/workcase/detail?id=${workcase.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 获取状态样式
|
||||
function getStatusClass(status: string) {
|
||||
return {
|
||||
'status-pending': status === 'pending',
|
||||
'status-processing': status === 'processing',
|
||||
'status-completed': status === 'completed',
|
||||
'status-cancelled': status === 'cancelled'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取优先级样式
|
||||
function getPriorityClass(priority: string) {
|
||||
return {
|
||||
'priority-normal': priority === '一般',
|
||||
'priority-urgent': priority === '紧急',
|
||||
'priority-emergency': priority === '非常紧急'
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(date: Date) {
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - new Date(date).getTime()
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (days === 0) {
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60))
|
||||
if (hours === 0) {
|
||||
const minutes = Math.floor(diff / (1000 * 60))
|
||||
return `${minutes}分钟前`
|
||||
}
|
||||
return `${hours}小时前`
|
||||
} else if (days === 1) {
|
||||
return '昨天'
|
||||
} else if (days < 7) {
|
||||
return `${days}天前`
|
||||
} else {
|
||||
return new Date(date).toLocaleDateString()
|
||||
}
|
||||
}
|
||||
|
||||
// 取消工单
|
||||
function cancelWorkcase(workcase: Workcase) {
|
||||
uni.showModal({
|
||||
title: '确认取消',
|
||||
content: `确定要取消工单"${workcase.title}"吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
workcase.status = 'cancelled'
|
||||
workcase.statusText = '已取消'
|
||||
updateStats()
|
||||
updateDisplayList()
|
||||
|
||||
uni.showToast({
|
||||
title: '工单已取消',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 确认完成
|
||||
function confirmComplete(workcase: Workcase) {
|
||||
uni.showModal({
|
||||
title: '确认完成',
|
||||
content: `确认工单"${workcase.title}"已处理完成?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
workcase.status = 'completed'
|
||||
workcase.statusText = '已完成'
|
||||
workcase.progress = 100
|
||||
updateStats()
|
||||
updateDisplayList()
|
||||
|
||||
uni.showToast({
|
||||
title: '工单已完成',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 联系客服
|
||||
function contactService(workcase: Workcase) {
|
||||
uni.showActionSheet({
|
||||
itemList: ['拨打电话', '在线客服', '查看进度'],
|
||||
success: (res) => {
|
||||
switch (res.tapIndex) {
|
||||
case 0:
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: '400-123-4567'
|
||||
})
|
||||
break
|
||||
case 1:
|
||||
uni.navigateTo({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
break
|
||||
case 2:
|
||||
goToDetail(workcase)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 创建工单
|
||||
function createWorkcase() {
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './list.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,284 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f4f5f7;
|
||||
}
|
||||
|
||||
.nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 176rpx;
|
||||
padding-top: 88rpx;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-left: 24rpx;
|
||||
padding-right: 24rpx;
|
||||
box-sizing: border-box;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-left: 4rpx solid #222;
|
||||
border-bottom: 4rpx solid #222;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.nav-capsule {
|
||||
width: 174rpx;
|
||||
height: 64rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 176rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
padding-bottom: 60rpx;
|
||||
min-height: calc(100vh - 176rpx);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
width: 6rpx;
|
||||
height: 28rpx;
|
||||
background: #173294;
|
||||
margin-right: 12rpx;
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.info-item.column {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 160rpx;
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #222;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.info-desc {
|
||||
font-size: 26rpx;
|
||||
color: #222;
|
||||
line-height: 1.6;
|
||||
margin-top: 8rpx;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fff7e6;
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.status-pending .tag-text {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background: #e7f7ea;
|
||||
color: #3abe59;
|
||||
}
|
||||
|
||||
.status-processing .tag-text {
|
||||
color: #3abe59;
|
||||
}
|
||||
|
||||
.status-done {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.status-done .tag-text {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tag-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.level-tag {
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.level-tag.urgent {
|
||||
background: #fff1f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.level-tag.urgent .level-text {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.level-tag.normal {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.level-tag.normal .level-text {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.level-text {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.photo-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.photo-item {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.photo-item image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.timeline-item:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
background: #d0d5dd;
|
||||
border-radius: 50%;
|
||||
margin-right: 20rpx;
|
||||
margin-top: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.timeline-dot.active {
|
||||
background: #173294;
|
||||
}
|
||||
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
left: 6rpx;
|
||||
top: 28rpx;
|
||||
width: 4rpx;
|
||||
height: calc(100% - 20rpx);
|
||||
background: #e5ebff;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.timeline-time {
|
||||
font-size: 26rpx;
|
||||
color: #173294;
|
||||
font-weight: 600;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.timeline-date {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #222;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.timeline-desc {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<!-- #ifdef APP -->
|
||||
<scroll-view style="flex:1">
|
||||
<!-- #endif -->
|
||||
<view class="page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<text class="nav-back-icon">←</text>
|
||||
</view>
|
||||
<text class="nav-title">工单详情</text>
|
||||
<view class="nav-capsule"></view>
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<scroll-view class="content" scroll-y="true" :style="{ marginTop: headerTotalHeight + 'px' }">
|
||||
<!-- 工单信息 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<view class="title-bar"></view>
|
||||
<text class="title-text">工单信息</text>
|
||||
</view>
|
||||
<view class="info-card">
|
||||
<view class="info-item">
|
||||
<text class="info-label">工单号</text>
|
||||
<text class="info-value">{{ workcase.workcaseId }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">工单状态</text>
|
||||
<view class="status-tag" :class="getStatusClass(workcase.status)">
|
||||
<text class="tag-text">{{ getStatusText(workcase.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">客户姓名</text>
|
||||
<text class="info-value">{{ workcase.username || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">联系电话</text>
|
||||
<text class="info-value">{{ workcase.phone || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">紧急程度</text>
|
||||
<view class="level-tag" :class="workcase.emergency === 'emergency' ? 'urgent' : 'normal'">
|
||||
<text class="level-text">{{ workcase.emergency === 'emergency' ? '紧急' : '普通' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">设备型号</text>
|
||||
<text class="info-value">{{ workcase.device || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">设备序列号</text>
|
||||
<text class="info-value">{{ workcase.deviceCode || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 故障信息 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<view class="title-bar"></view>
|
||||
<text class="title-text">故障信息</text>
|
||||
</view>
|
||||
<view class="info-card">
|
||||
<view class="info-item">
|
||||
<text class="info-label">故障类型</text>
|
||||
<text class="info-value">{{ workcase.type || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item column">
|
||||
<text class="info-label">故障描述</text>
|
||||
<text class="info-desc">{{ workcase.remark || '暂无描述' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">创建时间</text>
|
||||
<text class="info-value">{{ workcase.createTime || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">处理人</text>
|
||||
<text class="info-value">{{ workcase.processor || '待分配' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 故障图片 -->
|
||||
<view class="section" v-if="workcase.imgs && workcase.imgs.length > 0">
|
||||
<view class="section-title">
|
||||
<view class="title-bar"></view>
|
||||
<text class="title-text">故障图片</text>
|
||||
</view>
|
||||
<view class="photo-list">
|
||||
<view class="photo-item" v-for="(img, index) in workcase.imgs" :key="index">
|
||||
<image :src="img" mode="aspectFill" @tap="previewImage(img)" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 处理记录 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<view class="title-bar"></view>
|
||||
<text class="title-text">处理记录</text>
|
||||
</view>
|
||||
<view class="timeline">
|
||||
<view class="timeline-item" v-for="(item, index) in processList" :key="index">
|
||||
<view class="timeline-dot" :class="{ active: index === 0 }"></view>
|
||||
<view class="timeline-line" v-if="index < processList.length - 1"></view>
|
||||
<view class="timeline-content">
|
||||
<view class="timeline-header">
|
||||
<text class="timeline-time">{{ item.time }}</text>
|
||||
<text class="timeline-date">{{ item.date }}</text>
|
||||
</view>
|
||||
<text class="timeline-title">{{ item.title }}</text>
|
||||
<text class="timeline-desc" v-if="item.desc">{{ item.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- #ifdef APP -->
|
||||
</scroll-view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { TbWorkcaseDTO } from '@/types/workcase'
|
||||
|
||||
// 接口定义
|
||||
interface ProcessItem {
|
||||
time: string
|
||||
date: string
|
||||
title: string
|
||||
desc?: string
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const headerPaddingTop = ref<number>(44)
|
||||
const headerTotalHeight = ref<number>(88)
|
||||
const workcaseId = ref<string>('')
|
||||
|
||||
// 工单数据
|
||||
const workcase = ref<TbWorkcaseDTO>({
|
||||
workcaseId: 'TH20241217001',
|
||||
userId: '1',
|
||||
username: '李经理',
|
||||
phone: '13800138001',
|
||||
type: '控制系统故障',
|
||||
device: 'TH-500GF',
|
||||
deviceCode: 'TH20230501001',
|
||||
emergency: 'emergency',
|
||||
status: 'processing',
|
||||
processor: '张三',
|
||||
remark: '发电机组无法启动,控制面板显示E03错误代码',
|
||||
createTime: '2024-12-17 15:30:00',
|
||||
imgs: []
|
||||
})
|
||||
|
||||
// 处理记录
|
||||
const processList = ref<ProcessItem[]>([
|
||||
{ time: '16:45', date: '2024-12-17', title: '更换控制器主板', desc: '' },
|
||||
{ time: '16:30', date: '2024-12-17', title: '发现控制器主板故障', desc: '经检测,主板供电模块损坏' },
|
||||
{ time: '16:15', date: '2024-12-17', title: '到达现场,开始检查设备', desc: '' },
|
||||
{ time: '15:45', date: '2024-12-17', title: '工程师张三已接单', desc: '' },
|
||||
{ time: '15:30', date: '2024-12-17', title: '工单已创建', desc: '' }
|
||||
])
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
|
||||
headerPaddingTop.value = menuButtonInfo.top
|
||||
headerTotalHeight.value = menuButtonInfo.bottom + 8
|
||||
} catch (e) {
|
||||
headerPaddingTop.value = res.statusBarHeight || 44
|
||||
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
|
||||
}
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
headerPaddingTop.value = res.statusBarHeight || 44
|
||||
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
|
||||
// #endif
|
||||
}
|
||||
})
|
||||
|
||||
// 获取页面参数
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1] as any
|
||||
if (currentPage && currentPage.options && currentPage.options.workcaseId) {
|
||||
workcaseId.value = currentPage.options.workcaseId
|
||||
loadWorkcaseDetail(workcaseId.value)
|
||||
}
|
||||
})
|
||||
|
||||
// 加载工单详情
|
||||
function loadWorkcaseDetail(id: string) {
|
||||
console.log('加载工单详情:', id)
|
||||
// TODO: 调用 workcaseAPI.getWorkcaseById(id) 获取数据
|
||||
}
|
||||
|
||||
// 获取状态样式类
|
||||
function getStatusClass(status?: string): string {
|
||||
switch (status) {
|
||||
case 'pending': return 'status-pending'
|
||||
case 'processing': return 'status-processing'
|
||||
case 'done': return 'status-done'
|
||||
default: return 'status-pending'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status?: string): string {
|
||||
switch (status) {
|
||||
case 'pending': return '待处理'
|
||||
case 'processing': return '处理中'
|
||||
case 'done': return '已完成'
|
||||
default: return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
function previewImage(url: string) {
|
||||
uni.previewImage({
|
||||
urls: workcase.value.imgs || [],
|
||||
current: url
|
||||
})
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./workcaseDetail.scss";
|
||||
</style>
|
||||
@@ -0,0 +1,289 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f4f5f7;
|
||||
}
|
||||
|
||||
.nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 176rpx;
|
||||
padding-top: 88rpx;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding-left: 24rpx;
|
||||
padding-right: 24rpx;
|
||||
box-sizing: border-box;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-left: 4rpx solid #222;
|
||||
border-bottom: 4rpx solid #222;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding-right: 174rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nav-capsule {
|
||||
width: 174rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
position: fixed;
|
||||
// top: 176rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100rpx;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
font-size: 30rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #173294;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-line {
|
||||
width: 48rpx;
|
||||
height: 6rpx;
|
||||
background: #173294;
|
||||
border-radius: 3rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-text {
|
||||
color: #173294;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 276rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
padding-bottom: 60rpx;
|
||||
box-sizing: border-box;
|
||||
min-height: calc(100vh - 276rpx);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
width: 6rpx;
|
||||
height: 28rpx;
|
||||
background: #173294;
|
||||
margin-right: 12rpx;
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fff7e6;
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.status-pending .tag-text {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background: #e7f7ea;
|
||||
color: #3abe59;
|
||||
}
|
||||
|
||||
.status-processing .tag-text {
|
||||
color: #3abe59;
|
||||
}
|
||||
|
||||
.status-done {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.status-done .tag-text {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tag-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.fault-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.fault-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.level-tag {
|
||||
font-size: 24rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.level-tag.urgent {
|
||||
background: #fff1f0;
|
||||
color: #ff4d4f;
|
||||
border: 1rpx solid rgba(255,77,79,0.3);
|
||||
}
|
||||
|
||||
.level-tag.urgent .level-text {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.level-tag.normal {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
border: 1rpx solid rgba(24,144,255,0.3);
|
||||
}
|
||||
|
||||
.level-tag.normal .level-text {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.level-text {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 140rpx;
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 24rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.create-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
padding: 12rpx 24rpx;
|
||||
border: 2rpx solid #173294;
|
||||
border-radius: 32rpx;
|
||||
font-size: 24rpx;
|
||||
color: #173294;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-text {
|
||||
font-size: 24rpx;
|
||||
color: #173294;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<!-- #ifdef APP -->
|
||||
<scroll-view style="flex:1">
|
||||
<!-- #endif -->
|
||||
<view class="page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<text class="nav-back-icon">←</text>
|
||||
</view>
|
||||
<text class="nav-title">我的工单</text>
|
||||
<view class="nav-capsule"></view>
|
||||
</view>
|
||||
|
||||
<!-- Tab切换 -->
|
||||
<view class="tabs" :style="{ marginTop: headerTotalHeight + 'px' }">
|
||||
<view class="tab-item" :class="{ active: activeTab === 'all' }" @tap="changeTab('all')">
|
||||
<text class="tab-text">全部</text>
|
||||
</view>
|
||||
<view class="tab-item" :class="{ active: activeTab === 'pending' }" @tap="changeTab('pending')">
|
||||
<text class="tab-text">待处理</text>
|
||||
</view>
|
||||
<view class="tab-item" :class="{ active: activeTab === 'processing' }" @tap="changeTab('processing')">
|
||||
<text class="tab-text">处理中</text>
|
||||
</view>
|
||||
<view class="tab-item" :class="{ active: activeTab === 'done' }" @tap="changeTab('done')">
|
||||
<text class="tab-text">已完成</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 工单列表 -->
|
||||
<scroll-view class="list" scroll-y="true">
|
||||
<view class="card" v-for="(item, index) in filteredOrders" :key="index">
|
||||
<view class="card-header">
|
||||
<view class="card-title">
|
||||
<view class="title-bar"></view>
|
||||
<text class="title-text">工单号:{{ item.workcaseId }}</text>
|
||||
</view>
|
||||
<view class="status-tag" :class="getStatusClass(item.status)">
|
||||
<text class="tag-text">{{ getStatusText(item.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-body">
|
||||
<view class="fault-row">
|
||||
<text class="fault-title">{{ item.type || '故障报修' }}</text>
|
||||
<view class="level-tag" :class="item.emergency === 'emergency' ? 'urgent' : 'normal'">
|
||||
<text class="level-text">{{ item.emergency === 'emergency' ? '紧急' : '普通' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">设备型号</text>
|
||||
<text class="info-value">{{ item.device || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">联系人</text>
|
||||
<text class="info-value">{{ item.username || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">联系电话</text>
|
||||
<text class="info-value">{{ item.phone || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-footer">
|
||||
<text class="create-time">{{ item.createTime || '' }}</text>
|
||||
<view class="detail-btn" @tap="goDetail(item.workcaseId)">
|
||||
<text class="detail-text">查看详情</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="filteredOrders.length === 0">
|
||||
<text class="empty-text">暂无工单数据</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- #ifdef APP -->
|
||||
</scroll-view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import type { TbWorkcaseDTO } from '@/types/workcase'
|
||||
|
||||
// 响应式数据
|
||||
const headerPaddingTop = ref<number>(44)
|
||||
const headerTotalHeight = ref<number>(88)
|
||||
const activeTab = ref<string>('all')
|
||||
|
||||
// 模拟工单数据
|
||||
const orders = ref<TbWorkcaseDTO[]>([
|
||||
{
|
||||
workcaseId: 'TH20241217001',
|
||||
userId: '1',
|
||||
username: '李经理',
|
||||
phone: '13800138001',
|
||||
type: '控制系统故障',
|
||||
device: 'TH-500GF',
|
||||
deviceCode: 'TH20230501001',
|
||||
emergency: 'emergency',
|
||||
status: 'processing',
|
||||
createTime: '2024-12-17 15:30:00'
|
||||
},
|
||||
{
|
||||
workcaseId: 'TH20241217002',
|
||||
userId: '2',
|
||||
username: '王工',
|
||||
phone: '13800138002',
|
||||
type: '发动机故障',
|
||||
device: 'TH-300GF',
|
||||
deviceCode: 'TH20230502001',
|
||||
emergency: 'normal',
|
||||
status: 'pending',
|
||||
createTime: '2024-12-17 14:20:00'
|
||||
},
|
||||
{
|
||||
workcaseId: 'TH20241216001',
|
||||
userId: '3',
|
||||
username: '张总',
|
||||
phone: '13800138003',
|
||||
type: '电气系统故障',
|
||||
device: 'TH-800GF',
|
||||
deviceCode: 'TH20230503001',
|
||||
emergency: 'normal',
|
||||
status: 'done',
|
||||
createTime: '2024-12-16 09:15:00'
|
||||
}
|
||||
])
|
||||
|
||||
// 计算属性:根据tab筛选工单
|
||||
const filteredOrders = computed(() => {
|
||||
if (activeTab.value === 'all') {
|
||||
return orders.value
|
||||
}
|
||||
return orders.value.filter(o => o.status === activeTab.value)
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
|
||||
headerPaddingTop.value = menuButtonInfo.top
|
||||
headerTotalHeight.value = menuButtonInfo.bottom + 8
|
||||
} catch (e) {
|
||||
headerPaddingTop.value = res.statusBarHeight || 44
|
||||
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
|
||||
}
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
headerPaddingTop.value = res.statusBarHeight || 44
|
||||
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
|
||||
// #endif
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: 实际调用API获取工单列表
|
||||
loadWorkcaseList()
|
||||
})
|
||||
|
||||
// 加载工单列表
|
||||
function loadWorkcaseList() {
|
||||
// TODO: 调用 workcaseAPI.getWorkcaseList() 获取数据
|
||||
console.log('加载工单列表')
|
||||
}
|
||||
|
||||
// 切换Tab
|
||||
function changeTab(tab: string) {
|
||||
activeTab.value = tab
|
||||
}
|
||||
|
||||
// 获取状态样式类
|
||||
function getStatusClass(status?: string): string {
|
||||
switch (status) {
|
||||
case 'pending': return 'status-pending'
|
||||
case 'processing': return 'status-processing'
|
||||
case 'done': return 'status-done'
|
||||
default: return 'status-pending'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status?: string): string {
|
||||
switch (status) {
|
||||
case 'pending': return '待处理'
|
||||
case 'processing': return '处理中'
|
||||
case 'done': return '已完成'
|
||||
default: return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
// 跳转到工单详情
|
||||
function goDetail(workcaseId?: string) {
|
||||
if (!workcaseId) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/workcase/workcaseDetail/workcaseDetail?workcaseId=${workcaseId}`
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./workcaseList.scss";
|
||||
</style>
|
||||
Reference in New Issue
Block a user