diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java index e82941cf..6f0c5ea3 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java @@ -161,12 +161,11 @@ public class WorkcaseChatContorller { // ========================= ChatRoom聊天室管理(实时IM) ========================= - @Operation(summary = "创建聊天室") + @Operation(summary = "创建聊天室(转人工时调用)") @PreAuthorize("hasAuthority('workcase:room:create')") @PostMapping("/room") public ResultDomain 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 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") diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/ChatRoomServiceImpl.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/ChatRoomServiceImpl.java index 5c16777e..0a4711b1 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/ChatRoomServiceImpl.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/ChatRoomServiceImpl.java @@ -61,9 +61,9 @@ public class ChatRoomServiceImpl implements ChatRoomService { @Override @Transactional public ResultDomain 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()); diff --git a/urbanLifelineServ/workcase/工单+Jitsi Meet技术方案.md b/urbanLifelineServ/workcase/工单+Jitsi Meet技术方案.md index 7f99da48..f9ba0f82 100644 --- a/urbanLifelineServ/workcase/工单+Jitsi Meet技术方案.md +++ b/urbanLifelineServ/workcase/工单+Jitsi Meet技术方案.md @@ -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 +- 聊天室可独立存在,无需绑定工单 - 工单完成后聊天室归档 --- diff --git a/urbanLifelineServ/workcase/工单流程.md b/urbanLifelineServ/workcase/工单流程.md index 5f876c68..1b11b09b 100644 --- a/urbanLifelineServ/workcase/工单流程.md +++ b/urbanLifelineServ/workcase/工单流程.md @@ -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要避免任何人都能创建会议的问题 -有视频会议的需求 \ No newline at end of file +有视频会议的需求 + +总体内容变化,交互逻辑改变,工单和会议没有前置关系。用户先ai对话,然后创建聊天室,可以随时在聊天室内创建工单;员工可以在聊天室和web端创建工单并更新状态。 \ No newline at end of file diff --git a/urbanLifelineWeb/packages/shared/src/api/sys/guest.ts b/urbanLifelineWeb/packages/shared/src/api/sys/guest.ts new file mode 100644 index 00000000..7898a6f3 --- /dev/null +++ b/urbanLifelineWeb/packages/shared/src/api/sys/guest.ts @@ -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> { + const response = await api.post(`${this.baseUrl}`, guest) + return response.data + }, + + /** + * 更新来客 + */ + async updateGuest(guest: TbGuestDTO): Promise> { + const response = await api.put(`${this.baseUrl}`, guest) + return response.data + }, + + /** + * 删除来客 + */ + async deleteGuest(userId: string): Promise> { + const response = await api.delete(`${this.baseUrl}`, { params: { userId } }) + return response.data + }, + + /** + * 根据微信ID查询来客 + */ + async selectGuestByWechat(wechatId: string): Promise> { + const response = await api.get(`${this.baseUrl}/wechat/${wechatId}`) + return response.data + }, + + /** + * 获取来客列表 + */ + async listGuest(filter?: TbGuestDTO): Promise> { + const response = await api.get(`${this.baseUrl}/list`, { params: filter }) + return response.data + }, + + /** + * 分页查询来客 + */ + async pageGuest(pageRequest: PageRequest): Promise> { + const response = await api.post(`${this.baseUrl}/page`, pageRequest) + return response.data + }, + + /** + * 微信小程序用户识别登录 + * 优先尝试员工登录,失败则自动注册/查询来客 + * @param loginParam 登录参数(wechatId或phone必填) + * @returns LoginDomain 包含用户信息和token + */ + async identify(loginParam: LoginParam): Promise> { + const response = await api.post(`${this.baseUrl}/identify`, loginParam) + return response.data + } +} diff --git a/urbanLifelineWeb/packages/shared/src/types/auth/auth.ts b/urbanLifelineWeb/packages/shared/src/types/auth/auth.ts index fd80e213..0174f163 100644 --- a/urbanLifelineWeb/packages/shared/src/types/auth/auth.ts +++ b/urbanLifelineWeb/packages/shared/src/types/auth/auth.ts @@ -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 - 登录信息 diff --git a/urbanLifelineWeb/packages/shared/src/types/sys/guest.ts b/urbanLifelineWeb/packages/shared/src/types/sys/guest.ts new file mode 100644 index 00000000..75186014 --- /dev/null +++ b/urbanLifelineWeb/packages/shared/src/types/sys/guest.ts @@ -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 +} diff --git a/urbanLifelineWeb/packages/shared/src/types/sys/index.ts b/urbanLifelineWeb/packages/shared/src/types/sys/index.ts index ad0672e5..50788723 100644 --- a/urbanLifelineWeb/packages/shared/src/types/sys/index.ts +++ b/urbanLifelineWeb/packages/shared/src/types/sys/index.ts @@ -1,3 +1,4 @@ export * from "./config" +export * from "./guest" export * from "./permission" export * from "./user" diff --git a/urbanLifelineWeb/packages/workcase_wechat/App.uvue b/urbanLifelineWeb/packages/workcase_wechat/App.uvue index 8f05cc59..89fb3507 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/App.uvue +++ b/urbanLifelineWeb/packages/workcase_wechat/App.uvue @@ -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('默认使用访客模式') + } + }) + } + } } diff --git a/urbanLifelineWeb/packages/workcase_wechat/api/base.ts b/urbanLifelineWeb/packages/workcase_wechat/api/base.ts new file mode 100644 index 00000000..7c0476f8 --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/api/base.ts @@ -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(options: { + url: string + method?: 'GET' | 'POST' | 'PUT' | 'DELETE' + data?: any + header?: Record +}): Promise> { + 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) + } else { + reject(new Error(`请求失败: ${res.statusCode}`)) + } + }, + fail: (err: any) => { + reject(err) + } + }) + }) +} + + diff --git a/urbanLifelineWeb/packages/workcase_wechat/api/index.ts b/urbanLifelineWeb/packages/workcase_wechat/api/index.ts new file mode 100644 index 00000000..594598bd --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/api/index.ts @@ -0,0 +1,3 @@ +export * from "./base" +export * from "./sys" +export * from "./workcase" \ No newline at end of file diff --git a/urbanLifelineWeb/packages/workcase_wechat/api/sys/guest.ts b/urbanLifelineWeb/packages/workcase_wechat/api/sys/guest.ts new file mode 100644 index 00000000..081e6452 --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/api/sys/guest.ts @@ -0,0 +1,25 @@ +import { request } from '../base' +import type { LoginParam, ResultDomain, LoginDomain, TbGuestDTO } from '../../types' +// 来客 API +export const guestAPI = { + /** + * 微信小程序用户识别登录 + */ + identify(loginParam: LoginParam): Promise> { + return request({ + url: '/urban-lifeline/system/guest/identify', + method: 'POST', + data: loginParam + }) + }, + + /** + * 根据微信ID查询来客 + */ + selectGuestByWechat(wechatId: string): Promise> { + return request({ + url: `/urban-lifeline/system/guest/wechat/${wechatId}`, + method: 'GET' + }) + } +} \ No newline at end of file diff --git a/urbanLifelineWeb/packages/workcase_wechat/api/sys/index.ts b/urbanLifelineWeb/packages/workcase_wechat/api/sys/index.ts new file mode 100644 index 00000000..cf44b6d4 --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/api/sys/index.ts @@ -0,0 +1 @@ +export * from './guest' \ No newline at end of file diff --git a/urbanLifelineWeb/packages/workcase_wechat/api/workcase/index.ts b/urbanLifelineWeb/packages/workcase_wechat/api/workcase/index.ts new file mode 100644 index 00000000..ff366b1e --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/api/workcase/index.ts @@ -0,0 +1,2 @@ +export * from './workcase' +export * from './workcaseChat' \ No newline at end of file diff --git a/urbanLifelineWeb/packages/workcase_wechat/api/workcase/workcase.ts b/urbanLifelineWeb/packages/workcase_wechat/api/workcase/workcase.ts new file mode 100644 index 00000000..1ccc2585 --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/api/workcase/workcase.ts @@ -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> { + return request({ url: this.baseUrl, method: 'POST', data: workcase }) + }, + + /** + * 更新工单 + * @param workcase 工单信息 + */ + updateWorkcase(workcase: TbWorkcaseDTO): Promise> { + return request({ url: this.baseUrl, method: 'PUT', data: workcase }) + }, + + /** + * 删除工单 + * @param workcaseId 工单ID + */ + deleteWorkcase(workcaseId: string): Promise> { + return request({ url: `${this.baseUrl}/${workcaseId}`, method: 'DELETE' }) + }, + + /** + * 获取工单详情 + * @param workcaseId 工单ID + */ + getWorkcaseById(workcaseId: string): Promise> { + return request({ url: `${this.baseUrl}/${workcaseId}`, method: 'GET' }) + }, + + /** + * 查询工单列表 + * @param filter 筛选条件 + */ + getWorkcaseList(filter?: TbWorkcaseDTO): Promise> { + return request({ url: `${this.baseUrl}/list`, method: 'POST', data: filter || {} }) + }, + + /** + * 分页查询工单 + * @param pageRequest 分页请求 + */ + getWorkcasePage(pageRequest: PageRequest): Promise> { + return request({ url: `${this.baseUrl}/page`, method: 'POST', data: pageRequest }) + }, + + // ========================= CRM同步接口 ========================= + + /** + * 同步工单到CRM + * @param workcase 工单信息 + */ + syncWorkcaseToCrm(workcase: TbWorkcaseDTO): Promise> { + return request({ url: `${this.baseUrl}/sync/crm`, method: 'POST', data: workcase }) + }, + + /** + * 接收CRM工单更新(CRM回调) + * @param jsonBody JSON字符串 + */ + receiveWorkcaseFromCrm(jsonBody: string): Promise> { + return request({ url: `${this.baseUrl}/receive/crm`, method: 'POST', data: jsonBody }) + }, + + // ========================= 工单处理过程 ========================= + + /** + * 创建工单处理过程 + * @param process 处理过程信息 + */ + createWorkcaseProcess(process: TbWorkcaseProcessDTO): Promise> { + return request({ url: `${this.baseUrl}/process`, method: 'POST', data: process }) + }, + + /** + * 更新工单处理过程 + * @param process 处理过程信息 + */ + updateWorkcaseProcess(process: TbWorkcaseProcessDTO): Promise> { + return request({ url: `${this.baseUrl}/process`, method: 'PUT', data: process }) + }, + + /** + * 删除工单处理过程 + * @param processId 处理过程ID + */ + deleteWorkcaseProcess(processId: string): Promise> { + return request({ url: `${this.baseUrl}/process/${processId}`, method: 'DELETE' }) + }, + + /** + * 查询工单处理过程列表 + * @param filter 筛选条件 + */ + getWorkcaseProcessList(filter?: TbWorkcaseProcessDTO): Promise> { + return request({ url: `${this.baseUrl}/process/list`, method: 'POST', data: filter || {} }) + }, + + /** + * 分页查询工单处理过程 + * @param pageRequest 分页请求 + */ + getWorkcaseProcessPage(pageRequest: PageRequest): Promise> { + return request({ url: `${this.baseUrl}/process/page`, method: 'POST', data: pageRequest }) + }, + + // ========================= 工单设备管理 ========================= + + /** + * 创建工单设备 + * @param device 设备信息 + */ + createWorkcaseDevice(device: TbWorkcaseDeviceDTO): Promise> { + return request({ url: `${this.baseUrl}/device`, method: 'POST', data: device }) + }, + + /** + * 更新工单设备 + * @param device 设备信息 + */ + updateWorkcaseDevice(device: TbWorkcaseDeviceDTO): Promise> { + return request({ url: `${this.baseUrl}/device`, method: 'PUT', data: device }) + }, + + /** + * 删除工单设备 + * @param workcaseId 工单ID + * @param device 设备名称 + */ + deleteWorkcaseDevice(workcaseId: string, device: string): Promise> { + return request({ url: `${this.baseUrl}/device/${workcaseId}/${device}`, method: 'DELETE' }) + }, + + /** + * 查询工单设备列表 + * @param filter 筛选条件 + */ + getWorkcaseDeviceList(filter?: TbWorkcaseDeviceDTO): Promise> { + return request({ url: `${this.baseUrl}/device/list`, method: 'POST', data: filter || {} }) + }, + + /** + * 分页查询工单设备 + * @param pageRequest 分页请求 + */ + getWorkcaseDevicePage(pageRequest: PageRequest): Promise> { + return request({ url: `${this.baseUrl}/device/page`, method: 'POST', data: pageRequest }) + } +} diff --git a/urbanLifelineWeb/packages/workcase_wechat/api/workcase/workcaseChat.ts b/urbanLifelineWeb/packages/workcase_wechat/api/workcase/workcaseChat.ts new file mode 100644 index 00000000..8c251de3 --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/api/workcase/workcaseChat.ts @@ -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> { + return request({ url: this.baseUrl, method: 'POST', data: chat }) + }, + + /** + * 更新对话 + */ + updateChat(chat: TbChat): Promise> { + return request({ url: this.baseUrl, method: 'PUT', data: chat }) + }, + + /** + * 查询对话列表 + */ + getChatList(filter: TbChat): Promise> { + return request({ url: `${this.baseUrl}/list`, method: 'POST', data: filter }) + }, + + /** + * 获取对话消息列表 + */ + getChatMessageList(filter: TbChat): Promise> { + return request({ url: `${this.baseUrl}/message/list`, method: 'POST', data: filter }) + }, + + /** + * 准备对话会话 + */ + prepareChatMessageSession(prepareData: ChatPrepareData): Promise> { + return request({ 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> { + return request({ url: `${this.baseUrl}/stop/${taskId}`, method: 'POST', data: filter }) + }, + + /** + * 评论对话消息 + */ + commentChatMessage(filter: TbChat, messageId: string, comment: string): Promise> { + return request({ url: `${this.baseUrl}/comment?messageId=${messageId}&comment=${comment}`, method: 'POST', data: filter }) + }, + + // ====================== 对话分析 ====================== + + /** + * 分析对话(AI预填工单信息) + */ + analyzeChat(chatId: string): Promise> { + return request({ url: `${this.baseUrl}/analyze/${chatId}`, method: 'GET' }) + }, + + /** + * 总结对话 + */ + summaryChat(chatId: string): Promise> { + return request({ url: `${this.baseUrl}/summary/${chatId}`, method: 'POST' }) + }, + + // ====================== ChatRoom聊天室管理 ====================== + + /** + * 创建聊天室 + */ + createChatRoom(chatRoom: TbChatRoomDTO): Promise> { + return request({ url: `${this.baseUrl}/room`, method: 'POST', data: chatRoom }) + }, + + /** + * 更新聊天室 + */ + updateChatRoom(chatRoom: TbChatRoomDTO): Promise> { + return request({ url: `${this.baseUrl}/room`, method: 'PUT', data: chatRoom }) + }, + + /** + * 关闭聊天室 + */ + closeChatRoom(roomId: string, closedBy: string): Promise> { + return request({ url: `${this.baseUrl}/room/${roomId}/close?closedBy=${closedBy}`, method: 'POST' }) + }, + + /** + * 获取聊天室详情 + */ + getChatRoomById(roomId: string): Promise> { + return request({ url: `${this.baseUrl}/room/${roomId}`, method: 'GET' }) + }, + + /** + * 分页查询聊天室 + */ + getChatRoomPage(pageRequest: PageRequest): Promise> { + return request({ url: `${this.baseUrl}/room/page`, method: 'POST', data: pageRequest }) + }, + + // ====================== ChatRoom成员管理 ====================== + + /** + * 添加聊天室成员 + */ + addChatRoomMember(member: TbChatRoomMemberDTO): Promise> { + return request({ url: `${this.baseUrl}/room/member`, method: 'POST', data: member }) + }, + + /** + * 移除聊天室成员 + */ + removeChatRoomMember(memberId: string): Promise> { + return request({ url: `${this.baseUrl}/room/member/${memberId}`, method: 'DELETE' }) + }, + + /** + * 获取聊天室成员列表 + */ + getChatRoomMemberList(roomId: string): Promise> { + return request({ url: `${this.baseUrl}/room/${roomId}/members`, method: 'GET' }) + }, + + // ====================== ChatRoom消息管理 ====================== + + /** + * 发送聊天室消息 + */ + sendMessage(message: TbChatRoomMessageDTO): Promise> { + return request({ url: `${this.baseUrl}/room/message`, method: 'POST', data: message }) + }, + + /** + * 分页查询聊天室消息 + */ + getChatMessagePage(pageRequest: PageRequest): Promise> { + return request({ url: `${this.baseUrl}/room/message/page`, method: 'POST', data: pageRequest }) + }, + + /** + * 删除聊天室消息 + */ + deleteMessage(messageId: string): Promise> { + return request({ url: `${this.baseUrl}/room/message/${messageId}`, method: 'DELETE' }) + }, + + // ====================== 客服人员管理 ====================== + + /** + * 添加客服人员 + */ + addCustomerService(customerService: TbCustomerServiceDTO): Promise> { + return request({ url: `${this.baseUrl}/customer-service`, method: 'POST', data: customerService }) + }, + + /** + * 更新客服人员 + */ + updateCustomerService(customerService: TbCustomerServiceDTO): Promise> { + return request({ url: `${this.baseUrl}/customer-service`, method: 'PUT', data: customerService }) + }, + + /** + * 删除客服人员 + */ + deleteCustomerService(userId: string): Promise> { + return request({ url: `${this.baseUrl}/customer-service/${userId}`, method: 'DELETE' }) + }, + + /** + * 分页查询客服人员 + */ + getCustomerServicePage(pageRequest: PageRequest): Promise> { + return request({ url: `${this.baseUrl}/customer-service/page`, method: 'POST', data: pageRequest }) + }, + + /** + * 更新客服在线状态 + */ + updateCustomerServiceStatus(userId: string, status: string): Promise> { + return request({ url: `${this.baseUrl}/customer-service/${userId}/status?status=${status}`, method: 'POST' }) + }, + + /** + * 获取可接待客服列表 + */ + getAvailableCustomerServices(): Promise> { + return request({ url: `${this.baseUrl}/customer-service/available`, method: 'GET' }) + }, + + /** + * 自动分配客服 + */ + assignCustomerService(roomId: string): Promise> { + return request({ url: `${this.baseUrl}/room/${roomId}/assign`, method: 'POST' }) + }, + + // ====================== 词云管理 ====================== + + /** + * 添加词云 + */ + addWordCloud(wordCloud: TbWordCloudDTO): Promise> { + return request({ url: `${this.baseUrl}/wordcloud`, method: 'POST', data: wordCloud }) + }, + + /** + * 更新词云 + */ + updateWordCloud(wordCloud: TbWordCloudDTO): Promise> { + return request({ url: `${this.baseUrl}/wordcloud`, method: 'PUT', data: wordCloud }) + }, + + /** + * 查询词云列表 + */ + getWordCloudList(filter: TbWordCloudDTO): Promise> { + return request({ url: `${this.baseUrl}/wordcloud/list`, method: 'POST', data: filter }) + }, + + /** + * 分页查询词云 + */ + getWordCloudPage(pageRequest: PageRequest): Promise> { + return request({ url: `${this.baseUrl}/wordcloud/page`, method: 'POST', data: pageRequest }) + } +} diff --git a/urbanLifelineWeb/packages/workcase_wechat/package.json b/urbanLifelineWeb/packages/workcase_wechat/package.json index 9fd4539a..11441080 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/package.json +++ b/urbanLifelineWeb/packages/workcase_wechat/package.json @@ -1,6 +1,3 @@ { - "dependencies": { - "sockjs-client": "^1.6.1", - "@stomp/stompjs": "^7.2.1" - } + "dependencies": {} } diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages.json b/urbanLifelineWeb/packages/workcase_wechat/pages.json index b031dee3..d12a7828 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/pages.json +++ b/urbanLifelineWeb/packages/workcase_wechat/pages.json @@ -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": "" } } ], diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.scss b/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.scss new file mode 100644 index 00000000..5dd841c4 --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.scss @@ -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; +} \ No newline at end of file diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.uvue b/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.uvue new file mode 100644 index 00000000..63ffb964 --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.uvue @@ -0,0 +1,259 @@ + + + + + \ No newline at end of file diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoomList/chatRoomList.scss b/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoomList/chatRoomList.scss new file mode 100644 index 00000000..c38be997 --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoomList/chatRoomList.scss @@ -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; +} diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoomList/chatRoomList.uvue b/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoomList/chatRoomList.uvue new file mode 100644 index 00000000..ec20b90b --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoomList/chatRoomList.uvue @@ -0,0 +1,175 @@ + + + + + \ No newline at end of file diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.scss b/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.scss index 5f5c5842..be59f77c 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.scss +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.scss @@ -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; } diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.uvue b/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.uvue index 91d0285f..a4ed0cc7 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.uvue +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.uvue @@ -3,18 +3,48 @@ 泰豪小电 - + + + + + + + + + + + + + + + + + + + + 查询质保状态 + 发动机无法启动 + 申请上门维修 + + + + Hi~ 有什么可以帮您! + 泰豪小电为您服务:) + + - - - - - Hi~ 有什么可以帮您! - 泰豪小电为您服务:) + + + + 您好,我是泰豪小电智能客服。请描述您的问题,我会尽力协助。 @@ -44,34 +74,33 @@ - - - - - - - - - - - - - + + + 发动机无法启动 + - + - - - + + + + + @@ -111,8 +140,85 @@ const headerPaddingTop = ref(44) // header顶部padding,默认44px const headerTotalHeight = ref(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) } // 显示上传选项 diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/meeting/Meeting/Meeting.scss b/urbanLifelineWeb/packages/workcase_wechat/pages/meeting/Meeting/Meeting.scss new file mode 100644 index 00000000..a78aff6d --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/meeting/Meeting/Meeting.scss @@ -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; +} \ No newline at end of file diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/meeting/Meeting/Meeting.uvue b/urbanLifelineWeb/packages/workcase_wechat/pages/meeting/Meeting/Meeting.uvue new file mode 100644 index 00000000..25117bc4 --- /dev/null +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/meeting/Meeting/Meeting.uvue @@ -0,0 +1,191 @@ + + + + + \ No newline at end of file diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/workcase/detail.scss b/urbanLifelineWeb/packages/workcase_wechat/pages/workcase/detail.scss deleted file mode 100644 index 90f08eb6..00000000 --- a/urbanLifelineWeb/packages/workcase_wechat/pages/workcase/detail.scss +++ /dev/null @@ -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; -} diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/workcase/detail.uvue b/urbanLifelineWeb/packages/workcase_wechat/pages/workcase/detail.uvue deleted file mode 100644 index 8d2061de..00000000 --- a/urbanLifelineWeb/packages/workcase_wechat/pages/workcase/detail.uvue +++ /dev/null @@ -1,507 +0,0 @@ -