1轮修复

This commit is contained in:
2026-01-20 16:17:39 +08:00
parent 0bf7361672
commit 8ab6107f25
23 changed files with 2587 additions and 612 deletions

View File

@@ -7,13 +7,13 @@
<button class="workcase-btn" @tap="showUserSelector">
<text class="btn-text">{{userInfo.username || '切换'}}</text>
</button>
<button class="workcase-btn" @tap="goToChatRoomList">
<!-- <button class="workcase-btn" @tap="showOperationSelect('chat')">
<text class="btn-text">聊天室</text>
</button>
<button class="workcase-btn" @tap="goToWorkList">
<button class="workcase-btn" @tap="showOperationSelect('workcase')">
<image class="btn-icon" src="/static/imgs/case.svg" />
<text class="btn-text">工单</text>
</button>
</button> -->
</view>
</view>
<!-- 欢迎区域(机器人+浮动标签) -->
@@ -41,7 +41,8 @@
</view>
<!-- 聊天消息区域 -->
<scroll-view class="chat-messages" scroll-y="true" :scroll-top="scrollTop" scroll-with-animation="true" :class="{ started: messages.length > 0 }">
<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>
@@ -59,7 +60,8 @@
</view>
<!-- 用户消息的文件列表(在气泡外面) -->
<view v-if="item.files && item.files.length > 0" class="message-files">
<view v-for="fileId in item.files" :key="fileId" class="message-file-item" @tap="previewFile(fileId)">
<view v-for="fileId in item.files" :key="fileId" class="message-file-item"
@tap="previewFile(fileId)">
<view v-if="isImageFileById(fileId)" class="file-thumb image">
<image :src="getFileDownloadUrl(fileId)" mode="aspectFill" class="file-img" />
</view>
@@ -86,69 +88,74 @@
<view class="typing-dot"></view>
<view class="typing-dot"></view>
</view>
<rich-text v-else :nodes="renderMarkdown(item.content)" class="message-rich-text"></rich-text>
<rich-text v-else :nodes="renderMarkdown(item.content)"
class="message-rich-text"></rich-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>
<!-- 快捷按钮横向滚动 -->
<scroll-view class="quick-scroll" scroll-x="true" show-scrollbar="false">
<view class="quick-inner">
<view class="quick-btn has-icon" @tap="showOperationSelect('chat')">
<text class="quick-icon">☎</text>
<text class="quick-text">联系人工</text>
</view>
</scroll-view>
<view class="quick-btn has-icon" @tap="showOperationSelect('workcase')">
<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 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">
<!-- 已上传文件预览 -->
<view v-if="uploadedFiles.length > 0" class="uploaded-files">
<view v-for="(file, index) in uploadedFiles" :key="file.id || index" class="uploaded-file-item">
<view v-if="isImageFile(file)" class="file-preview-image">
<image :src="getFilePreviewUrl(file)" mode="aspectFill" class="preview-img" />
</view>
<view v-else class="file-preview-doc">
<text class="doc-icon">📄</text>
</view>
<text class="file-name">{{file.name || '文件'}}</text>
<view class="remove-file-btn" @tap="removeUploadedFile(index)">
<text class="remove-icon">✕</text>
</view>
<!-- 输入区域 -->
<view class="chat-input-wrap">
<!-- 已上传文件预览 -->
<view v-if="uploadedFiles.length > 0" class="uploaded-files">
<view v-for="(file, index) in uploadedFiles" :key="file.id || index" class="uploaded-file-item">
<view v-if="isImageFile(file)" class="file-preview-image">
<image :src="getFilePreviewUrl(file)" mode="aspectFill" class="preview-img" />
</view>
</view>
<!-- 输入框 -->
<view class="input-row">
<view class="upload-btn" :class="{ uploading: isUploading }" @tap="showUploadOptions">
<text v-if="isUploading" class="upload-icon">⏳</text>
<text v-else class="upload-icon">📎</text>
<view v-else class="file-preview-doc">
<text class="doc-icon">📄</text>
</view>
<input class="chat-input" v-model="inputText" placeholder="输入问题 来问问我~" @confirm="sendMessage" />
<view class="send-btn" :class="{ active: inputText.trim() || uploadedFiles.length > 0 }" @tap="sendMessage">
<text class="send-icon"></text>
<text class="file-name">{{file.name || '文件'}}</text>
<view class="remove-file-btn" @tap="removeUploadedFile(index)">
<text class="remove-icon"></text>
</view>
</view>
</view>
<!-- 输入框 -->
<view class="input-row">
<view class="upload-btn" :class="{ uploading: isUploading }" @tap="showUploadOptions">
<text v-if="isUploading" class="upload-icon">⏳</text>
<text v-else class="upload-icon">📎</text>
</view>
<input class="chat-input" v-model="inputText" placeholder="输入问题 来问问我~" @confirm="sendMessage" />
<view class="send-btn" :class="{ active: inputText.trim() || uploadedFiles.length > 0 }"
@tap="sendMessage">
<text class="send-icon">➤</text>
</view>
</view>
</view>
</view>
<!-- 设备代码输入弹窗 -->
<view class="device-code-modal" v-if="showDeviceCodeDialog">
<view class="modal-mask" @tap="cancelDeviceCodeInput"></view>
@@ -157,13 +164,8 @@
<text class="modal-title">请输入设备代码</text>
</view>
<view class="modal-body">
<input
class="device-code-input"
v-model="deviceCodeInput"
placeholder="请输入设备代码"
focus
@confirm="confirmDeviceCodeInput"
/>
<input class="device-code-input" v-model="deviceCodeInput" placeholder="请输入设备代码" focus
@confirm="confirmDeviceCodeInput" />
</view>
<view class="modal-footer">
<button class="modal-btn cancel" @tap="cancelDeviceCodeInput">
@@ -176,6 +178,28 @@
</view>
</view>
<!-- 操作选择弹窗 -->
<view class="operation-select-modal" v-if="showOperationSelectDialog">
<view class="modal-mask" @tap="cancelOperationSelect"></view>
<view class="modal-content">
<view class="modal-header">
<text class="modal-title">{{ operationSelectTitle }}</text>
</view>
<view class="modal-body">
<view class="operation-select-list">
<view class="operation-select-item" @tap="handleOperationSelect('existing')">
<text class="select-icon">📋</text>
<text class="select-text">进入已有{{ operationSelectType === 'chat' ? '聊天室' : '工单' }}</text>
</view>
<view class="operation-select-item" @tap="handleOperationSelect('new')">
<text class="select-icon"></text>
<text class="select-text">创建新{{ operationSelectType === 'chat' ? '聊天室' : '工单' }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 手机号授权弹窗 -->
<view class="phone-auth-modal" v-if="showPhoneAuthModal">
<view class="modal-mask"></view>
@@ -237,11 +261,11 @@
import { AGENT_ID } from '@/config'
// 前端消息展示类型
interface ChatMessageItem {
type: 'user' | 'bot'
content: string
time: string
actions?: string[] | null
files?: string[] // 文件ID数组
type : 'user' | 'bot'
content : string
time : string
actions ?: string[] | null
files ?: string[] // 文件ID数组
}
const agentId = AGENT_ID
// 响应式数据
@@ -268,7 +292,7 @@
userId: ''
})
const userType = ref(false)
// 是否显示手机号授权弹窗
const showPhoneAuthModal = ref(false)
// 临时保存的微信登录code
@@ -283,6 +307,12 @@
const deviceCodeInput = ref<string>('') // 弹窗中的设备代码输入
const pendingAction = ref<'workcase' | 'human' | ''>('') // 待执行的操作类型
// 操作选择弹窗相关
const showOperationSelectDialog = ref<boolean>(false) // 是否显示操作选择弹窗
const operationSelectType = ref<'chat' | 'workcase'>('chat') // 选择类型chat=聊天室, workcase=工单
const operationSelectTitle = ref<string>('') // 弹窗标题
const pendingOperation = ref<'existing' | 'new' | ''>('') // 待执行的操作existing=进入已有, new=创建新的
// 初始化用户信息
async function initUserInfo() {
try {
@@ -290,7 +320,7 @@
const cachedWechatId = uni.getStorageSync('wechatId')
const cachedUserInfo = uni.getStorageSync('userInfo')
const cachedToken = uni.getStorageSync('token')
if (cachedWechatId && cachedUserInfo && cachedToken) {
// 有完整缓存,直接使用
const parsedUserInfo = typeof cachedUserInfo === 'string' ? JSON.parse(cachedUserInfo) : cachedUserInfo
@@ -300,14 +330,14 @@
phone: parsedUserInfo.phone || '',
userId: parsedUserInfo.userId || ''
}
// 判断用户类型
if (parsedUserInfo.status === 'guest') {
userType.value = false
} else {
userType.value = true
}
console.log('使用缓存的用户信息:', userInfo.value)
return
}
@@ -324,7 +354,7 @@
// 自动登录
async function autoLogin() {
uni.showLoading({ title: '初始化中...' })
try {
// 使用 wx.login 获取 code
uni.login({
@@ -332,10 +362,10 @@
success: async (loginRes) => {
console.log('微信登录成功code:', loginRes.code)
uni.hideLoading()
// 保存code等待手机号授权后使用
tempWechatCode.value = loginRes.code
// 显示手机号授权弹窗
showPhoneAuthModal.value = true
},
@@ -345,7 +375,7 @@
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
}
})
} catch (error: any) {
} catch (error : any) {
console.error('自动登录失败:', error)
uni.hideLoading()
uni.showToast({
@@ -362,7 +392,7 @@
uni.removeStorageSync('userInfo')
uni.removeStorageSync('loginDomain')
uni.removeStorageSync('wechatId')
uni.showLoading({ title: '登录中...' })
try {
const res = await guestAPI.identify({
@@ -379,7 +409,7 @@
userInfo.value.userId = loginDomain.user?.userId || ''
console.log('identify成功:', loginDomain)
uni.showToast({ title: '登录成功', icon: 'success' })
if(loginDomain.user.status == 'guest') {
if (loginDomain.user.status == 'guest') {
userType.value = false
} else {
userType.value = true
@@ -394,12 +424,12 @@
}
// 获取手机号回调(保留用于正式环境)
async function onGetPhoneNumber(e: any) {
async function onGetPhoneNumber(e : any) {
console.log('获取手机号回调:', e)
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
console.error('获取手机号失败:', e.detail)
// 如果是权限问题,提示用户
if (e.detail.errno === 102) {
uni.showModal({
@@ -418,20 +448,20 @@
// 关闭弹窗
showPhoneAuthModal.value = false
uni.showLoading({ title: '登录中...' })
try {
const code = tempWechatCode.value
const wechatId = code.substring(0, 20) // 使用部分code作为临时ID
// 获取手机号授权返回的数据
const phoneCode = e.detail.code // 手机号授权code推荐使用
const encryptedData = e.detail.encryptedData // 加密数据
const iv = e.detail.iv // 解密向量
console.log('手机号授权数据:', { code, phoneCode, encryptedData: encryptedData?.substring(0, 50) + '...', iv })
// 调用 identify 接口
// 后端可以选择使用 phoneCode 或 encryptedData+iv 来解密手机号
const identifyRes = await guestAPI.identify({
@@ -442,18 +472,18 @@
iv: iv, // 解密向量旧版API
loginType: 'wechat_miniprogram'
})
uni.hideLoading()
if (identifyRes.success && identifyRes.data) {
const loginDomain = identifyRes.data
// 保存登录信息
uni.setStorageSync('token', loginDomain.token || '')
uni.setStorageSync('userInfo', JSON.stringify(loginDomain.user))
uni.setStorageSync('loginDomain', JSON.stringify(loginDomain))
uni.setStorageSync('wechatId', wechatId)
// 更新用户信息
userInfo.value = {
wechatId: wechatId,
@@ -461,14 +491,14 @@
phone: loginDomain.user?.phone || '',
userId: loginDomain.user?.userId || ''
}
// 判断用户类型
if (loginDomain.user?.status === 'guest') {
userType.value = false
} else {
userType.value = true
}
console.log('手机号授权登录成功:', userInfo.value)
uni.showToast({ title: '登录成功', icon: 'success' })
} else {
@@ -479,7 +509,7 @@
// 登录失败,重新显示授权弹窗
showPhoneAuthModal.value = true
}
} catch (error: any) {
} catch (error : any) {
console.error('手机号授权登录失败:', error)
uni.hideLoading()
uni.showToast({
@@ -492,7 +522,7 @@
}
// 选择模拟用户(测试用)
async function selectMockUser(phone: string, name: string, wechatId: string) {
async function selectMockUser(phone : string, name : string, wechatId : string) {
showPhoneAuthModal.value = false
uni.showLoading({ title: '登录中...' })
@@ -505,18 +535,18 @@
mockMode: true,
loginType: 'wechat_miniprogram'
})
uni.hideLoading()
if (identifyRes.success && identifyRes.data) {
const loginDomain = identifyRes.data
// 保存登录信息
uni.setStorageSync('token', loginDomain.token || '')
uni.setStorageSync('userInfo', JSON.stringify(loginDomain.user))
uni.setStorageSync('loginDomain', JSON.stringify(loginDomain))
uni.setStorageSync('wechatId', wechatId)
// 更新用户信息
userInfo.value = {
wechatId: wechatId,
@@ -524,14 +554,14 @@
phone: phone,
userId: loginDomain.user?.userId || ''
}
// 判断用户类型
if (loginDomain.user?.status === 'guest') {
userType.value = false
} else {
userType.value = true
}
console.log('模拟登录成功:', userInfo.value)
uni.showToast({ title: `${name} 登录成功`, icon: 'success' })
} else {
@@ -541,7 +571,7 @@
})
showPhoneAuthModal.value = true
}
} catch (error: any) {
} catch (error : any) {
console.error('模拟登录失败:', error)
uni.hideLoading()
uni.showToast({
@@ -570,7 +600,7 @@
// 获取窗口信息
const windowInfo = uni.getWindowInfo()
statusBarHeight.value = windowInfo.statusBarHeight || 0
// #ifdef MP-WEIXIN
// 获取胶囊按钮信息仅小程序计算header位置
try {
@@ -602,7 +632,7 @@
})
// 添加用户消息(包含文件)
const userMessage: ChatMessageItem = {
const userMessage : ChatMessageItem = {
type: 'user',
content: text,
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
@@ -640,7 +670,7 @@
}
// 准备流式对话(包含文件)
const prepareData: ChatPrepareData = {
const prepareData : ChatPrepareData = {
chatId: chatId.value,
query: query,
agentId: agentId,
@@ -776,7 +806,7 @@
}
// 检查并获取设备代码
function checkDeviceCode(action: 'workcase' | 'human') {
function checkDeviceCode(action : 'workcase' | 'human') {
if (!deviceCode.value) {
// 如果没有设备代码,显示输入弹窗
pendingAction.value = action
@@ -881,7 +911,7 @@
icon: 'none'
})
}
} catch (error: any) {
} catch (error : any) {
uni.hideLoading()
console.error('创建聊天室失败:', error)
uni.showToast({
@@ -891,6 +921,47 @@
}
}
// 显示操作选择弹窗
function showOperationSelect(type : 'chat' | 'workcase') {
operationSelectType.value = type
operationSelectTitle.value = type === 'chat' ? '选择聊天室操作' : '选择工单操作'
showOperationSelectDialog.value = true
}
// 处理操作选择
function handleOperationSelect(operation : 'existing' | 'new') {
showOperationSelectDialog.value = false
pendingOperation.value = operation
if (operation === 'existing') {
// 进入已有
if (operationSelectType.value === 'chat') {
// 进入已有聊天室
goToChatRoomList()
} else {
// 进入已有工单
goToWorkList()
}
} else {
// 创建新的
if (operationSelectType.value === 'chat') {
// 创建新聊天室(联系人工)
contactHuman()
} else {
// 创建新工单
showCreator()
}
}
}
// 取消操作选择
function cancelOperationSelect() {
showOperationSelectDialog.value = false
operationSelectType.value = 'chat'
operationSelectTitle.value = ''
pendingOperation.value = ''
}
// 直接跳转到工单详情页的 create 模式(复用 workcaseDetail 页面)
async function showCreator() {
// 检查设备代码
@@ -1023,7 +1094,7 @@
sourceType: ['album'],
success: (res) => {
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
res.tempFilePaths.forEach((filePath: string) => {
res.tempFilePaths.forEach((filePath : string) => {
uploadSingleFile(filePath)
})
}
@@ -1039,15 +1110,15 @@
count: 5,
type: 'file',
extension: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt'],
success: (res: any) => {
success: (res : any) => {
console.log('选择文件成功:', res)
if (res.tempFiles && res.tempFiles.length > 0) {
res.tempFiles.forEach((file: any) => {
res.tempFiles.forEach((file : any) => {
uploadSingleFile(file.path)
})
}
},
fail: (err: any) => {
fail: (err : any) => {
console.error('选择文件失败:', err)
uni.showToast({
title: '选择文件失败',
@@ -1065,15 +1136,15 @@
uni.chooseFile({
count: 5,
extension: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.txt'],
success: (res: any) => {
success: (res : any) => {
console.log('选择文件成功:', res)
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
res.tempFilePaths.forEach((filePath: string) => {
res.tempFilePaths.forEach((filePath : string) => {
uploadSingleFile(filePath)
})
}
},
fail: (err: any) => {
fail: (err : any) => {
console.error('选择文件失败:', err)
uni.showToast({
title: '选择文件失败',
@@ -1091,7 +1162,7 @@
}
// 上传单个文件
async function uploadSingleFile(filePath: string) {
async function uploadSingleFile(filePath : string) {
console.log('开始上传文件:', filePath)
if (!agentId) {
@@ -1112,7 +1183,7 @@
} else {
uni.showToast({ title: result.message || '上传失败', icon: 'none' })
}
} catch (error: any) {
} catch (error : any) {
console.error('文件上传失败:', error)
uni.showToast({ title: '上传失败: ' + (error.message || '未知错误'), icon: 'none' })
} finally {
@@ -1122,27 +1193,27 @@
}
// 移除已上传的文件
function removeUploadedFile(index: number) {
function removeUploadedFile(index : number) {
uploadedFiles.value.splice(index, 1)
}
// 判断是否为图片文件
function isImageFile(file: DifyFileInfo): boolean {
function isImageFile(file : DifyFileInfo) : boolean {
return file.type === 'image' || file.mime_type?.startsWith('image/') || false
}
// 获取文件预览URL
function getFilePreviewUrl(file: DifyFileInfo): string {
function getFilePreviewUrl(file : DifyFileInfo) : string {
return file.preview_url || file.source_url || file.url || ''
}
// 获取文件下载URL通过文件ID
function getFileDownloadUrl(fileId: string): string {
function getFileDownloadUrl(fileId : string) : string {
return fileAPI.getDownloadUrl(fileId)
}
// 判断文件ID对应的文件是否为图片
function isImageFileById(fileId: string): boolean {
function isImageFileById(fileId : string) : boolean {
// 从缓存中查找文件信息
const file = fileInfoCache.value.get(fileId)
if (file) {
@@ -1157,13 +1228,13 @@
}
// 获取文件名(从缓存)
function getFileName(fileId: string): string {
function getFileName(fileId : string) : string {
const file = fileInfoCache.value.get(fileId)
return file?.name || fileId.substring(0, 8) + '...'
}
// 文件预览
function previewFile(fileId: string) {
function previewFile(fileId : string) {
const url = getFileDownloadUrl(fileId)
// 如果是图片,使用图片预览
if (isImageFileById(fileId)) {
@@ -1191,7 +1262,6 @@
})
}
}
</script>
<style lang="scss" scoped>