Files
urbanLifeline/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.uvue
2025-12-24 18:22:13 +08:00

569 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="chat-container">
<!-- 顶部标题栏 -->
<view class="header" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
<text class="title">泰豪小电</text>
<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" :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>
<view class="message-item" v-for="(item, index) in messages" :key="index"
:class="item.type === 'user' ? 'user-message' : 'bot-message'">
<!-- 用户消息(右侧) -->
<view class="user-message-content" v-if="item.type === 'user'">
<view class="message-bubble user-bubble">
<text class="message-text">{{item.content}}</text>
</view>
<view class="avatar user-avatar">
<text class="avatar-text">我</text>
</view>
</view>
<!-- Bot/员工消息(左侧) -->
<view class="bot-message-content" v-else>
<view class="avatar bot-avatar">
<text class="avatar-text">AI</text>
</view>
<view class="message-bubble bot-bubble">
<!-- 加载动画:内容为空时显示 -->
<view class="typing-indicator" v-if="!item.content && isTyping">
<view class="typing-dot"></view>
<view class="typing-dot"></view>
<view class="typing-dot"></view>
</view>
<text class="message-text" v-else>{{item.content}}</text>
</view>
</view>
<text class="message-time">{{item.time}}</text>
</view>
</view>
</scroll-view>
<!-- 底部操作区域 -->
<view class="bottom-area">
<!-- 快捷按钮横向滚动 -->
<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>
</view>
<view class="quick-btn" @tap="handleQuickQuestion('发动机无法启动')">
<text class="quick-text">发动机无法启动</text>
</view>
</view>
</scroll-view>
<!-- 输入区域 -->
<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>
</view>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue'
import { guestAPI, aiChatAPI, workcaseChatAPI } from '@/api'
import type { TbWorkcaseDTO } from '@/types'
import { AGENT_ID } from '@/config'
// 前端消息展示类型
interface ChatMessageItem {
type: 'user' | 'bot'
content: string
time: string
actions?: string[] | null
}
const agentId = AGENT_ID
// 响应式数据
const messages = ref<ChatMessageItem[]>([])
const inputText = ref<string>('')
const isTyping = ref<boolean>(false)
const scrollTop = ref<number>(0)
const showWorkcaseCreator = ref<boolean>(false)
const statusBarHeight = ref<number>(0)
const headerPaddingTop = ref<number>(44) // header顶部padding默认44px
const headerTotalHeight = ref<number>(76) // header总高度默认76px
// 用户信息
const userInfo = ref({
wechatId: '',
username: '',
phone: '',
userId: ''
})
const isMockMode = ref(true) // 开发环境mock模式
const userType = ref(false)
// AI 对话相关
const chatId = ref<string>('') // 当前会话ID
const currentTaskId = ref<string>('') // 当前任务ID用于停止
// 初始化用户信息
async function initUserInfo() {
// #ifdef MP-WEIXIN
// 正式环境:从微信获取用户信息
// wx.login({
// success: (loginRes) => {
// // 使用code换取openid等信息
// console.log('微信登录code:', loginRes.code)
// }
// })
// #endif
// 开发环境使用mock数据
if (isMockMode.value) {
userInfo.value = {
wechatId: '17857100377',
username: '访客用户',
phone: '17857100377',
userId: ''
}
await doIdentify()
}
}
// 切换mock用户开发调试用
function switchMockUser() {
uni.showActionSheet({
itemList: ['员工 (17857100375)', '访客 (17857100377)'],
success: (res) => {
if (res.tapIndex === 0) {
userInfo.value = { wechatId: '17857100375', username: '员工用户', phone: '17857100375', userId: '' }
} else {
userInfo.value = { wechatId: '17857100377', username: '访客用户', phone: '17857100377', userId: '' }
}
doIdentify()
}
})
}
// 调用identify接口
async function doIdentify() {
uni.showLoading({ title: '登录中...' })
try {
const res = await guestAPI.identify({
wechatId: userInfo.value.wechatId,
phone: userInfo.value.phone
})
uni.hideLoading()
if (res.success && res.data) {
const loginDomain = res.data
uni.setStorageSync('token', loginDomain.token || '')
uni.setStorageSync('userInfo', JSON.stringify(loginDomain.user))
uni.setStorageSync('loginDomain', JSON.stringify(loginDomain))
uni.setStorageSync('wechatId', userInfo.value.wechatId)
userInfo.value.userId = loginDomain.user?.userId || ''
console.log('identify成功:', loginDomain)
uni.showToast({ title: '登录成功', icon: 'success' })
if(loginDomain.user.status == 'guest') {
userType.value = false
} else {
userType.value = true
}
} else {
console.error('identify失败:', res.message)
}
} catch (err) {
uni.hideLoading()
console.error('identify请求失败:', err)
}
}
// 生命周期
onMounted(() => {
// 初始化用户信息
initUserInfo()
// 设置页面标题
uni.setNavigationBarTitle({
title: '智能助手'
})
// 获取窗口信息
const windowInfo = uni.getWindowInfo()
statusBarHeight.value = windowInfo.statusBarHeight || 0
// #ifdef MP-WEIXIN
// 获取胶囊按钮信息仅小程序计算header位置
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
headerPaddingTop.value = menuButtonInfo.top
headerTotalHeight.value = menuButtonInfo.bottom
} catch (e) {
// 使用默认值
headerPaddingTop.value = 44
headerTotalHeight.value = 76
}
// #endif
})
// 发送消息
async function sendMessage() {
const text = inputText.value.trim()
if (!text || isTyping.value) return
// 添加用户消息
addMessage('user', text)
inputText.value = ''
// 调用AI聊天接口
await callAIChat(text)
}
// 调用AI聊天接口
async function callAIChat(query : string) {
isTyping.value = true
try {
// 如果没有会话ID先创建会话
if (!chatId.value) {
const createRes = await aiChatAPI.createChat({
title: '智能助手对话',
userId: userInfo.value.userId || userInfo.value.wechatId,
agentId: agentId,
userType: userType.value
})
if (createRes.success && createRes.data) {
chatId.value = createRes.data.chatId || ''
console.log('创建会话成功:', chatId.value)
} else {
throw new Error(createRes.message || '创建会话失败')
}
}
// 准备流式对话
const prepareRes = await aiChatAPI.prepareChatMessageSession({
chatId: chatId.value,
query: query,
agentId: agentId,
userType: userType.value,
userId: userInfo.value.userId
})
if (!prepareRes.success || !prepareRes.data) {
throw new Error(prepareRes.message || '准备对话失败')
}
const sessionId = prepareRes.data
console.log('准备流式对话成功:', sessionId)
// 添加空的AI消息占位
const messageIndex = messages.value.length
addMessage('bot', '')
// 建立SSE连接
startStreamChat(sessionId, messageIndex)
} catch (error : any) {
console.error('AI聊天失败:', error)
isTyping.value = false
addMessage('bot', '抱歉AI服务暂时不可用请稍后重试。')
}
}
// SSE 流式对话
function startStreamChat(sessionId : string, messageIndex : number) {
console.log('建立SSE连接, sessionId:', sessionId)
aiChatAPI.streamChat(sessionId, {
onMessage: (data) => {
if (data.answer) {
messages.value[messageIndex].content += data.answer
nextTick(() => scrollToBottom())
}
},
onEnd: (taskId) => {
isTyping.value = false
if (taskId) {
currentTaskId.value = taskId
}
},
onError: (error) => {
console.error('SSE错误:', error)
isTyping.value = false
messages.value[messageIndex].content = error
},
onComplete: () => {
isTyping.value = false
}
})
}
// 添加消息
function addMessage(type : 'user' | 'bot', content : string, actions : string[] | null = null) {
const now = new Date()
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
messages.value.push({
type,
content,
time,
actions
})
// 滚动到底部
nextTick(() => {
scrollToBottom()
})
}
// 模拟AI回复
function simulateAIResponse(userMessage : string) {
isTyping.value = true
setTimeout(() => {
isTyping.value = false
let response = ''
let actions : string[] | null = null
// 根据用户输入生成回复
if (userMessage.includes('工单') || userMessage.includes('报修') || userMessage.includes('问题')) {
response = '我理解您需要处理工单相关的事务。我可以帮您:'
actions = ['创建新工单', '查看工单状态', '联系客服']
} else if (userMessage.includes('你好') || userMessage.includes('您好')) {
response = '您好!很高兴为您服务。请问有什么可以帮助您的吗?'
actions = ['创建工单', '查看工单', '常见问题']
} else if (userMessage.includes('帮助') || userMessage.includes('功能')) {
response = '我可以为您提供以下服务:\n1. 创建工单 - 报告问题或提交服务请求\n2. 查看工单 - 跟踪您的工单处理进度\n3. 智能问答 - 解答常见问题'
actions = ['创建工单', '查看工单']
} else {
response = '感谢您的咨询。如果您遇到具体问题,建议创建工单,我们的专业团队会尽快为您处理。'
actions = ['创建工单', '联系人工客服']
}
addMessage('bot', response, actions)
}, 1000 + Math.random() * 1000)
}
// 快捷操作
function quickAction(action : string) {
if (action === '创建工单') {
showCreator()
} else if (action === '查看工单') {
goToWorkList()
} else {
addMessage('user', action)
simulateAIResponse(action)
}
}
// 处理建议操作
function handleSuggestedAction(action : string) {
if (action === '创建工单' || action === '创建新工单') {
showCreator()
} else if (action === '查看工单' || action === '查看工单状态') {
goToWorkList()
} else if (action === '联系客服' || action === '联系人工客服') {
uni.showModal({
title: '联系客服',
content: '客服电话400-123-4567\n工作时间9:00-18:00',
showCancel: false
})
} else {
addMessage('user', action)
simulateAIResponse(action)
}
}
// 显示工单创建器
function showCreator() {
showWorkcaseCreator.value = true
}
// 隐藏工单创建器
function hideCreator() {
showWorkcaseCreator.value = false
}
// 工单创建成功
function onWorkcaseCreated(workcaseData : TbWorkcaseDTO) {
hideCreator()
uni.showToast({
title: '工单创建成功',
icon: 'success'
})
// 添加成功消息
addMessage('bot', `工单创建成功!\n类型${workcaseData.type || ''}\n设备${workcaseData.device || ''}\n我们会尽快处理您的问题。`, ['查看工单', '创建新工单'])
}
// 跳转到工单列表
function goToWorkList() {
uni.navigateTo({
url: '/pages/workcase/workcaseList/workcaseList'
})
}
// 跳转到聊天室列表
function goToChatRoomList() {
uni.navigateTo({
url: '/pages/chatRoom/chatRoomList/chatRoomList'
})
}
// 滚动到底部
function scrollToBottom() {
// 先重置再设置大值,确保值变化触发滚动
scrollTop.value = scrollTop.value + 1
nextTick(() => {
scrollTop.value = 999999
})
}
// 联系人工客服 - 创建聊天室并进入
async function contactHuman() {
uni.showLoading({ title: '正在连接客服...' })
try {
// 创建聊天室
const res = await workcaseChatAPI.createChatRoom({
guestId: userInfo.value.userId || userInfo.value.wechatId,
guestName: userInfo.value.username || '访客',
roomName: `${userInfo.value.username || '访客'}的咨询`,
roomType: 'guest',
status: 'active',
aiSessionId: chatId.value || ''
})
uni.hideLoading()
if (res.success && res.data) {
const roomId = res.data.roomId
console.log('创建聊天室成功:', roomId)
// 跳转到聊天室页面
uni.navigateTo({
url: `/pages/chatRoom/chatRoom/chatRoom?roomId=${roomId}&roomName=${encodeURIComponent(res.data.roomName || '人工客服')}`
})
} else {
uni.showToast({
title: res.message || '连接客服失败',
icon: 'none'
})
}
} catch (error: any) {
uni.hideLoading()
console.error('创建聊天室失败:', error)
uni.showToast({
title: '连接客服失败,请稍后重试',
icon: 'none'
})
}
}
// 处理快速问题
async function handleQuickQuestion(question : string) {
addMessage('user', question)
await callAIChat(question)
}
// 显示上传选项
function showUploadOptions() {
uni.showActionSheet({
itemList: ['拍照', '从相册选择', '选择文件'],
success: (res) => {
switch (res.tapIndex) {
case 0:
// 拍照
chooseImageFromCamera()
break
case 1:
// 从相册选择
chooseImageFromAlbum()
break
case 2:
// 选择文件
chooseFile()
break
}
}
})
}
// 拍照
function chooseImageFromCamera() {
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success: (res) => {
// 处理图片上传逻辑
console.log('选择的图片:', res.tempFilePaths)
addMessage('user', '[图片]')
simulateAIResponse('收到您发送的图片')
}
})
}
// 从相册选择
function chooseImageFromAlbum() {
uni.chooseImage({
count: 1,
sourceType: ['album'],
success: (res) => {
// 处理图片上传逻辑
console.log('选择的图片:', res.tempFilePaths)
addMessage('user', '[图片]')
simulateAIResponse('收到您发送的图片')
}
})
}
// 选择文件
function chooseFile() {
// 这里可以扩展文件选择功能
uni.showToast({
title: '文件选择功能开发中',
icon: 'none'
})
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>