Files
urbanLifeline/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.uvue

853 lines
26 KiB
Plaintext
Raw Normal View History

2025-12-08 19:01:09 +08:00
<template>
2025-12-10 17:00:54 +08:00
<view class="chat-container">
<!-- 顶部标题栏 -->
<view class="header" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
<text class="title">泰豪小电</text>
2025-12-22 19:16:53 +08:00
<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>
2025-12-10 17:00:54 +08:00
</view>
2025-12-22 19:16:53 +08:00
<!-- 欢迎区域(机器人+浮动标签) -->
<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>
2025-12-10 17:00:54 +08:00
<!-- 聊天消息区域 -->
2025-12-22 19:16:53 +08:00
<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>
2025-12-10 17:00:54 +08:00
</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'">
2025-12-29 12:49:23 +08:00
<view class="message-content-wrapper">
<!-- 文字气泡 -->
<view class="message-bubble user-bubble" v-if="item.content">
<text class="message-text">{{item.content}}</text>
</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-if="isImageFileById(fileId)" class="file-thumb image">
<image :src="getFileDownloadUrl(fileId)" mode="aspectFill" class="file-img" />
</view>
<view v-else class="file-thumb doc">
<text class="file-icon">📄</text>
<text class="file-name-small">{{getFileName(fileId)}}</text>
</view>
</view>
</view>
2025-12-10 17:00:54 +08:00
</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">
2025-12-23 15:57:11 +08:00
<!-- 加载动画:内容为空时显示 -->
<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>
2025-12-25 12:33:12 +08:00
<rich-text v-else :nodes="renderMarkdown(item.content)" class="message-rich-text"></rich-text>
2025-12-10 17:00:54 +08:00
</view>
</view>
<text class="message-time">{{item.time}}</text>
</view>
</view>
</scroll-view>
<!-- 底部操作区域 -->
<view class="bottom-area">
2025-12-22 19:16:53 +08:00
<!-- 快捷按钮横向滚动 -->
<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('查询质保状态')">
2025-12-10 17:00:54 +08:00
<text class="quick-text">查询质保状态</text>
2025-12-22 19:16:53 +08:00
</view>
<view class="quick-btn" @tap="handleQuickQuestion('发动机无法启动')">
<text class="quick-text">发动机无法启动</text>
</view>
2025-12-10 17:00:54 +08:00
</view>
2025-12-22 19:16:53 +08:00
</scroll-view>
2025-12-10 17:00:54 +08:00
<!-- 输入区域 -->
2025-12-22 19:16:53 +08:00
<view class="chat-input-wrap">
2025-12-29 12:49:23 +08:00
<!-- 已上传文件预览 -->
<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>
</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>
2025-12-22 19:16:53 +08:00
</view>
2025-12-10 17:00:54 +08:00
</view>
</view>
2025-12-24 18:22:13 +08:00
2025-12-08 19:01:09 +08:00
</view>
</template>
2025-12-10 17:00:54 +08:00
<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue'
2025-12-23 16:56:22 +08:00
import { guestAPI, aiChatAPI, workcaseChatAPI } from '@/api'
2025-12-23 13:27:36 +08:00
import type { TbWorkcaseDTO } from '@/types'
2025-12-29 12:49:23 +08:00
import type { DifyFileInfo } from '@/types/ai/aiChat'
import { AGENT_ID, FILE_DOWNLOAD_URL } from '@/config'
2025-12-23 13:27:36 +08:00
// 前端消息展示类型
interface ChatMessageItem {
type: 'user' | 'bot'
content: string
time: string
actions?: string[] | null
2025-12-29 12:49:23 +08:00
files?: string[] // 文件ID数组
2025-12-10 17:00:54 +08:00
}
2025-12-23 15:57:11 +08:00
const agentId = AGENT_ID
2025-12-10 17:00:54 +08:00
// 响应式数据
2025-12-23 13:27:36 +08:00
const messages = ref<ChatMessageItem[]>([])
2025-12-10 17:00:54 +08:00
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
2025-12-29 12:49:23 +08:00
// 文件上传相关
const uploadedFiles = ref<DifyFileInfo[]>([])
const isUploading = ref<boolean>(false)
// 文件信息缓存 (fileId -> DifyFileInfo)
const fileInfoCache = ref<Map<string, DifyFileInfo>>(new Map())
2025-12-22 19:16:53 +08:00
// 用户信息
const userInfo = ref({
wechatId: '',
username: '',
2025-12-23 13:27:36 +08:00
phone: '',
userId: ''
2025-12-22 19:16:53 +08:00
})
const isMockMode = ref(true) // 开发环境mock模式
2025-12-23 15:57:11 +08:00
const userType = ref(false)
2025-12-23 13:27:36 +08:00
// AI 对话相关
const chatId = ref<string>('') // 当前会话ID
const currentTaskId = ref<string>('') // 当前任务ID用于停止
2025-12-22 19:16:53 +08:00
// 初始化用户信息
2025-12-23 13:27:36 +08:00
async function initUserInfo() {
2025-12-22 19:16:53 +08:00
// #ifdef MP-WEIXIN
// 正式环境:从微信获取用户信息
// wx.login({
// success: (loginRes) => {
// // 使用code换取openid等信息
// console.log('微信登录code:', loginRes.code)
// }
// })
// #endif
// 开发环境使用mock数据
if (isMockMode.value) {
userInfo.value = {
2025-12-28 18:18:05 +08:00
wechatId: '17857100377',
2025-12-23 15:57:11 +08:00
username: '访客用户',
2025-12-28 18:18:05 +08:00
phone: '17857100377',
2025-12-23 13:27:36 +08:00
userId: ''
2025-12-22 19:16:53 +08:00
}
2025-12-23 13:27:36 +08:00
await doIdentify()
2025-12-22 19:16:53 +08:00
}
}
// 切换mock用户开发调试用
function switchMockUser() {
uni.showActionSheet({
2025-12-28 18:18:05 +08:00
itemList: ['员工 (17857100375)', '访客 (17857100377)'],
2025-12-22 19:16:53 +08:00
success: (res) => {
if (res.tapIndex === 0) {
2025-12-23 13:27:36 +08:00
userInfo.value = { wechatId: '17857100375', username: '员工用户', phone: '17857100375', userId: '' }
2025-12-22 19:16:53 +08:00
} else {
2025-12-28 18:18:05 +08:00
userInfo.value = { wechatId: '17857100377', username: '访客用户', phone: '17857100377', userId: '' }
2025-12-22 19:16:53 +08:00
}
doIdentify()
}
})
}
// 调用identify接口
2025-12-23 13:27:36 +08:00
async function doIdentify() {
// 先清空本地存储的登录信息,确保重新识别身份
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
uni.removeStorageSync('loginDomain')
uni.removeStorageSync('wechatId')
2025-12-22 19:16:53 +08:00
uni.showLoading({ title: '登录中...' })
2025-12-23 13:27:36 +08:00
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))
2025-12-23 15:57:11 +08:00
uni.setStorageSync('loginDomain', JSON.stringify(loginDomain))
2025-12-23 13:27:36 +08:00
uni.setStorageSync('wechatId', userInfo.value.wechatId)
userInfo.value.userId = loginDomain.user?.userId || ''
console.log('identify成功:', loginDomain)
uni.showToast({ title: '登录成功', icon: 'success' })
2025-12-23 15:57:11 +08:00
if(loginDomain.user.status == 'guest') {
userType.value = false
} else {
userType.value = true
}
2025-12-23 13:27:36 +08:00
} else {
console.error('identify失败:', res.message)
2025-12-22 19:16:53 +08:00
}
2025-12-23 13:27:36 +08:00
} catch (err) {
uni.hideLoading()
console.error('identify请求失败:', err)
}
2025-12-22 19:16:53 +08:00
}
2025-12-10 17:00:54 +08:00
// 生命周期
onMounted(() => {
2025-12-22 19:16:53 +08:00
// 初始化用户信息
initUserInfo()
2025-12-10 17:00:54 +08:00
// 设置页面标题
uni.setNavigationBarTitle({
title: '智能助手'
})
2025-12-24 18:22:13 +08:00
// 获取窗口信息
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
2025-12-10 17:00:54 +08:00
})
// 发送消息
2025-12-23 13:27:36 +08:00
async function sendMessage() {
2025-12-10 17:00:54 +08:00
const text = inputText.value.trim()
2025-12-29 12:49:23 +08:00
// 允许只有文件或只有文本
if ((!text && uploadedFiles.value.length === 0) || isTyping.value) return
2025-12-08 19:01:09 +08:00
2025-12-29 12:49:23 +08:00
const query = text || '[文件]'
const currentFiles = [...uploadedFiles.value] // 保存当前文件列表副本
// 将文件信息缓存起来,用于立即渲染
currentFiles.forEach(f => {
if (f.sys_file_id) {
fileInfoCache.value.set(f.sys_file_id, f)
}
})
// 添加用户消息(包含文件)
const userMessage: ChatMessageItem = {
type: 'user',
content: text,
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
files: currentFiles.length > 0 ? currentFiles.map(item => item.sys_file_id || '') : undefined
}
messages.value.push(userMessage)
2025-12-10 17:00:54 +08:00
inputText.value = ''
2025-12-08 19:01:09 +08:00
2025-12-29 12:49:23 +08:00
// 清空已上传的文件
uploadedFiles.value = []
// 调用AI聊天接口携带文件
await callAIChat(query, currentFiles)
2025-12-23 13:27:36 +08:00
}
// 调用AI聊天接口
2025-12-29 12:49:23 +08:00
async function callAIChat(query : string, files : DifyFileInfo[] = []) {
2025-12-23 13:27:36 +08:00
isTyping.value = true
try {
// 如果没有会话ID先创建会话
if (!chatId.value) {
2025-12-23 15:57:11 +08:00
const createRes = await aiChatAPI.createChat({
2025-12-23 13:27:36 +08:00
title: '智能助手对话',
2025-12-23 15:57:11 +08:00
userId: userInfo.value.userId || userInfo.value.wechatId,
agentId: agentId,
userType: userType.value
2025-12-23 13:27:36 +08:00
})
if (createRes.success && createRes.data) {
chatId.value = createRes.data.chatId || ''
console.log('创建会话成功:', chatId.value)
} else {
throw new Error(createRes.message || '创建会话失败')
}
}
2025-12-29 12:49:23 +08:00
// 准备流式对话(包含文件)
2025-12-29 18:40:26 +08:00
const prepareData: ChatPrepareData = {
2025-12-23 13:27:36 +08:00
chatId: chatId.value,
2025-12-23 15:57:11 +08:00
query: query,
agentId: agentId,
userType: userType.value,
2025-12-29 12:49:23 +08:00
userId: userInfo.value.userId,
2025-12-29 18:40:26 +08:00
files: files.length > 0 ? files : undefined,
service: "workcase"
2025-12-29 12:49:23 +08:00
}
console.log('准备流式对话参数:', JSON.stringify(prepareData))
const prepareRes = await aiChatAPI.prepareChatMessageSession(prepareData)
2025-12-23 13:27:36 +08:00
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连接
2025-12-23 15:57:11 +08:00
startStreamChat(sessionId, messageIndex)
2025-12-23 13:27:36 +08:00
} catch (error : any) {
console.error('AI聊天失败:', error)
isTyping.value = false
addMessage('bot', '抱歉AI服务暂时不可用请稍后重试。')
}
}
// SSE 流式对话
2025-12-23 15:57:11 +08:00
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())
}
2025-12-23 13:27:36 +08:00
},
2025-12-23 15:57:11 +08:00
onEnd: (taskId) => {
2025-12-23 13:27:36 +08:00
isTyping.value = false
2025-12-23 15:57:11 +08:00
if (taskId) {
currentTaskId.value = taskId
2025-12-23 13:27:36 +08:00
}
2025-12-23 15:57:11 +08:00
},
onError: (error) => {
console.error('SSE错误:', error)
isTyping.value = false
messages.value[messageIndex].content = error
},
onComplete: () => {
isTyping.value = false
2025-12-23 13:27:36 +08:00
}
})
2025-12-10 17:00:54 +08:00
}
// 添加消息
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)
2025-12-08 19:01:09 +08:00
}
}
2025-12-10 17:00:54 +08:00
// 处理建议操作
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)
}
2025-12-08 19:01:09 +08:00
}
2025-12-25 13:36:53 +08:00
// 直接跳转到工单详情页的 create 模式(复用 workcaseDetail 页面)
async function showCreator() {
// 首页直接创建工单为了让工单和聊天室绑定这里先创建一个聊天室workcase类型再带 roomId 跳转
// 如果你希望“无聊天室也能创建工单”,后端 WorkcaseServiceImpl 也支持 roomId 为空时自动创建聊天室
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: 'workcase',
status: 'active',
aiSessionId: chatId.value || ''
})
uni.hideLoading()
if (res.success && res.data?.roomId) {
const roomId = res.data.roomId
uni.navigateTo({
url: `/pages/workcase/workcaseDetail/workcaseDetail?mode=create&roomId=${roomId}`
})
return
}
uni.showToast({ title: res.message || '创建工单失败', icon: 'none' })
} catch (e) {
uni.hideLoading()
console.error('首页创建工单失败:', e)
uni.showToast({ title: '创建工单失败,请稍后重试', icon: 'none' })
}
2025-12-08 19:01:09 +08:00
}
2025-12-10 17:00:54 +08:00
2025-12-25 13:36:53 +08:00
// 兼容旧逻辑:不再使用页面内工单创建器
2025-12-10 17:00:54 +08:00
function hideCreator() {
showWorkcaseCreator.value = false
}
2025-12-25 13:36:53 +08:00
// 兼容旧逻辑:不再使用页面内工单创建器
2025-12-23 13:27:36 +08:00
function onWorkcaseCreated(workcaseData : TbWorkcaseDTO) {
2025-12-10 17:00:54 +08:00
hideCreator()
2025-12-23 13:27:36 +08:00
addMessage('bot', `工单创建成功!\n类型${workcaseData.type || ''}\n设备${workcaseData.device || ''}\n我们会尽快处理您的问题。`, ['查看工单', '创建新工单'])
2025-12-10 17:00:54 +08:00
}
// 跳转到工单列表
function goToWorkList() {
uni.navigateTo({
2025-12-22 19:16:53 +08:00
url: '/pages/workcase/workcaseList/workcaseList'
})
}
// 跳转到聊天室列表
function goToChatRoomList() {
uni.navigateTo({
url: '/pages/chatRoom/chatRoomList/chatRoomList'
2025-12-10 17:00:54 +08:00
})
}
// 滚动到底部
function scrollToBottom() {
2025-12-23 15:57:11 +08:00
// 先重置再设置大值,确保值变化触发滚动
scrollTop.value = scrollTop.value + 1
nextTick(() => {
scrollTop.value = 999999
})
2025-12-10 17:00:54 +08:00
}
2025-12-23 16:56:22 +08:00
// 联系人工客服 - 创建聊天室并进入
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'
})
2025-12-10 17:00:54 +08:00
}
2025-12-23 16:56:22 +08:00
} catch (error: any) {
uni.hideLoading()
console.error('创建聊天室失败:', error)
uni.showToast({
title: '连接客服失败,请稍后重试',
icon: 'none'
})
}
2025-12-10 17:00:54 +08:00
}
// 处理快速问题
2025-12-23 13:27:36 +08:00
async function handleQuickQuestion(question : string) {
2025-12-22 19:16:53 +08:00
addMessage('user', question)
2025-12-23 13:27:36 +08:00
await callAIChat(question)
2025-12-10 17:00:54 +08:00
}
2025-12-25 12:33:12 +08:00
// Markdown渲染函数返回富文本节点
function renderMarkdown(text : string) : string {
if (!text) return ''
// 转义HTML特殊字符
let html = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// 处理粗体(**语法)
html = html.replace(/\*\*([^\*]+)\*\*/g, '<strong>$1</strong>')
// 处理斜体(*语法,但要避免和粗体冲突)
html = html.replace(/(?<!\*)\*([^\*]+)\*(?!\*)/g, '<em>$1</em>')
// 处理行内代码(`语法)
html = html.replace(/`([^`]+)`/g, '<code style="background-color:#f5f5f5;padding:2px 6px;border-radius:3px;font-family:monospace;color:#e53e3e;">$1</code>')
// 处理链接([text](url)语法)
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color:#0055AA;text-decoration:underline;">$1</a>')
// 处理标题(# ## ###等)
html = html.replace(/^### (.+)$/gm, '<div style="font-size:16px;font-weight:600;margin:8px 0 4px;">$1</div>')
html = html.replace(/^## (.+)$/gm, '<div style="font-size:18px;font-weight:600;margin:10px 0 6px;">$1</div>')
html = html.replace(/^# (.+)$/gm, '<div style="font-size:20px;font-weight:700;margin:12px 0 8px;">$1</div>')
// 处理无序列表(- 或 * 开头)
html = html.replace(/^[*-] (.+)$/gm, '<div style="margin-left:16px;">• $1</div>')
// 处理换行
html = html.replace(/\n/g, '<br/>')
return html
}
2025-12-10 17:00:54 +08:00
// 显示上传选项
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) => {
2025-12-29 12:49:23 +08:00
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
uploadSingleFile(res.tempFilePaths[0])
}
2025-12-10 17:00:54 +08:00
}
})
}
// 从相册选择
function chooseImageFromAlbum() {
uni.chooseImage({
2025-12-29 12:49:23 +08:00
count: 9,
2025-12-10 17:00:54 +08:00
sourceType: ['album'],
success: (res) => {
2025-12-29 12:49:23 +08:00
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
res.tempFilePaths.forEach((filePath: string) => {
uploadSingleFile(filePath)
})
}
2025-12-10 17:00:54 +08:00
}
})
}
// 选择文件
function chooseFile() {
2025-12-29 12:49:23 +08:00
// #ifdef MP-WEIXIN
// 微信小程序使用 chooseMessageFile
uni.chooseMessageFile({
count: 5,
type: 'file',
extension: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt'],
success: (res: any) => {
console.log('选择文件成功:', res)
if (res.tempFiles && res.tempFiles.length > 0) {
res.tempFiles.forEach((file: any) => {
uploadSingleFile(file.path)
})
}
},
fail: (err: any) => {
console.error('选择文件失败:', err)
uni.showToast({
title: '选择文件失败',
icon: 'none'
})
}
2025-12-10 17:00:54 +08:00
})
2025-12-29 12:49:23 +08:00
// #endif
// #ifndef MP-WEIXIN
// 非微信小程序环境
// @ts-ignore
if (typeof uni.chooseFile === 'function') {
// @ts-ignore
uni.chooseFile({
count: 5,
extension: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.txt'],
success: (res: any) => {
console.log('选择文件成功:', res)
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
res.tempFilePaths.forEach((filePath: string) => {
uploadSingleFile(filePath)
})
}
},
fail: (err: any) => {
console.error('选择文件失败:', err)
uni.showToast({
title: '选择文件失败',
icon: 'none'
})
}
})
} else {
uni.showToast({
title: '当前环境不支持文件选择',
icon: 'none'
})
}
// #endif
}
// 上传单个文件
async function uploadSingleFile(filePath: string) {
console.log('开始上传文件:', filePath)
if (!agentId) {
uni.showToast({ title: '智能体未配置', icon: 'none' })
return
}
isUploading.value = true
uni.showLoading({ title: '上传中...' })
try {
const result = await aiChatAPI.uploadFileForChat(filePath, agentId)
console.log('上传结果:', result)
if (result.success && result.data) {
uploadedFiles.value.push(result.data)
uni.showToast({ title: '上传成功', icon: 'success', duration: 1000 })
} else {
uni.showToast({ title: result.message || '上传失败', icon: 'none' })
}
} catch (error: any) {
console.error('文件上传失败:', error)
uni.showToast({ title: '上传失败: ' + (error.message || '未知错误'), icon: 'none' })
} finally {
isUploading.value = false
uni.hideLoading()
}
}
// 移除已上传的文件
function removeUploadedFile(index: number) {
uploadedFiles.value.splice(index, 1)
}
// 判断是否为图片文件
function isImageFile(file: DifyFileInfo): boolean {
return file.type === 'image' || file.mime_type?.startsWith('image/') || false
}
// 获取文件预览URL
function getFilePreviewUrl(file: DifyFileInfo): string {
return file.preview_url || file.source_url || file.url || ''
}
// 获取文件下载URL通过文件ID
function getFileDownloadUrl(fileId: string): string {
return `${FILE_DOWNLOAD_URL}${fileId}`
}
// 判断文件ID对应的文件是否为图片
function isImageFileById(fileId: string): boolean {
// 从缓存中查找文件信息
const file = fileInfoCache.value.get(fileId)
if (file) {
return isImageFile(file)
}
// 如果缓存中没有尝试从uploadedFiles中查找
const uploadedFile = uploadedFiles.value.find(f => f.sys_file_id === fileId)
if (uploadedFile) {
return isImageFile(uploadedFile)
}
return false
}
// 获取文件名(从缓存)
function getFileName(fileId: string): string {
const file = fileInfoCache.value.get(fileId)
return file?.name || fileId.substring(0, 8) + '...'
}
// 文件预览
function previewFile(fileId: string) {
const url = getFileDownloadUrl(fileId)
// 如果是图片,使用图片预览
if (isImageFileById(fileId)) {
uni.previewImage({
urls: [url],
current: url
})
} else {
// 其他文件,提示下载或打开
uni.showModal({
title: '提示',
content: '是否下载该文件?',
success: (res) => {
if (res.confirm) {
uni.downloadFile({
url: url,
success: (downloadRes) => {
if (downloadRes.statusCode === 200) {
uni.showToast({ title: '下载成功', icon: 'success' })
}
}
})
}
}
})
}
2025-12-10 17:00:54 +08:00
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>