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