微信修改

This commit is contained in:
2025-12-22 19:16:53 +08:00
parent ae16757984
commit cfb160cf09
70 changed files with 4697 additions and 1839 deletions

View File

@@ -0,0 +1,72 @@
import { api } from '@/api/index'
import type { TbGuestDTO } from '@/types/sys/guest'
import type { LoginParam, LoginDomain } from '@/types/auth/auth'
import type { ResultDomain, PageRequest } from '@/types'
/**
* 来客 API
* 通过 Gateway (8180) 访问 System Service
* 路由规则:/urban-lifeline/system/** → system-service
*/
export const guestAPI = {
baseUrl: '/urban-lifeline/system/guest',
/**
* 创建来客
*/
async createGuest(guest: TbGuestDTO): Promise<ResultDomain<TbGuestDTO>> {
const response = await api.post<TbGuestDTO>(`${this.baseUrl}`, guest)
return response.data
},
/**
* 更新来客
*/
async updateGuest(guest: TbGuestDTO): Promise<ResultDomain<TbGuestDTO>> {
const response = await api.put<TbGuestDTO>(`${this.baseUrl}`, guest)
return response.data
},
/**
* 删除来客
*/
async deleteGuest(userId: string): Promise<ResultDomain<TbGuestDTO>> {
const response = await api.delete<TbGuestDTO>(`${this.baseUrl}`, { params: { userId } })
return response.data
},
/**
* 根据微信ID查询来客
*/
async selectGuestByWechat(wechatId: string): Promise<ResultDomain<TbGuestDTO>> {
const response = await api.get<TbGuestDTO>(`${this.baseUrl}/wechat/${wechatId}`)
return response.data
},
/**
* 获取来客列表
*/
async listGuest(filter?: TbGuestDTO): Promise<ResultDomain<TbGuestDTO>> {
const response = await api.get<TbGuestDTO>(`${this.baseUrl}/list`, { params: filter })
return response.data
},
/**
* 分页查询来客
*/
async pageGuest(pageRequest: PageRequest<TbGuestDTO>): Promise<ResultDomain<TbGuestDTO>> {
const response = await api.post<TbGuestDTO>(`${this.baseUrl}/page`, pageRequest)
return response.data
},
/**
* 微信小程序用户识别登录
* 优先尝试员工登录,失败则自动注册/查询来客
* @param loginParam 登录参数wechatId或phone必填
* @returns LoginDomain 包含用户信息和token
*/
async identify(loginParam: LoginParam): Promise<ResultDomain<LoginDomain>> {
const response = await api.post<LoginDomain>(`${this.baseUrl}/identify`, loginParam)
return response.data
}
}

View File

@@ -4,16 +4,26 @@
// LoginParam - 登录参数
export interface LoginParam {
/** 登录用户名或邮箱或手机 */
/** 登录用户名 */
username?: string
/** 登录密码 */
password?: string
/** 邮箱 */
email?: string
/** 手机号 */
phone?: string
/** 微信ID */
wechatId?: string
/** 验证码类型 */
captchaType?: string
/** 验证码 */
captcha?: string
/** 验证码ID */
captchaId?: string
/** 登录方式password/captcha/oauth */
/** 登录方式password/captcha/oauth/wechat_miniprogram */
loginType?: string
/** 是否记住我 */
rememberMe?: boolean
}
// LoginDomain - 登录信息

View File

@@ -0,0 +1,17 @@
import { BaseDTO } from '@/types/base'
/**
* 来客DTO - 根据后端 TbGuestDTO 转换
*/
export interface TbGuestDTO extends BaseDTO {
/** 来客ID */
userId?: string
/** 姓名 */
name?: string
/** 电话 */
phone?: string
/** 邮箱 */
email?: string
/** 微信ID */
wechatId?: string
}

View File

@@ -1,3 +1,4 @@
export * from "./config"
export * from "./guest"
export * from "./permission"
export * from "./user"

View File

@@ -5,6 +5,8 @@
export default {
onLaunch: function () {
console.log('App Launch')
// 检查是否已选择模式
this.checkModeSelection()
},
onShow: function () {
console.log('App Show')
@@ -33,6 +35,50 @@
onExit: function () {
console.log('App Exit')
},
methods: {
// 检查并选择模式
checkModeSelection() {
const mode = uni.getStorageSync('userMode')
if (!mode) {
this.showModeSelector()
}
},
// 显示模式选择器
showModeSelector() {
uni.showActionSheet({
itemList: ['员工模式 (17857100375)', '访客模式 (17857100376)'],
success: (res) => {
let wechatId = ''
let userMode = ''
let phone = ''
if (res.tapIndex === 0) {
wechatId = '17857100375'
phone = '17857100375'
userMode = 'staff'
} else {
wechatId = '17857100376'
phone = '17857100376'
userMode = 'guest'
}
// 存储选择
uni.setStorageSync('userMode', userMode)
uni.setStorageSync('wechatId', wechatId)
uni.setStorageSync('phone', phone)
console.log('已选择模式:', userMode, 'wechatId:', wechatId)
uni.showToast({
title: userMode === 'staff' ? '员工模式' : '访客模式',
icon: 'success'
})
},
fail: () => {
// 用户取消,默认使用访客模式
uni.setStorageSync('userMode', 'guest')
uni.setStorageSync('wechatId', '17857100376')
console.log('默认使用访客模式')
}
})
}
}
}
</script>

View File

@@ -0,0 +1,49 @@
/**
* workcase_wechat API 封装
* 使用 uni.request 替代 axios
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
declare const uni: {
getStorageSync: (key: string) => any
request: (options: any) => void
}
import type { ResultDomain } from '../types'
// API 基础配置
const BASE_URL = 'http://localhost:8180'
// 通用请求方法
export function request<T>(options: {
url: string
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
data?: any
header?: Record<string, string>
}): Promise<ResultDomain<T>> {
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('token') as string
uni.request({
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data,
header: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
...options.header
},
success: (res: any) => {
if (res.statusCode === 200) {
resolve(res.data as ResultDomain<T>)
} else {
reject(new Error(`请求失败: ${res.statusCode}`))
}
},
fail: (err: any) => {
reject(err)
}
})
})
}

View File

@@ -0,0 +1,3 @@
export * from "./base"
export * from "./sys"
export * from "./workcase"

View File

@@ -0,0 +1,25 @@
import { request } from '../base'
import type { LoginParam, ResultDomain, LoginDomain, TbGuestDTO } from '../../types'
// 来客 API
export const guestAPI = {
/**
* 微信小程序用户识别登录
*/
identify(loginParam: LoginParam): Promise<ResultDomain<LoginDomain>> {
return request<LoginDomain>({
url: '/urban-lifeline/system/guest/identify',
method: 'POST',
data: loginParam
})
},
/**
* 根据微信ID查询来客
*/
selectGuestByWechat(wechatId: string): Promise<ResultDomain<TbGuestDTO>> {
return request<TbGuestDTO>({
url: `/urban-lifeline/system/guest/wechat/${wechatId}`,
method: 'GET'
})
}
}

View File

@@ -0,0 +1 @@
export * from './guest'

View File

@@ -0,0 +1,2 @@
export * from './workcase'
export * from './workcaseChat'

View File

@@ -0,0 +1,167 @@
import { request } from '../base'
import type { ResultDomain, PageRequest } from '../../types'
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO, TbWorkcaseDeviceDTO } from '../../types/workcase'
/**
* @description 工单管理接口
* @filename workcase.ts
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
export const workcaseAPI = {
baseUrl: '/urban-lifeline/workcase',
// ========================= 工单管理 =========================
/**
* 创建工单
* @param workcase 工单信息
*/
createWorkcase(workcase: TbWorkcaseDTO): Promise<ResultDomain<TbWorkcaseDTO>> {
return request<TbWorkcaseDTO>({ url: this.baseUrl, method: 'POST', data: workcase })
},
/**
* 更新工单
* @param workcase 工单信息
*/
updateWorkcase(workcase: TbWorkcaseDTO): Promise<ResultDomain<TbWorkcaseDTO>> {
return request<TbWorkcaseDTO>({ url: this.baseUrl, method: 'PUT', data: workcase })
},
/**
* 删除工单
* @param workcaseId 工单ID
*/
deleteWorkcase(workcaseId: string): Promise<ResultDomain<TbWorkcaseDTO>> {
return request<TbWorkcaseDTO>({ url: `${this.baseUrl}/${workcaseId}`, method: 'DELETE' })
},
/**
* 获取工单详情
* @param workcaseId 工单ID
*/
getWorkcaseById(workcaseId: string): Promise<ResultDomain<TbWorkcaseDTO>> {
return request<TbWorkcaseDTO>({ url: `${this.baseUrl}/${workcaseId}`, method: 'GET' })
},
/**
* 查询工单列表
* @param filter 筛选条件
*/
getWorkcaseList(filter?: TbWorkcaseDTO): Promise<ResultDomain<TbWorkcaseDTO>> {
return request<TbWorkcaseDTO>({ url: `${this.baseUrl}/list`, method: 'POST', data: filter || {} })
},
/**
* 分页查询工单
* @param pageRequest 分页请求
*/
getWorkcasePage(pageRequest: PageRequest<TbWorkcaseDTO>): Promise<ResultDomain<TbWorkcaseDTO>> {
return request<TbWorkcaseDTO>({ url: `${this.baseUrl}/page`, method: 'POST', data: pageRequest })
},
// ========================= CRM同步接口 =========================
/**
* 同步工单到CRM
* @param workcase 工单信息
*/
syncWorkcaseToCrm(workcase: TbWorkcaseDTO): Promise<ResultDomain<void>> {
return request<void>({ url: `${this.baseUrl}/sync/crm`, method: 'POST', data: workcase })
},
/**
* 接收CRM工单更新CRM回调
* @param jsonBody JSON字符串
*/
receiveWorkcaseFromCrm(jsonBody: string): Promise<ResultDomain<void>> {
return request<void>({ url: `${this.baseUrl}/receive/crm`, method: 'POST', data: jsonBody })
},
// ========================= 工单处理过程 =========================
/**
* 创建工单处理过程
* @param process 处理过程信息
*/
createWorkcaseProcess(process: TbWorkcaseProcessDTO): Promise<ResultDomain<TbWorkcaseProcessDTO>> {
return request<TbWorkcaseProcessDTO>({ url: `${this.baseUrl}/process`, method: 'POST', data: process })
},
/**
* 更新工单处理过程
* @param process 处理过程信息
*/
updateWorkcaseProcess(process: TbWorkcaseProcessDTO): Promise<ResultDomain<TbWorkcaseProcessDTO>> {
return request<TbWorkcaseProcessDTO>({ url: `${this.baseUrl}/process`, method: 'PUT', data: process })
},
/**
* 删除工单处理过程
* @param processId 处理过程ID
*/
deleteWorkcaseProcess(processId: string): Promise<ResultDomain<TbWorkcaseProcessDTO>> {
return request<TbWorkcaseProcessDTO>({ url: `${this.baseUrl}/process/${processId}`, method: 'DELETE' })
},
/**
* 查询工单处理过程列表
* @param filter 筛选条件
*/
getWorkcaseProcessList(filter?: TbWorkcaseProcessDTO): Promise<ResultDomain<TbWorkcaseProcessDTO>> {
return request<TbWorkcaseProcessDTO>({ url: `${this.baseUrl}/process/list`, method: 'POST', data: filter || {} })
},
/**
* 分页查询工单处理过程
* @param pageRequest 分页请求
*/
getWorkcaseProcessPage(pageRequest: PageRequest<TbWorkcaseProcessDTO>): Promise<ResultDomain<TbWorkcaseProcessDTO>> {
return request<TbWorkcaseProcessDTO>({ url: `${this.baseUrl}/process/page`, method: 'POST', data: pageRequest })
},
// ========================= 工单设备管理 =========================
/**
* 创建工单设备
* @param device 设备信息
*/
createWorkcaseDevice(device: TbWorkcaseDeviceDTO): Promise<ResultDomain<TbWorkcaseDeviceDTO>> {
return request<TbWorkcaseDeviceDTO>({ url: `${this.baseUrl}/device`, method: 'POST', data: device })
},
/**
* 更新工单设备
* @param device 设备信息
*/
updateWorkcaseDevice(device: TbWorkcaseDeviceDTO): Promise<ResultDomain<TbWorkcaseDeviceDTO>> {
return request<TbWorkcaseDeviceDTO>({ url: `${this.baseUrl}/device`, method: 'PUT', data: device })
},
/**
* 删除工单设备
* @param workcaseId 工单ID
* @param device 设备名称
*/
deleteWorkcaseDevice(workcaseId: string, device: string): Promise<ResultDomain<TbWorkcaseDeviceDTO>> {
return request<TbWorkcaseDeviceDTO>({ url: `${this.baseUrl}/device/${workcaseId}/${device}`, method: 'DELETE' })
},
/**
* 查询工单设备列表
* @param filter 筛选条件
*/
getWorkcaseDeviceList(filter?: TbWorkcaseDeviceDTO): Promise<ResultDomain<TbWorkcaseDeviceDTO>> {
return request<TbWorkcaseDeviceDTO>({ url: `${this.baseUrl}/device/list`, method: 'POST', data: filter || {} })
},
/**
* 分页查询工单设备
* @param pageRequest 分页请求
*/
getWorkcaseDevicePage(pageRequest: PageRequest<TbWorkcaseDeviceDTO>): Promise<ResultDomain<TbWorkcaseDeviceDTO>> {
return request<TbWorkcaseDeviceDTO>({ url: `${this.baseUrl}/device/page`, method: 'POST', data: pageRequest })
}
}

View File

@@ -0,0 +1,281 @@
import { request } from '../base'
import type { ResultDomain, PageRequest } from '../../types'
import type { TbWorkcaseDTO } from '../../types/workcase/workcase'
import type {
TbChatRoomDTO,
TbChatRoomMemberDTO,
TbChatRoomMessageDTO,
TbCustomerServiceDTO,
TbWordCloudDTO,
ChatRoomVO,
ChatMemberVO,
ChatRoomMessageVO,
CustomerServiceVO
} from '../../types/workcase/chatRoom'
// AI对话相关类型简化版
interface TbChat {
chatId?: string
userId?: string
title?: string
status?: string
}
interface TbChatMessage {
messageId?: string
chatId?: string
content?: string
role?: string
}
interface ChatPrepareData {
chatId?: string
message?: string
}
/**
* @description 工单对话相关接口
* @filename workcaseChat.ts
* @author cascade
* @copyright xyzh
* @since 2025-12-22
*/
export const workcaseChatAPI = {
baseUrl: '/urban-lifeline/workcase/chat',
// ====================== AI对话管理 ======================
/**
* 创建对话
*/
createChat(chat: TbChat): Promise<ResultDomain<TbChat>> {
return request<TbChat>({ url: this.baseUrl, method: 'POST', data: chat })
},
/**
* 更新对话
*/
updateChat(chat: TbChat): Promise<ResultDomain<TbChat>> {
return request<TbChat>({ url: this.baseUrl, method: 'PUT', data: chat })
},
/**
* 查询对话列表
*/
getChatList(filter: TbChat): Promise<ResultDomain<TbChat>> {
return request<TbChat>({ url: `${this.baseUrl}/list`, method: 'POST', data: filter })
},
/**
* 获取对话消息列表
*/
getChatMessageList(filter: TbChat): Promise<ResultDomain<TbChatMessage>> {
return request<TbChatMessage>({ url: `${this.baseUrl}/message/list`, method: 'POST', data: filter })
},
/**
* 准备对话会话
*/
prepareChatMessageSession(prepareData: ChatPrepareData): Promise<ResultDomain<string>> {
return request<string>({ url: `${this.baseUrl}/prepare`, method: 'POST', data: prepareData })
},
/**
* 流式对话SSE- 返回EventSource URL
*/
getStreamUrl(sessionId: string): string {
return `${this.baseUrl}/stream/${sessionId}`
},
/**
* 停止对话
*/
stopChat(filter: TbChat, taskId: string): Promise<ResultDomain<boolean>> {
return request<boolean>({ url: `${this.baseUrl}/stop/${taskId}`, method: 'POST', data: filter })
},
/**
* 评论对话消息
*/
commentChatMessage(filter: TbChat, messageId: string, comment: string): Promise<ResultDomain<boolean>> {
return request<boolean>({ url: `${this.baseUrl}/comment?messageId=${messageId}&comment=${comment}`, method: 'POST', data: filter })
},
// ====================== 对话分析 ======================
/**
* 分析对话AI预填工单信息
*/
analyzeChat(chatId: string): Promise<ResultDomain<TbWorkcaseDTO>> {
return request<TbWorkcaseDTO>({ url: `${this.baseUrl}/analyze/${chatId}`, method: 'GET' })
},
/**
* 总结对话
*/
summaryChat(chatId: string): Promise<ResultDomain<TbWorkcaseDTO>> {
return request<TbWorkcaseDTO>({ url: `${this.baseUrl}/summary/${chatId}`, method: 'POST' })
},
// ====================== ChatRoom聊天室管理 ======================
/**
* 创建聊天室
*/
createChatRoom(chatRoom: TbChatRoomDTO): Promise<ResultDomain<TbChatRoomDTO>> {
return request<TbChatRoomDTO>({ url: `${this.baseUrl}/room`, method: 'POST', data: chatRoom })
},
/**
* 更新聊天室
*/
updateChatRoom(chatRoom: TbChatRoomDTO): Promise<ResultDomain<TbChatRoomDTO>> {
return request<TbChatRoomDTO>({ url: `${this.baseUrl}/room`, method: 'PUT', data: chatRoom })
},
/**
* 关闭聊天室
*/
closeChatRoom(roomId: string, closedBy: string): Promise<ResultDomain<boolean>> {
return request<boolean>({ url: `${this.baseUrl}/room/${roomId}/close?closedBy=${closedBy}`, method: 'POST' })
},
/**
* 获取聊天室详情
*/
getChatRoomById(roomId: string): Promise<ResultDomain<TbChatRoomDTO>> {
return request<TbChatRoomDTO>({ url: `${this.baseUrl}/room/${roomId}`, method: 'GET' })
},
/**
* 分页查询聊天室
*/
getChatRoomPage(pageRequest: PageRequest<TbChatRoomDTO>): Promise<ResultDomain<ChatRoomVO>> {
return request<ChatRoomVO>({ url: `${this.baseUrl}/room/page`, method: 'POST', data: pageRequest })
},
// ====================== ChatRoom成员管理 ======================
/**
* 添加聊天室成员
*/
addChatRoomMember(member: TbChatRoomMemberDTO): Promise<ResultDomain<TbChatRoomMemberDTO>> {
return request<TbChatRoomMemberDTO>({ url: `${this.baseUrl}/room/member`, method: 'POST', data: member })
},
/**
* 移除聊天室成员
*/
removeChatRoomMember(memberId: string): Promise<ResultDomain<boolean>> {
return request<boolean>({ url: `${this.baseUrl}/room/member/${memberId}`, method: 'DELETE' })
},
/**
* 获取聊天室成员列表
*/
getChatRoomMemberList(roomId: string): Promise<ResultDomain<ChatMemberVO>> {
return request<ChatMemberVO>({ url: `${this.baseUrl}/room/${roomId}/members`, method: 'GET' })
},
// ====================== ChatRoom消息管理 ======================
/**
* 发送聊天室消息
*/
sendMessage(message: TbChatRoomMessageDTO): Promise<ResultDomain<TbChatRoomMessageDTO>> {
return request<TbChatRoomMessageDTO>({ url: `${this.baseUrl}/room/message`, method: 'POST', data: message })
},
/**
* 分页查询聊天室消息
*/
getChatMessagePage(pageRequest: PageRequest<TbChatRoomMessageDTO>): Promise<ResultDomain<ChatRoomMessageVO>> {
return request<ChatRoomMessageVO>({ url: `${this.baseUrl}/room/message/page`, method: 'POST', data: pageRequest })
},
/**
* 删除聊天室消息
*/
deleteMessage(messageId: string): Promise<ResultDomain<boolean>> {
return request<boolean>({ url: `${this.baseUrl}/room/message/${messageId}`, method: 'DELETE' })
},
// ====================== 客服人员管理 ======================
/**
* 添加客服人员
*/
addCustomerService(customerService: TbCustomerServiceDTO): Promise<ResultDomain<TbCustomerServiceDTO>> {
return request<TbCustomerServiceDTO>({ url: `${this.baseUrl}/customer-service`, method: 'POST', data: customerService })
},
/**
* 更新客服人员
*/
updateCustomerService(customerService: TbCustomerServiceDTO): Promise<ResultDomain<TbCustomerServiceDTO>> {
return request<TbCustomerServiceDTO>({ url: `${this.baseUrl}/customer-service`, method: 'PUT', data: customerService })
},
/**
* 删除客服人员
*/
deleteCustomerService(userId: string): Promise<ResultDomain<boolean>> {
return request<boolean>({ url: `${this.baseUrl}/customer-service/${userId}`, method: 'DELETE' })
},
/**
* 分页查询客服人员
*/
getCustomerServicePage(pageRequest: PageRequest<TbCustomerServiceDTO>): Promise<ResultDomain<CustomerServiceVO>> {
return request<CustomerServiceVO>({ url: `${this.baseUrl}/customer-service/page`, method: 'POST', data: pageRequest })
},
/**
* 更新客服在线状态
*/
updateCustomerServiceStatus(userId: string, status: string): Promise<ResultDomain<boolean>> {
return request<boolean>({ url: `${this.baseUrl}/customer-service/${userId}/status?status=${status}`, method: 'POST' })
},
/**
* 获取可接待客服列表
*/
getAvailableCustomerServices(): Promise<ResultDomain<CustomerServiceVO>> {
return request<CustomerServiceVO>({ url: `${this.baseUrl}/customer-service/available`, method: 'GET' })
},
/**
* 自动分配客服
*/
assignCustomerService(roomId: string): Promise<ResultDomain<CustomerServiceVO>> {
return request<CustomerServiceVO>({ url: `${this.baseUrl}/room/${roomId}/assign`, method: 'POST' })
},
// ====================== 词云管理 ======================
/**
* 添加词云
*/
addWordCloud(wordCloud: TbWordCloudDTO): Promise<ResultDomain<TbWordCloudDTO>> {
return request<TbWordCloudDTO>({ url: `${this.baseUrl}/wordcloud`, method: 'POST', data: wordCloud })
},
/**
* 更新词云
*/
updateWordCloud(wordCloud: TbWordCloudDTO): Promise<ResultDomain<TbWordCloudDTO>> {
return request<TbWordCloudDTO>({ url: `${this.baseUrl}/wordcloud`, method: 'PUT', data: wordCloud })
},
/**
* 查询词云列表
*/
getWordCloudList(filter: TbWordCloudDTO): Promise<ResultDomain<TbWordCloudDTO>> {
return request<TbWordCloudDTO>({ url: `${this.baseUrl}/wordcloud/list`, method: 'POST', data: filter })
},
/**
* 分页查询词云
*/
getWordCloudPage(pageRequest: PageRequest<TbWordCloudDTO>): Promise<ResultDomain<TbWordCloudDTO>> {
return request<TbWordCloudDTO>({ url: `${this.baseUrl}/wordcloud/page`, method: 'POST', data: pageRequest })
}
}

View File

@@ -1,6 +1,3 @@
{
"dependencies": {
"sockjs-client": "^1.6.1",
"@stomp/stompjs": "^7.2.1"
}
"dependencies": {}
}

View File

@@ -5,27 +5,39 @@
"style": {
"navigationStyle": "custom",
"backgroundColor": "#667eea",
"enablePullDownRefresh": false,
"safeAreaInsets": {
"top": true,
"bottom": false
}
"enablePullDownRefresh": false
}
},
{
"path": "pages/workcase/list",
// 聊天室列表
"path": "pages/chatRoom/chatRoomList/chatRoomList",
"style": {
"navigationBarTitleText": "我的工单",
"navigationBarBackgroundColor": "#667eea",
"navigationBarTextStyle": "white"
"navigationBarTitleText": ""
}
},
{
"path": "pages/workcase/detail",
// 聊天室
"path": "pages/chatRoom/chatRoom/chatRoom",
"style": {
"navigationBarTitleText": "工单详情",
"navigationBarBackgroundColor": "#667eea",
"navigationBarTextStyle": "white"
"navigationBarTitleText": ""
}
},
{
"path": "pages/workcase/workcaseList/workcaseList",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/workcase/workcaseDetail/workcaseDetail",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/meeting/Meeting/Meeting",
"style": {
"navigationBarTitleText": ""
}
}
],

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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)
}
// 显示上传选项

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -1,4 +1,6 @@
{
"cloudfunctionRoot": "",
"cloudbaseRoot": "",
"setting": {
"es6": true,
"postcss": true,

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="16"/>
<line x1="8" y1="12" x2="16" y2="12"/>
</svg>

After

Width:  |  Height:  |  Size: 277 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>

After

Width:  |  Height:  |  Size: 460 B

View File

@@ -0,0 +1,78 @@
/**
* 认证服务相关 types - 根据后端 VO 和 DTO 转换
*/
// LoginParam - 登录参数
export interface LoginParam {
/** 登录用户名 */
username?: string
/** 登录密码 */
password?: string
/** 邮箱 */
email?: string
/** 手机号 */
phone?: string
/** 微信ID */
wechatId?: string
/** 验证码类型 */
captchaType?: string
/** 验证码 */
captcha?: string
/** 验证码ID */
captchaId?: string
/** 登录方式password/captcha/oauth/wechat_miniprogram */
loginType?: string
/** 是否记住我 */
rememberMe?: boolean
}
// LoginDomain - 登录信息
import type {
TbSysUserDTO,
TbSysUserInfoDTO,
TbSysUserRoleDTO
} from '../sys/user'
import type {
TbSysDeptDTO,
TbSysPermissionDTO,
TbSysViewDTO
} from '../sys/permission'
/**
* 登录返回的领域对象
*/
export interface LoginDomain {
/** 用户基本信息 */
user?: TbSysUserDTO
/** 用户详细信息 */
userInfo?: TbSysUserInfoDTO
/** 用户角色列表 */
userRoles?: TbSysUserRoleDTO[]
/** 用户部门列表 */
userDepts?: TbSysDeptDTO[]
/** 用户权限列表 */
userPermissions?: TbSysPermissionDTO[]
/** 用户视图列表(视图即菜单,用于生成路由和侧边栏) */
userViews?: TbSysViewDTO[]
/** 访问令牌 */
token?: string
/** 令牌过期时间 */
tokenExpireTime?: string | Date
/** 登录时间 */
loginTime?: string
/** IP地址 */
ipAddress?: string
/** 登录类型 */
loginType?: string
}

View File

@@ -0,0 +1 @@
export * from "./auth"

View File

@@ -0,0 +1,49 @@
/**
* 排序字段
*/
export interface OrderField {
/** 排序字段 */
field: string
/** 排序方式 */
order: 'ASC' | 'DESC'
}
/**
* 基础DTO - 包含所有数据传输对象的公共字段
*/
export interface BaseDTO {
/** 操作流水号 */
optsn?: string
/** 创建人 */
creator?: string
/** 更新人 */
updater?: string
/** 部门路径 */
deptPath?: string
/** 备注 */
remark?: string
/** 创建时间 */
createTime?: string
/** 更新时间 */
updateTime?: string
/** 删除时间 */
deleteTime?: string
/** 是否已删除 */
deleted?: boolean
/** 数量限制 */
limit?: number
/** 开始时间 */
startTime?: string
/** 结束时间 */
endTime?: string
/** 排序字段列表 */
orderFields?: OrderField[]
}
/**
* 基础VO - 包含所有实体的公共字段
*/
export interface BaseVO extends BaseDTO {
/** 主键ID */
id?: string
}

View File

@@ -0,0 +1,6 @@
export * from "./base"
export * from "./sys"
export * from "./workcase"
export * from "./auth"
export * from "./response"
export * from "./page"

View File

@@ -0,0 +1,28 @@
/**
* 分页参数
*/
export interface PageParam {
/** 当前页码 */
pageNumber: number;
/** 每页条数 */
pageSize: number;
/** 总页数 */
totalPages?: number;
/** 总记录数 */
totalElements?: number;
}
/**
* 分页结果
*/
export interface PageDomain<T> {
pageParam: PageParam;
/** 数据列表 */
dataList?: T[];
}
export interface PageRequest<T> {
pageParam: PageParam;
filter: T;
}

View File

@@ -0,0 +1,21 @@
import type { PageParam, PageDomain } from "../page";
export interface ResultDomain<T>{
/** 状态码 */
code: number;
/** 返回消息 */
message: string;
/** 操作是否成功 */
success: boolean;
/** 是否登录 */
login: boolean;
/** 是否有权限 */
auth: boolean;
/** 返回数据 */
data?: T;
/** 返回数据列表 */
dataList?: T[];
/** 分页参数 */
pageParam?: PageParam;
/** 分页信息 */
pageDomain?: PageDomain<T>;
}

View File

@@ -0,0 +1,60 @@
import { BaseVO, BaseDTO } from "../base";
export interface SysConfigVO extends BaseVO {
/** 配置ID */
configId?: string;
/** 配置键 */
key?: string;
/** 配置名称 */
name?: string;
/** 配置值 */
value?: string;
/** 数据类型(String, Integer, Boolean, Float, Double) */
configType?: string;
/** 配置渲染类型(select, input, textarea, checkbox, radio, switch) */
renderType?: string;
/** 配置描述 */
description?: string;
/** 正则表达式校验规则(JSON) */
re?: Record<string, any>;
/** 可选项(JSON)render_type为select、checkbox、radio时使用 */
options?: Record<string, any>;
/** 配置组 */
group?: string;
/** 模块ID */
moduleId?: string;
/** 配置顺序 */
orderNum?: number;
/** 状态 */
status?: number;
}
// TbSysConfigDTO - 系统配置DTO创建和更新
export interface TbSysConfigDTO extends BaseDTO {
/** 配置ID */
configId?: string;
/** 配置键 */
key?: string;
/** 配置名称 */
name?: string;
/** 配置值 */
value?: string;
/** 数据类型(String, Integer, Boolean, Float, Double) */
configType?: string;
/** 配置渲染类型(select, input, textarea, checkbox, radio, switch) */
renderType?: string;
/** 配置描述 */
description?: string;
/** 正则表达式校验规则(JSON) */
re?: Record<string, any>;
/** 可选项(JSON)render_type为select、checkbox、radio时使用 */
options?: Record<string, any>;
/** 配置组 */
group?: string;
/** 模块ID */
moduleId?: string;
/** 配置顺序 */
orderNum?: number;
/** 配置状态 0:启用 1:禁用 */
status?: number;
}

View File

@@ -0,0 +1,17 @@
import { BaseDTO } from '../base'
/**
* 来客DTO - 根据后端 TbGuestDTO 转换
*/
export interface TbGuestDTO extends BaseDTO {
/** 来客ID */
userId?: string
/** 姓名 */
name?: string
/** 电话 */
phone?: string
/** 邮箱 */
email?: string
/** 微信ID */
wechatId?: string
}

View File

@@ -0,0 +1,4 @@
export * from "./config"
export * from "./guest"
export * from "./permission"
export * from "./user"

View File

@@ -0,0 +1,247 @@
import { BaseVO, BaseDTO } from "../base";
export interface PermissionVO extends BaseVO {
/** 部门ID */
deptId?: string;
/** 部门名称 */
deptName?: string;
/** 父级部门ID */
deptParentId?: string;
/** 部门描述 */
deptDescription?: string;
/** 角色ID */
roleId?: string;
/** 角色名称 */
roleName?: string;
/** 角色描述 */
roleDescription?: string;
/** 角色作用域 */
roleScope?: string;
/** 所属部门ID */
roleOwnerDeptId?: string;
/** 角色状态 */
roleStatus?: boolean;
/** 模块ID */
moduleId?: string;
/** 模块名称 */
moduleName?: string;
/** 模块描述 */
moduleDescription?: string;
/** 权限ID */
permissionId?: string;
/** 权限名称 */
permissionName?: string;
/** 权限代码 */
permissionCode?: string;
/** 权限描述 */
permissionDescription?: string;
/** 权限状态 */
permissionStatus?: string;
/** 视图ID */
viewId?: string;
/** 视图名称 */
viewName?: string;
/** 父视图ID */
viewParentId?: string;
/** URL */
viewUrl?: string;
/** 组件 */
viewComponent?: string;
/** 图标 */
viewIcon?: string;
/** 类型 */
viewType?: number;
/** 布局 */
viewLayout?: string;
/** 排序 */
viewOrderNum?: number;
/** 视图描述 */
viewDescription?: string;
/** 用户视图权限列表 */
permissionIdList?: string[];
}
// TbSysDeptDTO - 系统部门DTO
export interface TbSysDeptDTO extends BaseDTO {
/** 部门ID */
deptId?: string;
/** 部门名称 */
name?: string;
/** 父级部门ID */
parentId?: string;
/** 部门描述 */
description?: string;
}
// TbSysDeptRoleDTO - 系统部门角色关系DTO
export interface TbSysDeptRoleDTO extends BaseDTO {
/** 部门ID */
deptId?: string;
/** 角色ID */
roleId?: string;
}
// TbSysRoleDTO - 系统角色DTO
export interface TbSysRoleDTO extends BaseDTO {
/** 角色ID */
roleId?: string;
/** 角色名称 */
name?: string;
/** 角色描述 */
description?: string;
/** 角色作用域 global 全局角色, dept 部门角色 */
scope?: string;
/** 所属部门ID */
ownerDeptId?: string;
/** 角色状态 true 有效, false 无效 */
status?: boolean;
}
// TbSysModuleDTO - 系统模块DTO
export interface TbSysModuleDTO extends BaseDTO {
/** 模块ID */
moduleId?: string;
/** 模块名称 */
name?: string;
/** 模块描述 */
description?: string;
}
// TbSysViewDTO - 系统视图DTO
export interface TbSysViewDTO extends BaseDTO {
/** 视图ID */
viewId?: string;
/** 视图名称 */
name?: string;
/** 父视图ID */
parentId?: string;
/** URL */
url?: string;
/** 组件 */
component?: string;
/** iframe URL */
iframeUrl?: string;
/** 图标 */
icon?: string;
/** 类型 */
type?: number;
/** 视图类型 route\iframe*/
viewType?: string;
/** 所属服务 platform\workcase\bidding */
service?: string;
/** 布局 */
layout?: string;
/** 排序 */
orderNum?: number;
/** 描述 */
description?: string;
/** 子视图列表(用于构建树形结构) */
children?: TbSysViewDTO[];
}
// TbSysPermissionDTO - 系统权限DTO
export interface TbSysPermissionDTO extends BaseDTO {
/** 权限ID */
permissionId?: string;
/** 权限名称 */
name?: string;
/** 权限代码 */
code?: string;
/** 权限描述 */
description?: string;
/** 模块ID */
moduleId?: string;
/** 状态 */
status?: string;
}
// TbSysRolePermissionDTO - 系统角色权限关系DTO
export interface TbSysRolePermissionDTO extends BaseDTO {
/** 角色ID */
roleId?: string;
/** 权限ID */
permissionId?: string;
}
// TbSysViewPermissionDTO - 系统视图权限关系DTO
export interface TbSysViewPermissionDTO extends BaseDTO {
/** 视图ID */
viewId?: string;
/** 权限ID */
permissionId?: string;
}
// =============== 数据权限 ===============
export interface AclVO extends BaseVO {
/** 权限ID */
aclId?: string;
/** 对象类型article/file/course/... */
objectType?: string;
/** 对象ID */
objectId?: string;
/** 主体类型user/dept/role */
principalType?: string;
/** 主体ID */
principalId?: string;
/** 当主体为role且限定到某部门时的部门ID支持某部门的某角色 */
principalDeptId?: string;
/** 权限位1读 2写 4执行 */
permission?: number;
/** 允许或显式拒绝 */
allow?: boolean;
/** 是否包含子级对dept/role生效 */
includeDescendants?: boolean;
/** 策略ID */
policyId?: string;
/** 策略名称 */
policyName?: string;
/** 对象类型article/file/course/.. */
policyObjectType?: string;
/** 编辑层级规则parent_only/parent_or_same_admin/owner_only/none */
editHierarchyRule?: string;
/** 可见层级规则 children_all/children_specified/none */
viewHierarchyRule?: string;
}
// TbSysAclDTO - 系统访问控制列表DTO创建和更新
export interface TbSysAclDTO extends BaseDTO {
/** 权限ID */
aclId?: string;
/** 对象类型article/file/course/... */
objectType?: string;
/** 对象ID */
objectId?: string;
/** 主体类型user/dept/role */
principalType?: string;
/** 主体ID */
principalId?: string;
/** 当主体为role且限定到某部门时的部门ID支持某部门的某角色 */
principalDeptId?: string;
/** 权限位1读 2写 4执行 */
permission?: number;
/** 允许或显式拒绝 */
allow?: boolean;
/** 是否包含子级对dept/role生效 */
includeDescendants?: boolean;
}
// TbSysAclPolicyDTO - 系统访问控制策略DTO
export interface TbSysAclPolicyDTO extends BaseDTO {
/** 策略ID */
policyId?: string;
/** 策略名称 */
name?: string;
/** 对象类型article/file/course/.. */
objectType?: string;
/** 编辑层级规则parent_only/parent_or_same_admin/owner_only/none */
editHierarchyRule?: string;
/** 可见层级规则 children_all/children_specified/none */
viewHierarchyRule?: string;
/** 默认权限无显式ACL时应用 */
defaultPermission?: number;
/** 默认是否允许 */
defaultAllow?: boolean;
/** 是否默认应用到子级 */
applyToChildren?: boolean;
}

View File

@@ -0,0 +1,138 @@
import { BaseVO, BaseDTO } from "../base";
import { PermissionVO } from "./permission";
export interface SysUserVO extends BaseVO {
/** 用户ID */
userId?: string;
/** 用户编码 */
usercode?: string;
/** 密码(敏感信息,仅用于创建/修改) */
password?: string;
/** 邮箱 */
email?: string;
/** 手机 */
phone?: string;
/** 微信ID */
wechatId?: string;
/** 用户状态 */
status?: string;
/** 用户类型 */
userType?: string;
/** 用户名 */
username?: string;
/** 头像 */
avatar?: string;
/** 性别 */
gender?: number;
/** 等级 */
level?: number;
/** 身份证号 */
idCard?: string;
/** 地址 */
address?: string;
/** 用户部门角色列表 */
deptRoles?: UserDeptRoleVO[];
/** 用户角色权限列表 */
rolePermissions?: PermissionVO[];
/** 用户视图权限列表 */
viewPermissions?: PermissionVO[];
}
export interface UserDeptRoleVO extends BaseVO {
/** 用户ID */
userId?: string;
/** 密码 */
password?: string;
/** 邮箱 */
email?: string;
/** 手机 */
phone?: string;
/** 微信ID */
wechatId?: string;
/** 用户状态 */
status?: string;
/** 用户类型 */
userType?: string;
/** 头像 */
avatar?: string;
/** 用户名 */
username?: string;
/** 性别 */
gender?: number;
/** 等级 */
level?: number;
/** 身份证号 */
idCard?: string;
/** 地址 */
address?: string;
/** 部门ID */
deptId?: string;
/** 部门名称 */
deptName?: string;
/** 父级部门ID */
parentId?: string;
/** 部门描述 */
deptDescription?: string;
/** 角色ID */
roleId?: string;
/** 角色名称 */
roleName?: string;
/** 角色描述 */
roleDescription?: string;
/** 角色作用域 */
scope?: string;
/** 所属部门ID */
ownerDeptId?: string;
/** 角色状态 */
roleStatus?: boolean;
}
// DTO 类型
// TbSysUserDTO - 系统用户DTO创建和更新
export interface TbSysUserDTO extends BaseDTO {
/** 用户ID */
userId?: string;
/** 用户编码 */
usercode?: string;
/** 密码 */
password?: string;
/** 邮箱 */
email?: string;
/** 手机(加密) */
phone?: string;
/** 手机号哈希 */
phone_hash?: string;
/** 微信ID */
wechatId?: string;
/** 用户状态 */
status?: string;
/** 用户类型 */
userType?: string;
}
// TbSysUserInfoDTO - 系统用户信息DTO
export interface TbSysUserInfoDTO extends BaseDTO {
/** 用户ID */
userId?: string;
/** 头像 */
avatar?: string;
/** 性别 */
gender?: number;
/** 用户名称 */
username?: string;
/** 等级 */
level?: number;
/** 身份证号 */
idCard?: string;
/** 地址 */
address?: string;
}
// TbSysUserRoleDTO - 系统用户角色关系DTO
export interface TbSysUserRoleDTO extends BaseDTO {
/** 用户ID */
userId?: string;
/** 角色ID */
roleId?: string;
}

View File

@@ -0,0 +1,351 @@
import { BaseVO } from '../base'
import { BaseDTO } from '../base'
// ==================== DTO ====================
/**
* 聊天室DTO
*/
export interface TbChatRoomDTO extends BaseDTO {
roomId?: string
workcaseId?: string
roomName?: string
roomType?: string
status?: string
guestId?: string
guestName?: string
aiSessionId?: string
currentAgentId?: string
agentCount?: number
messageCount?: number
unreadCount?: number
lastMessageTime?: string
lastMessage?: string
closedBy?: string
closedTime?: string
}
/**
* 聊天消息DTO
*/
export interface TbChatRoomMessageDTO extends BaseDTO {
messageId?: string
roomId?: string
senderId?: string
senderType?: string
senderName?: string
messageType?: string
content?: string
files?: string[]
contentExtra?: Record<string, any>
replyToMsgId?: string
isAiMessage?: boolean
aiMessageId?: string
status?: string
readCount?: number
sendTime?: string
}
/**
* 聊天室成员DTO
*/
export interface TbChatRoomMemberDTO extends BaseDTO {
memberId?: string
roomId?: string
userId?: string
userType?: string
userName?: string
status?: string
unreadCount?: number
lastReadTime?: string
lastReadMsgId?: string
joinTime?: string
leaveTime?: string
}
/**
* 视频会议DTO
*/
export interface TbVideoMeetingDTO extends BaseDTO {
meetingId?: string
roomId?: string
workcaseId?: string
meetingName?: string
meetingPassword?: string
jwtToken?: string
jitsiRoomName?: string
jitsiServerUrl?: string
status?: string
creatorId?: string
creatorType?: string
creatorName?: string
participantCount?: number
maxParticipants?: number
actualStartTime?: string
actualEndTime?: string
durationSeconds?: number
iframeUrl?: string
config?: Record<string, any>
}
/**
* 会议参与记录DTO
*/
export interface TbMeetingParticipantDTO extends BaseDTO {
participantId?: string
meetingId?: string
userId?: string
userType?: string
userName?: string
joinTime?: string
leaveTime?: string
durationSeconds?: number
isModerator?: boolean
joinMethod?: string
deviceInfo?: string
}
/**
* 会议转录记录表数据对象DTO
*/
export interface TbMeetingTranscriptionDTO extends BaseDTO {
/** 转录记录ID */
transcriptionId?: string
/** 会议ID */
meetingId?: string
/** 说话人ID */
speakerId?: string
/** 说话人名称 */
speakerName?: string
/** 说话人类型guest-来客 agent-客服 */
speakerType?: string
/** 转录文本内容 */
content?: string
/** 原始转录结果 */
contentRaw?: string
/** 语言 */
language?: string
/** 识别置信度0-1 */
confidence?: number
/** 语音开始时间 */
speechStartTime?: string
/** 语音结束时间 */
speechEndTime?: string
/** 语音时长(毫秒) */
durationMs?: number
/** 音频片段URL */
audioUrl?: string
/** 片段序号 */
segmentIndex?: number
/** 是否最终结果 */
isFinal?: boolean
/** 服务提供商 */
serviceProvider?: string
}
// ==================== VO ====================
/**
* 聊天室VO
* 用于前端展示聊天室信息
*/
export interface ChatRoomVO extends BaseVO {
roomId?: string
workcaseId?: string
roomName?: string
roomType?: string
status?: string
guestId?: string
guestName?: string
aiSessionId?: string
currentAgentId?: string
currentAgentName?: string
agentCount?: number
messageCount?: number
unreadCount?: number
lastMessageTime?: string
lastMessage?: string
closedBy?: string
closedByName?: string
closedTime?: string
}
/**
* 聊天消息VO
* 用于前端展示聊天消息
*/
export interface ChatRoomMessageVO extends BaseVO {
messageId?: string
roomId?: string
senderId?: string
senderType?: string
senderName?: string
senderAvatar?: string
messageType?: string
content?: string
files?: string[]
fileCount?: number
contentExtra?: Record<string, any>
replyToMsgId?: string
replyToMsgContent?: string
isAiMessage?: boolean
aiMessageId?: string
status?: string
readCount?: number
sendTime?: string
}
/**
* 聊天室成员VO
* 用于前端展示聊天室成员信息
*/
export interface ChatMemberVO extends BaseVO {
memberId?: string
roomId?: string
userId?: string
userType?: string
userName?: string
userAvatar?: string
status?: string
unreadCount?: number
lastReadTime?: string
lastReadMsgId?: string
joinTime?: string
leaveTime?: string
}
/**
* 视频会议VO
* 用于前端展示Jitsi Meet会议信息
*/
export interface VideoMeetingVO extends BaseVO {
meetingId?: string
roomId?: string
workcaseId?: string
meetingName?: string
meetingPassword?: string
jwtToken?: string
jitsiRoomName?: string
jitsiServerUrl?: string
status?: string
creatorId?: string
creatorType?: string
creatorName?: string
participantCount?: number
maxParticipants?: number
startTime?: string
endTime?: string
durationSeconds?: number
durationFormatted?: string
iframeUrl?: string
config?: Record<string, any>
}
/**
* 会议参与记录VO
* 用于前端展示会议参与者信息
*/
export interface MeetingParticipantVO extends BaseVO {
participantId?: string
meetingId?: string
userId?: string
userType?: string
userName?: string
userAvatar?: string
joinTime?: string
leaveTime?: string
durationSeconds?: number
durationFormatted?: string
isModerator?: boolean
joinMethod?: string
joinMethodName?: string
deviceInfo?: string
isOnline?: boolean
}
/**
* 发送消息参数
*/
export interface SendMessageParam {
roomId: string
content: string
files?: string[]
messageType?: string
replyToMsgId?: string
}
/**
* 创建会议参数
*/
export interface CreateMeetingParam {
roomId: string
workcaseId: string
meetingName?: string
meetingPassword?: string
maxParticipants?: number
}
/**
* 标记已读参数
*/
export interface MarkReadParam {
roomId: string
messageIds?: string[]
}
// ==================== 客服相关 ====================
/**
* 客服人员DTO
*/
export interface TbCustomerServiceDTO extends BaseDTO {
userId?: string
userName?: string
status?: string
maxConcurrentChats?: number
currentChatCount?: number
totalServedCount?: number
avgResponseTime?: number
avgRating?: number
skills?: string[]
priority?: number
lastOnlineTime?: string
}
/**
* 客服人员VO
*/
export interface CustomerServiceVO extends BaseVO {
userId?: string
userName?: string
userAvatar?: string
status?: string
statusName?: string
maxConcurrentChats?: number
currentChatCount?: number
totalServedCount?: number
avgResponseTime?: number
avgResponseTimeFormatted?: string
avgRating?: number
skills?: string[]
skillNames?: string[]
priority?: number
lastOnlineTime?: string
isAvailable?: boolean
}
// ==================== 词云相关 ====================
/**
* 词云DTO
*/
export interface TbWordCloudDTO extends BaseDTO {
wordCloudId?: string
word?: string
category?: string
weight?: number
frequency?: number
sentiment?: string
source?: string
relatedWords?: string[]
}

View File

@@ -0,0 +1,66 @@
import { BaseVO } from '../base'
// ==================== VO ====================
/**
* 会话VO
* 用于前端展示会话信息
*/
export interface ConversationVO extends BaseVO {
/** 会话ID */
conversationId?: string
/** 客户ID */
customerId?: string
/** 客户姓名 */
customerName?: string
/** 客户头像 */
customerAvatar?: string
/** 会话类型 */
conversationType?: string
/** 会话类型名称 */
conversationTypeName?: string
/** 渠道 */
channel?: string
/** 渠道名称 */
channelName?: string
/** 智能体ID或客服人员ID */
agentId?: string
/** 座席名称 */
agentName?: string
/** 座席类型 */
agentType?: string
/** 座席类型名称 */
agentTypeName?: string
/** 会话开始时间 */
sessionStartTime?: string
/** 会话结束时间 */
sessionEndTime?: string
/** 会话时长(秒) */
durationSeconds?: number
/** 会话时长格式化显示 */
durationFormatted?: string
/** 消息数量 */
messageCount?: number
/** 会话状态 */
conversationStatus?: string
/** 会话状态名称 */
conversationStatusName?: string
/** 会话状态颜色 */
statusColor?: string
/** 满意度评分1-5星 */
satisfactionRating?: number
/** 满意度反馈 */
satisfactionFeedback?: string
/** 会话摘要 */
summary?: string
/** 会话标签 */
tags?: string[]
/** 会话元数据 */
metadata?: Record<string, any>
/** 最后一条消息内容 */
lastMessageContent?: string
/** 最后一条消息时间 */
lastMessageTime?: string
/** 创建者姓名 */
creatorName?: string
}

View File

@@ -0,0 +1,133 @@
import { BaseDTO, BaseVO } from '../base'
// ==================== DTO ====================
/**
* 客服人员配置表数据对象DTO
*/
export interface TbCustomerServiceDTO extends BaseDTO {
/** 员工ID关联sys用户ID */
userId?: string
/** 员工姓名 */
username?: string
/** 员工工号 */
userCode?: string
/** 状态online-在线 busy-忙碌 offline-离线 */
status?: string
/** 技能标签 */
skillTags?: string[]
/** 最大并发接待数 */
maxConcurrent?: number
/** 当前工作量 */
currentWorkload?: number
/** 累计服务次数 */
totalServed?: number
/** 平均响应时间(秒) */
avgResponseTime?: number
/** 满意度评分0-5 */
satisfactionScore?: number
}
// ==================== VO ====================
/**
* 客服人员配置VO
* 用于前端展示客服人员信息
*/
export interface CustomerServiceVO extends BaseVO {
/** 员工ID关联sys用户ID */
userId?: string
/** 员工姓名 */
username?: string
/** 员工工号 */
userCode?: string
/** 员工头像 */
avatar?: string
/** 状态online-在线 busy-忙碌 offline-离线 */
status?: string
/** 状态名称 */
statusName?: string
/** 技能标签 */
skillTags?: string[]
/** 最大并发接待数 */
maxConcurrent?: number
/** 当前工作量 */
currentWorkload?: number
/** 累计服务次数 */
totalServed?: number
/** 平均响应时间(秒) */
avgResponseTime?: number
/** 平均响应时间(格式化) */
avgResponseTimeFormatted?: string
/** 满意度评分0-5 */
satisfactionScore?: number
/** 是否可接待(工作量未满) */
isAvailable?: boolean
}
/**
* 客户信息VO
* 用于前端展示客户信息
*/
export interface CustomerVO extends BaseVO {
/** 客户ID */
customerId?: string
/** 客户编号 */
customerNo?: string
/** 客户姓名 */
customerName?: string
/** 客户类型 */
customerType?: string
/** 客户类型名称 */
customerTypeName?: string
/** 公司名称 */
companyName?: string
/** 电话 */
phone?: string
/** 邮箱 */
email?: string
/** 微信OpenID */
wechatOpenid?: string
/** 头像URL */
avatar?: string
/** 性别 */
gender?: number
/** 性别名称 */
genderName?: string
/** 地址 */
address?: string
/** 客户等级 */
customerLevel?: string
/** 客户等级名称 */
customerLevelName?: string
/** 客户来源 */
customerSource?: string
/** 客户来源名称 */
customerSourceName?: string
/** 客户标签 */
tags?: string[]
/** 备注 */
notes?: string
/** CRM系统客户ID */
crmCustomerId?: string
/** 最后联系时间 */
lastContactTime?: string
/** 咨询总次数 */
totalConsultations?: number
/** 订单总数 */
totalOrders?: number
/** 总消费金额 */
totalAmount?: number
/** 满意度评分 */
satisfactionScore?: number
/** 状态 */
status?: string
/** 状态名称 */
statusName?: string
/** 状态颜色 */
statusColor?: string
/** 创建者姓名 */
creatorName?: string
/** 更新者姓名 */
updaterName?: string
}

View File

@@ -0,0 +1,5 @@
export * from "./workcase"
export * from "./chatRoom"
export * from "./customer"
export * from "./conversation"
export * from "./wordCloud"

View File

@@ -0,0 +1,19 @@
import { BaseDTO } from '../base'
// ==================== DTO ====================
/**
* 词云表数据对象DTO
*/
export interface TbWordCloudDTO extends BaseDTO {
/** 词条ID */
wordId?: string
/** 词语 */
word?: string
/** 词频 */
frequency?: string
/** 分类 */
category?: string
/** 统计日期 */
statDate?: string
}

View File

@@ -0,0 +1,65 @@
import type { BaseDTO } from '@/types/base'
/**
* 工单表对象
*/
export interface TbWorkcaseDTO extends BaseDTO {
/** 工单ID */
workcaseId?: string
/** 来客ID */
userId?: string
/** 来客姓名 */
username?: string
/** 来客电话 */
phone?: string
/** 故障类型 */
type?: string
/** 设备名称 */
device?: string
/** 设备代码 */
deviceCode?: string
/** 工单图片列表 */
imgs?: string[]
/** 紧急程度 normal-普通 emergency-紧急 */
emergency?: 'normal' | 'emergency'
/** 状态 pending-待处理 processing-处理中 done-已完成 */
status?: 'pending' | 'processing' | 'done'
/** 处理人ID */
processor?: string
}
/**
* 工单过程表DTO
*/
export interface TbWorkcaseProcessDTO extends BaseDTO {
/** 工单ID */
workcaseId?: string
/** 过程ID */
processId?: string
/** 动作 info记录assign指派redeploy转派repeal撤销finish完成 */
action?: 'info' | 'assign' | 'redeploy' | 'repeal' | 'finish'
/** 消息 */
message?: string
/** 携带文件列表 */
files?: string[]
/** 处理人(指派、转派专属) */
processor?: string
}
/**
* 工单设备涉及的文件DTO
*/
export interface TbWorkcaseDeviceDTO extends BaseDTO {
/** 工单ID */
workcaseId?: string
/** 设备名称 */
device?: string
/** 设备代码 */
deviceCode?: string
/** 文件ID */
fileId?: string
/** 文件名 */
fileName?: string
/** 文件根ID */
fileRootId?: string
}

View File

@@ -0,0 +1 @@
{"code":"import {} from \"vue\";\nexport default defineComponent({\n onLaunch: function () {\n uni.__f__('log', 'at App.uvue:7', 'App Launch');\n // 检查是否已选择模式\n this.checkModeSelection();\n },\n onShow: function () {\n uni.__f__('log', 'at App.uvue:12', 'App Show');\n },\n onHide: function () {\n uni.__f__('log', 'at App.uvue:15', 'App Hide');\n },\n onExit: function () {\n uni.__f__('log', 'at App.uvue:36', 'App Exit');\n },\n methods: {\n // 检查并选择模式\n checkModeSelection() {\n const mode = uni.getStorageSync('userMode');\n if (!mode) {\n this.showModeSelector();\n }\n },\n // 显示模式选择器\n showModeSelector() {\n uni.showActionSheet({\n itemList: ['员工模式 (17857100375)', '访客模式 (17857100376)'],\n success: (res) => {\n let wechatId = '';\n let userMode = '';\n let phone = '';\n if (res.tapIndex === 0) {\n wechatId = '17857100375';\n phone = '17857100375';\n userMode = 'staff';\n }\n else {\n wechatId = '17857100376';\n phone = '17857100376';\n userMode = 'guest';\n }\n // 存储选择\n uni.setStorageSync('userMode', userMode);\n uni.setStorageSync('wechatId', wechatId);\n uni.setStorageSync('phone', phone);\n uni.__f__('log', 'at App.uvue:67', '已选择模式:', userMode, 'wechatId:', wechatId);\n uni.showToast({\n title: userMode === 'staff' ? '员工模式' : '访客模式',\n icon: 'success'\n });\n },\n fail: () => {\n // 用户取消,默认使用访客模式\n uni.setStorageSync('userMode', 'guest');\n uni.setStorageSync('wechatId', '17857100376');\n uni.__f__('log', 'at App.uvue:77', '默认使用访客模式');\n }\n });\n }\n }\n});\n//# sourceMappingURL=F:/Project/urbanLifeline/urbanLifelineWeb/packages/workcase_wechat/App.uvue?vue&type=script&lang.uts.js.map","references":[],"uniExtApis":["uni.__f__","uni.getStorageSync","uni.setStorageSync","uni.showToast","uni.showActionSheet"],"map":"{\"version\":3,\"file\":\"App.uvue?vue&type=script&lang.uts.js\",\"sourceRoot\":\"\",\"sources\":[\"App.uvue?vue&type=script&lang.uts\"],\"names\":[],\"mappings\":\";AAIC,+BAAe;IACd,QAAQ,EAAE;QACT,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,eAAe,EAAC,YAAY,CAAC,CAAA;QAC7C,YAAY;QACZ,IAAI,CAAC,kBAAkB,EAAE,CAAA;IAC1B,CAAC;IACD,MAAM,EAAE;QACP,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,UAAU,CAAC,CAAA;IAC7C,CAAC;IACD,MAAM,EAAE;QACP,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,UAAU,CAAC,CAAA;IAC7C,CAAC;IAmBD,MAAM,EAAE;QACP,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,UAAU,CAAC,CAAA;IAC7C,CAAC;IACD,OAAO,EAAE;QACR,UAAU;QACV,kBAAkB;YACjB,MAAM,IAAI,GAAG,GAAG,CAAC,cAAc,CAAC,UAAU,CAAC,CAAA;YAC3C,IAAI,CAAC,IAAI,EAAE;gBACV,IAAI,CAAC,gBAAgB,EAAE,CAAA;aACvB;QACF,CAAC;QACD,UAAU;QACV,gBAAgB;YACf,GAAG,CAAC,eAAe,CAAC;gBACnB,QAAQ,EAAE,CAAC,oBAAoB,EAAE,oBAAoB,CAAC;gBACtD,OAAO,EAAE,CAAC,GAAG;oBACZ,IAAI,QAAQ,GAAG,EAAE,CAAA;oBACjB,IAAI,QAAQ,GAAG,EAAE,CAAA;oBACjB,IAAI,KAAK,GAAG,EAAE,CAAA;oBACd,IAAI,GAAG,CAAC,QAAQ,KAAK,CAAC,EAAE;wBACvB,QAAQ,GAAG,aAAa,CAAA;wBACxB,KAAK,GAAG,aAAa,CAAA;wBACrB,QAAQ,GAAG,OAAO,CAAA;qBAClB;yBAAM;wBACN,QAAQ,GAAG,aAAa,CAAA;wBACxB,KAAK,GAAG,aAAa,CAAA;wBACrB,QAAQ,GAAG,OAAO,CAAA;qBAClB;oBACD,OAAO;oBACP,GAAG,CAAC,cAAc,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;oBACxC,GAAG,CAAC,cAAc,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;oBACxC,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;oBAClC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAA;oBAC3E,GAAG,CAAC,SAAS,CAAC;wBACb,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;wBAC7C,IAAI,EAAE,SAAS;qBACf,CAAC,CAAA;gBACH,CAAC;gBACD,IAAI,EAAE;oBACL,gBAAgB;oBAChB,GAAG,CAAC,cAAc,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;oBACvC,GAAG,CAAC,cAAc,CAAC,UAAU,EAAE,aAAa,CAAC,CAAA;oBAC7C,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,UAAU,CAAC,CAAA;gBAC7C,CAAC;aACD,CAAC,CAAA;QACH,CAAC;KACD;CACD,EAAA\"}"}

View File

@@ -1 +0,0 @@
{"code":"import {} from \"vue\";\nexport default defineComponent({\n onLaunch: function () {\n uni.__f__('log', 'at App.uvue:7', 'App Launch');\n },\n onShow: function () {\n uni.__f__('log', 'at App.uvue:10', 'App Show');\n },\n onHide: function () {\n uni.__f__('log', 'at App.uvue:13', 'App Hide');\n },\n onExit: function () {\n uni.__f__('log', 'at App.uvue:34', 'App Exit');\n },\n});\n//# sourceMappingURL=F:/Project/urbanLifeline/urbanLifelineWeb/packages/workcase_wechat/App.uvue?vue&type=script&lang.uts.js.map","references":[],"uniExtApis":["uni.__f__"],"map":"{\"version\":3,\"file\":\"App.uvue?vue&type=script&lang.uts.js\",\"sourceRoot\":\"\",\"sources\":[\"App.uvue?vue&type=script&lang.uts\"],\"names\":[],\"mappings\":\";AAIC,+BAAe;IACd,QAAQ,EAAE;QACT,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,eAAe,EAAC,YAAY,CAAC,CAAA;IAC9C,CAAC;IACD,MAAM,EAAE;QACP,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,UAAU,CAAC,CAAA;IAC7C,CAAC;IACD,MAAM,EAAE;QACP,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,UAAU,CAAC,CAAA;IAC7C,CAAC;IAmBD,MAAM,EAAE;QACP,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,UAAU,CAAC,CAAA;IAC7C,CAAC;CACD,EAAA\"}"}

View File

@@ -0,0 +1 @@
{"code":"import {} from \"vue\";\nexport default defineComponent({\n onLaunch: function () {\n uni.__f__('log', 'at App.uvue:7', 'App Launch');\n // 检查是否已选择模式\n this.checkModeSelection();\n },\n onShow: function () {\n uni.__f__('log', 'at App.uvue:12', 'App Show');\n },\n onHide: function () {\n uni.__f__('log', 'at App.uvue:15', 'App Hide');\n },\n onExit: function () {\n uni.__f__('log', 'at App.uvue:36', 'App Exit');\n },\n methods: {\n // 检查并选择模式\n checkModeSelection() {\n const mode = uni.getStorageSync('userMode');\n if (!mode) {\n this.showModeSelector();\n }\n },\n // 显示模式选择器\n showModeSelector() {\n uni.showActionSheet({\n itemList: ['员工模式 (17857100375)', '访客模式 (17857100376)'],\n success: (res) => {\n let wechatId = '';\n let userMode = '';\n let phone = '';\n if (res.tapIndex === 0) {\n wechatId = '17857100375';\n phone = '17857100375';\n userMode = 'staff';\n }\n else {\n wechatId = '17857100376';\n phone = '17857100376';\n userMode = 'guest';\n }\n // 存储选择\n uni.setStorageSync('userMode', userMode);\n uni.setStorageSync('wechatId', wechatId);\n uni.setStorageSync('phone', phone);\n uni.__f__('log', 'at App.uvue:67', '已选择模式:', userMode, 'wechatId:', wechatId);\n uni.showToast({\n title: userMode === 'staff' ? '员工模式' : '访客模式',\n icon: 'success'\n });\n },\n fail: () => {\n // 用户取消,默认使用访客模式\n uni.setStorageSync('userMode', 'guest');\n uni.setStorageSync('wechatId', '17857100376');\n uni.__f__('log', 'at App.uvue:77', '默认使用访客模式');\n }\n });\n }\n }\n});\n//# sourceMappingURL=F:/Project/urbanLifeline/urbanLifelineWeb/packages/workcase_wechat/App.uvue?vue&type=script&lang.uts.js.map","references":[],"uniExtApis":["uni.__f__","uni.getStorageSync","uni.setStorageSync","uni.showToast","uni.showActionSheet"],"map":"{\"version\":3,\"file\":\"App.uvue?vue&type=script&lang.uts.js\",\"sourceRoot\":\"\",\"sources\":[\"App.uvue?vue&type=script&lang.uts\"],\"names\":[],\"mappings\":\";AAIC,+BAAe;IACd,QAAQ,EAAE;QACT,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,eAAe,EAAC,YAAY,CAAC,CAAA;QAC7C,YAAY;QACZ,IAAI,CAAC,kBAAkB,EAAE,CAAA;IAC1B,CAAC;IACD,MAAM,EAAE;QACP,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,UAAU,CAAC,CAAA;IAC7C,CAAC;IACD,MAAM,EAAE;QACP,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,UAAU,CAAC,CAAA;IAC7C,CAAC;IAmBD,MAAM,EAAE;QACP,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,UAAU,CAAC,CAAA;IAC7C,CAAC;IACD,OAAO,EAAE;QACR,UAAU;QACV,kBAAkB;YACjB,MAAM,IAAI,GAAG,GAAG,CAAC,cAAc,CAAC,UAAU,CAAC,CAAA;YAC3C,IAAI,CAAC,IAAI,EAAE;gBACV,IAAI,CAAC,gBAAgB,EAAE,CAAA;aACvB;QACF,CAAC;QACD,UAAU;QACV,gBAAgB;YACf,GAAG,CAAC,eAAe,CAAC;gBACnB,QAAQ,EAAE,CAAC,oBAAoB,EAAE,oBAAoB,CAAC;gBACtD,OAAO,EAAE,CAAC,GAAG;oBACZ,IAAI,QAAQ,GAAG,EAAE,CAAA;oBACjB,IAAI,QAAQ,GAAG,EAAE,CAAA;oBACjB,IAAI,KAAK,GAAG,EAAE,CAAA;oBACd,IAAI,GAAG,CAAC,QAAQ,KAAK,CAAC,EAAE;wBACvB,QAAQ,GAAG,aAAa,CAAA;wBACxB,KAAK,GAAG,aAAa,CAAA;wBACrB,QAAQ,GAAG,OAAO,CAAA;qBAClB;yBAAM;wBACN,QAAQ,GAAG,aAAa,CAAA;wBACxB,KAAK,GAAG,aAAa,CAAA;wBACrB,QAAQ,GAAG,OAAO,CAAA;qBAClB;oBACD,OAAO;oBACP,GAAG,CAAC,cAAc,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;oBACxC,GAAG,CAAC,cAAc,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;oBACxC,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;oBAClC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAA;oBAC3E,GAAG,CAAC,SAAS,CAAC;wBACb,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;wBAC7C,IAAI,EAAE,SAAS;qBACf,CAAC,CAAA;gBACH,CAAC;gBACD,IAAI,EAAE;oBACL,gBAAgB;oBAChB,GAAG,CAAC,cAAc,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;oBACvC,GAAG,CAAC,cAAc,CAAC,UAAU,EAAE,aAAa,CAAC,CAAA;oBAC7C,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,UAAU,CAAC,CAAA;gBAC7C,CAAC;aACD,CAAC,CAAA;QACH,CAAC;KACD;CACD,EAAA\"}"}

View File

@@ -1 +0,0 @@
{"code":"import {} from \"vue\";\nexport default defineComponent({\n onLaunch: function () {\n uni.__f__('log', 'at App.uvue:7', 'App Launch');\n },\n onShow: function () {\n uni.__f__('log', 'at App.uvue:10', 'App Show');\n },\n onHide: function () {\n uni.__f__('log', 'at App.uvue:13', 'App Hide');\n },\n onExit: function () {\n uni.__f__('log', 'at App.uvue:34', 'App Exit');\n },\n});\n//# sourceMappingURL=F:/Project/urbanLifeline/urbanLifelineWeb/packages/workcase_wechat/App.uvue?vue&type=script&lang.uts.js.map","references":[],"uniExtApis":["uni.__f__"],"map":"{\"version\":3,\"file\":\"App.uvue?vue&type=script&lang.uts.js\",\"sourceRoot\":\"\",\"sources\":[\"App.uvue?vue&type=script&lang.uts\"],\"names\":[],\"mappings\":\";AAIC,+BAAe;IACd,QAAQ,EAAE;QACT,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,eAAe,EAAC,YAAY,CAAC,CAAA;IAC9C,CAAC;IACD,MAAM,EAAE;QACP,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,UAAU,CAAC,CAAA;IAC7C,CAAC;IACD,MAAM,EAAE;QACP,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,UAAU,CAAC,CAAA;IAC7C,CAAC;IAmBD,MAAM,EAAE;QACP,GAAG,CAAC,KAAK,CAAC,KAAK,EAAC,gBAAgB,EAAC,UAAU,CAAC,CAAA;IAC7C,CAAC;CACD,EAAA\"}"}

View File

@@ -0,0 +1,6 @@
1. 用户在index页面和AI进行聊天index页头部按钮区有我的工单和我的聊天室2个按钮人员切换按钮开发时有
2. 用户点击转人工,创建聊天室
3. 用户在聊天室进行对话, 聊天室头部区域有创建工单、查看工单选项根据当前聊天室是否绑定workcaseId判断
4. 可以发起会议进入meeting
** 当前开发环境通过微信小程序获取wechatId和用户名、手机号的代码进行实现但是用模拟数据。switchMode即进行人员切换实际获取数据代码要是小程序的代码只是进行了注释