refactor(ai-chat): 重构AI聊天国际化文件结构
- 将 ai-chat 语言文件从 ai/ 迁移至 ai-chat/ 目录 - 重构消息组件及多个子组件
This commit is contained in:
99
lang/en-us/ai-chat/index.js
Normal file
99
lang/en-us/ai-chat/index.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
export const lang = {
|
||||||
|
//title for each page
|
||||||
|
title: 'AI Customer Service',
|
||||||
|
|
||||||
|
// ====== Common ======
|
||||||
|
cancel: 'Cancel',
|
||||||
|
save: 'Save',
|
||||||
|
send: 'Send',
|
||||||
|
retry: 'Retry',
|
||||||
|
|
||||||
|
// ====== ai-chat-message ======
|
||||||
|
aiNickname: 'AI Assistant',
|
||||||
|
defaultNickname: 'Me',
|
||||||
|
audioMessage: 'Voice Message',
|
||||||
|
videoMessage: 'Video Message',
|
||||||
|
inputPlaceholder: 'Please enter your question...',
|
||||||
|
expandEditorTitle: 'Edit Content',
|
||||||
|
loadMoreHistory: 'Load more history',
|
||||||
|
loadingHistory: 'Loading history...',
|
||||||
|
aiThinking: 'AI is thinking',
|
||||||
|
receivingReply: 'Receiving reply',
|
||||||
|
aiThinkingStatus1: 'AI is thinking',
|
||||||
|
aiThinkingStatus2: 'AI is analyzing',
|
||||||
|
aiThinkingStatus3: 'AI is searching',
|
||||||
|
aiThinkingStatus4: 'AI is generating',
|
||||||
|
serviceUnavailable: 'Sorry, the AI service is temporarily unavailable. Please try again later.',
|
||||||
|
serviceTemporaryUnavailable: 'Sorry, the service is temporarily unavailable. Please try again later.',
|
||||||
|
noContentToCopy: 'No content to copy',
|
||||||
|
copySuccess: 'Copied',
|
||||||
|
copySelected: 'Copied selected content',
|
||||||
|
copyFail: 'Copy failed',
|
||||||
|
openingFile: 'Opening file...',
|
||||||
|
fileOpenFail: 'Failed to open file',
|
||||||
|
loadHistoryFail: 'Failed to load history',
|
||||||
|
about: '0B',
|
||||||
|
|
||||||
|
// ====== ai-chat-search ======
|
||||||
|
searchEntrance: 'Search',
|
||||||
|
searchPlaceholder: 'Search chat history',
|
||||||
|
searchClose: 'Done',
|
||||||
|
noMatchResult: 'No matching results',
|
||||||
|
searchAtTop: 'Already at top',
|
||||||
|
searchAtBottom: 'Already at bottom',
|
||||||
|
|
||||||
|
// ====== index.vue ======
|
||||||
|
settingsTitle: 'Settings',
|
||||||
|
fallbackWelcome: 'Hello! I am an AI customer service assistant, glad to serve you!\n\nI can help you with:\n• Product-related questions\n• Order issues\n• Technical support\n• Product recommendations\n\nPlease tell me how I can assist you?',
|
||||||
|
changeAvatar: 'Change Avatar',
|
||||||
|
changeAvatarDesc: 'Customize your chat avatar',
|
||||||
|
changeNickname: 'Change Nickname',
|
||||||
|
changeNicknameDesc: 'Customize your nickname',
|
||||||
|
clearChat: 'Clear Chat',
|
||||||
|
exportChat: 'Export Records',
|
||||||
|
help: 'Help',
|
||||||
|
confirmClearChat: 'Are you sure you want to clear the chat?',
|
||||||
|
chatCleared: 'Chat history has been cleared. How can I help you?',
|
||||||
|
featureDeveloping: 'Feature under development',
|
||||||
|
thanksFeedback: 'Thank you for your feedback!',
|
||||||
|
improveService: 'We will improve our service',
|
||||||
|
openLink: 'Open Link',
|
||||||
|
confirmOpenLink: 'Are you sure you want to open this link: ',
|
||||||
|
operationFailed: 'Operation failed, please try again',
|
||||||
|
prompt: 'Prompt',
|
||||||
|
serviceTypeNotSupported: 'Current service type does not support this page',
|
||||||
|
|
||||||
|
// ====== ai-chat-avatar-selector ======
|
||||||
|
avatarSelectorTitle: 'Change Avatar',
|
||||||
|
chooseFromAlbum: 'Choose from Album',
|
||||||
|
avatarChangeSuccess: 'Avatar changed successfully',
|
||||||
|
enableAlbumPermission: 'Please enable album permission',
|
||||||
|
|
||||||
|
// ====== ai-chat-emoji-panel ======
|
||||||
|
emojiTitle: 'Emoji',
|
||||||
|
|
||||||
|
// ====== ai-chat-nickname-editor ======
|
||||||
|
editNickname: 'Edit Nickname',
|
||||||
|
newNicknamePlaceholder: 'Please enter your new nickname',
|
||||||
|
nicknameTip: 'Nickname length 1-12 characters, visible only to you',
|
||||||
|
nicknameChangeSuccess: 'Nickname changed successfully',
|
||||||
|
|
||||||
|
// ====== ai-chat-tools-panel ======
|
||||||
|
moreFunctions: 'More Features',
|
||||||
|
image: 'Image',
|
||||||
|
file: 'File',
|
||||||
|
voice: 'Voice',
|
||||||
|
location: 'Location',
|
||||||
|
|
||||||
|
// ====== ai-chat-voice-panel ======
|
||||||
|
voiceInputTitle: 'Voice Input',
|
||||||
|
recordingTip: 'Recording, release to end',
|
||||||
|
startRecordTip: 'Tap to start recording',
|
||||||
|
voiceInputFailed: 'Voice input failed',
|
||||||
|
|
||||||
|
// ====== index.vue - Menu ======
|
||||||
|
menuClearChat: 'Clear Chat',
|
||||||
|
menuExport: 'Export Records',
|
||||||
|
menuSettings: 'Settings',
|
||||||
|
menuHelp: 'Help'
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export const lang = {
|
|
||||||
//title为每个页面的标题
|
|
||||||
title: 'AI Intelligent Customer Service'
|
|
||||||
}
|
|
||||||
99
lang/zh-cn/ai-chat/index.js
Normal file
99
lang/zh-cn/ai-chat/index.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
export const lang = {
|
||||||
|
//title为每个页面的标题
|
||||||
|
title: 'AI智能客服',
|
||||||
|
|
||||||
|
// ====== 通用 ======
|
||||||
|
cancel: '取消',
|
||||||
|
save: '保存',
|
||||||
|
send: '发送',
|
||||||
|
retry: '重试',
|
||||||
|
|
||||||
|
// ====== ai-chat-message ======
|
||||||
|
aiNickname: '智能助手',
|
||||||
|
defaultNickname: '我',
|
||||||
|
audioMessage: '语音消息',
|
||||||
|
videoMessage: '视频消息',
|
||||||
|
inputPlaceholder: '请输入您的问题...',
|
||||||
|
expandEditorTitle: '编辑内容',
|
||||||
|
loadMoreHistory: '加载更多历史消息',
|
||||||
|
loadingHistory: '加载历史消息中...',
|
||||||
|
aiThinking: 'AI正在思考',
|
||||||
|
receivingReply: '正在接收回复',
|
||||||
|
aiThinkingStatus1: 'AI正在思考',
|
||||||
|
aiThinkingStatus2: 'AI正在分析',
|
||||||
|
aiThinkingStatus3: 'AI正在检索',
|
||||||
|
aiThinkingStatus4: 'AI正在生成',
|
||||||
|
serviceUnavailable: '抱歉,AI服务暂时不可用,请稍后重试。',
|
||||||
|
serviceTemporaryUnavailable: '抱歉,服务暂时不可用,请稍后重试。',
|
||||||
|
noContentToCopy: '无可复制的内容',
|
||||||
|
copySuccess: '已复制',
|
||||||
|
copySelected: '已复制选中内容',
|
||||||
|
copyFail: '复制失败',
|
||||||
|
openingFile: '打开文件中...',
|
||||||
|
fileOpenFail: '文件打开失败',
|
||||||
|
loadHistoryFail: '加载历史失败',
|
||||||
|
about: '0B',
|
||||||
|
|
||||||
|
// ====== ai-chat-search ======
|
||||||
|
searchEntrance: '搜索',
|
||||||
|
searchPlaceholder: '搜索聊天记录',
|
||||||
|
searchClose: '完成',
|
||||||
|
noMatchResult: '未找到匹配结果',
|
||||||
|
searchAtTop: '已到顶部',
|
||||||
|
searchAtBottom: '已到底部',
|
||||||
|
|
||||||
|
// ====== index.vue ======
|
||||||
|
settingsTitle: '设置',
|
||||||
|
fallbackWelcome: '您好!我是AI智能客服,很高兴为您服务!\n\n我可以帮您:\n• 解答产品相关问题\n• 处理订单问题\n• 提供技术支持\n• 推荐相关商品\n\n请告诉我您需要什么帮助?',
|
||||||
|
changeAvatar: '更换头像',
|
||||||
|
changeAvatarDesc: '自定义您的聊天头像',
|
||||||
|
changeNickname: '更换昵称',
|
||||||
|
changeNicknameDesc: '自定义您的昵称',
|
||||||
|
clearChat: '清空聊天',
|
||||||
|
exportChat: '导出记录',
|
||||||
|
help: '帮助',
|
||||||
|
confirmClearChat: '确定要清空聊天记录吗?',
|
||||||
|
chatCleared: '聊天记录已清空,有什么可以帮助您的吗?',
|
||||||
|
featureDeveloping: '功能开发中',
|
||||||
|
thanksFeedback: '感谢您的反馈!',
|
||||||
|
improveService: '我们会改进服务',
|
||||||
|
openLink: '打开链接',
|
||||||
|
confirmOpenLink: '确定要打开链接:',
|
||||||
|
operationFailed: '操作失败,请重试',
|
||||||
|
prompt: '提示',
|
||||||
|
serviceTypeNotSupported: '当前客服类型不支持此页面',
|
||||||
|
|
||||||
|
// ====== ai-chat-avatar-selector ======
|
||||||
|
avatarSelectorTitle: '更换头像',
|
||||||
|
chooseFromAlbum: '从相册选择',
|
||||||
|
avatarChangeSuccess: '头像更换成功',
|
||||||
|
enableAlbumPermission: '请开启相册权限',
|
||||||
|
|
||||||
|
// ====== ai-chat-emoji-panel ======
|
||||||
|
emojiTitle: '表情',
|
||||||
|
|
||||||
|
// ====== ai-chat-nickname-editor ======
|
||||||
|
editNickname: '修改昵称',
|
||||||
|
newNicknamePlaceholder: '请输入您的新昵称',
|
||||||
|
nicknameTip: '昵称长度限制1-12个字符,仅自己可见',
|
||||||
|
nicknameChangeSuccess: '昵称修改成功',
|
||||||
|
|
||||||
|
// ====== ai-chat-tools-panel ======
|
||||||
|
moreFunctions: '更多功能',
|
||||||
|
image: '图片',
|
||||||
|
file: '文件',
|
||||||
|
voice: '语音',
|
||||||
|
location: '位置',
|
||||||
|
|
||||||
|
// ====== ai-chat-voice-panel ======
|
||||||
|
voiceInputTitle: '语音输入',
|
||||||
|
recordingTip: '正在录音,松开结束',
|
||||||
|
startRecordTip: '点击开始录音',
|
||||||
|
voiceInputFailed: '语音输入失败',
|
||||||
|
|
||||||
|
// ====== index.vue - 菜单 ======
|
||||||
|
menuClearChat: '清空聊天',
|
||||||
|
menuExport: '导出记录',
|
||||||
|
menuSettings: '设置',
|
||||||
|
menuHelp: '帮助'
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export const lang = {
|
|
||||||
//title为每个页面的标题
|
|
||||||
title: 'AI智能客服'
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
<ai-chat-search
|
<ai-chat-search
|
||||||
ref="searchRef"
|
ref="searchRef"
|
||||||
:messages="messages"
|
:messages="messages"
|
||||||
|
:show-label="false"
|
||||||
@scroll-to="onSearchScrollTo"
|
@scroll-to="onSearchScrollTo"
|
||||||
@active-change="onSearchActiveChange"
|
@active-change="onSearchActiveChange"
|
||||||
@result-change="onSearchResultChange"
|
@result-change="onSearchResultChange"
|
||||||
@@ -73,7 +74,7 @@
|
|||||||
<view class="message-content">
|
<view class="message-content">
|
||||||
<!-- AI昵称固定,不可修改 - 左移到气泡上方 -->
|
<!-- AI昵称固定,不可修改 - 左移到气泡上方 -->
|
||||||
<view class="message-nickname ai-nickname">
|
<view class="message-nickname ai-nickname">
|
||||||
<text class="ai-nickname-text">智能助手</text>
|
<text class="ai-nickname-text">{{ $lang('aiNickname') }}</text>
|
||||||
<text class="message-time">{{ formatTime(message.timestamp) }}</text>
|
<text class="message-time">{{ formatTime(message.timestamp) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="message-bubble"
|
<view class="message-bubble"
|
||||||
@@ -118,7 +119,7 @@
|
|||||||
<view v-else-if="message.type === 'audio'" class="audio-content">
|
<view v-else-if="message.type === 'audio'" class="audio-content">
|
||||||
<view class="audio-player">
|
<view class="audio-player">
|
||||||
<view class="audio-info">
|
<view class="audio-info">
|
||||||
<text class="audio-title">{{ message.title || '语音消息' }}</text>
|
<text class="audio-title">{{ message.title || $lang('audioMessage') }}</text>
|
||||||
<text class="audio-duration">{{ formatDuration(message.duration) }}</text>
|
<text class="audio-duration">{{ formatDuration(message.duration) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="audio-controls">
|
<view class="audio-controls">
|
||||||
@@ -156,7 +157,7 @@
|
|||||||
@pause="onVideoPause(message)">
|
@pause="onVideoPause(message)">
|
||||||
</video>
|
</video>
|
||||||
<view class="video-info">
|
<view class="video-info">
|
||||||
<text class="video-title">{{ message.title || '视频消息' }}</text>
|
<text class="video-title">{{ message.title || $lang('videoMessage') }}</text>
|
||||||
<text class="video-duration">{{ formatDuration(message.duration) }}</text>
|
<text class="video-duration">{{ formatDuration(message.duration) }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -186,7 +187,7 @@
|
|||||||
<text class="dot"></text>
|
<text class="dot"></text>
|
||||||
</view>
|
</view>
|
||||||
<text class="loading-text">
|
<text class="loading-text">
|
||||||
<text class="loading-status">{{ message.loadingText || 'AI正在思考' }}</text>
|
<text class="loading-status">{{ message.loadingText || $lang('aiThinking') }}</text>
|
||||||
<text class="loading-ellipsis">
|
<text class="loading-ellipsis">
|
||||||
<text class="ellipsis-dot">.</text>
|
<text class="ellipsis-dot">.</text>
|
||||||
<text class="ellipsis-dot">.</text>
|
<text class="ellipsis-dot">.</text>
|
||||||
@@ -231,7 +232,7 @@
|
|||||||
<textarea
|
<textarea
|
||||||
v-model="inputText"
|
v-model="inputText"
|
||||||
class="message-input"
|
class="message-input"
|
||||||
placeholder="请输入您的问题..."
|
:placeholder="$lang('inputPlaceholder')"
|
||||||
:maxlength="500"
|
:maxlength="500"
|
||||||
:auto-height="true"
|
:auto-height="true"
|
||||||
:show-confirm-bar="false"
|
:show-confirm-bar="false"
|
||||||
@@ -257,7 +258,7 @@
|
|||||||
:class="{ disabled: !canSendMessage }"
|
:class="{ disabled: !canSendMessage }"
|
||||||
@click="sendMessage"
|
@click="sendMessage"
|
||||||
:disabled="!canSendMessage">
|
:disabled="!canSendMessage">
|
||||||
<text>发送</text>
|
<text>{{ $lang('send') }}</text>
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -282,7 +283,7 @@
|
|||||||
<view class="expand-editor-mask" @click="closeExpandEditor"></view>
|
<view class="expand-editor-mask" @click="closeExpandEditor"></view>
|
||||||
<view class="expand-editor-panel">
|
<view class="expand-editor-panel">
|
||||||
<view class="expand-editor-header">
|
<view class="expand-editor-header">
|
||||||
<text>编辑内容</text>
|
<text>{{ $lang('expandEditorTitle') }}</text>
|
||||||
<button class="close-btn" @click="closeExpandEditor">
|
<button class="close-btn" @click="closeExpandEditor">
|
||||||
<text>×</text>
|
<text>×</text>
|
||||||
</button>
|
</button>
|
||||||
@@ -290,15 +291,15 @@
|
|||||||
<textarea
|
<textarea
|
||||||
v-model="expandInputText"
|
v-model="expandInputText"
|
||||||
class="expand-editor-input"
|
class="expand-editor-input"
|
||||||
placeholder="请输入您的问题..."
|
:placeholder="$lang('inputPlaceholder')"
|
||||||
:maxlength="500"
|
:maxlength="500"
|
||||||
:auto-height="true"
|
:auto-height="true"
|
||||||
:show-confirm-bar="false"
|
:show-confirm-bar="false"
|
||||||
:adjust-position="true"
|
:adjust-position="true"
|
||||||
:focus="true" />
|
:focus="true" />
|
||||||
<view class="expand-editor-footer">
|
<view class="expand-editor-footer">
|
||||||
<button class="expand-cancel-btn" @click="closeExpandEditor">取消</button>
|
<button class="expand-cancel-btn" @click="closeExpandEditor">{{ $lang('cancel') }}</button>
|
||||||
<button class="expand-send-btn" @click="sendFromExpandEditor" :disabled="!expandInputText.trim()">发送</button>
|
<button class="expand-send-btn" @click="sendFromExpandEditor" :disabled="!expandInputText.trim()">{{ $lang('send') }}</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -396,7 +397,7 @@ export default {
|
|||||||
scrollTop: 0,
|
scrollTop: 0,
|
||||||
scrollIntoViewId: null,
|
scrollIntoViewId: null,
|
||||||
hasMoreHistory: false,
|
hasMoreHistory: false,
|
||||||
loadingText: '加载更多历史消息',
|
loadingText: this.$lang('loadMoreHistory'),
|
||||||
voiceInputing: false,
|
voiceInputing: false,
|
||||||
audioContext: null,
|
audioContext: null,
|
||||||
currentAudio: null,
|
currentAudio: null,
|
||||||
@@ -416,7 +417,7 @@ export default {
|
|||||||
localUserAvatar: '',
|
localUserAvatar: '',
|
||||||
showAvatarSelector: false, // 头像选择弹窗控制
|
showAvatarSelector: false, // 头像选择弹窗控制
|
||||||
localUserNickname: '', // 当前用户昵称
|
localUserNickname: '', // 当前用户昵称
|
||||||
defaultNickname: '我', // 默认昵称
|
defaultNickname: this.$lang('defaultNickname'), // 默认昵称
|
||||||
showNicknameEditor: false, // 昵称编辑弹窗控制
|
showNicknameEditor: false, // 昵称编辑弹窗控制
|
||||||
// AI头像错误状态
|
// AI头像错误状态
|
||||||
avatarError: false,
|
avatarError: false,
|
||||||
@@ -823,12 +824,12 @@ async loadChatHistoryIfExist() {
|
|||||||
type: 'loading',
|
type: 'loading',
|
||||||
content: '',
|
content: '',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
loadingText: 'AI正在思考',
|
loadingText: this.$lang('aiThinking'),
|
||||||
_loadingIndex: 0
|
_loadingIndex: 0
|
||||||
}
|
}
|
||||||
this.shouldScrollToBottom = true
|
this.shouldScrollToBottom = true
|
||||||
this.messages.push(loadingMessage)
|
this.messages.push(loadingMessage)
|
||||||
|
|
||||||
// 启动处理状态文字轮换
|
// 启动处理状态文字轮换
|
||||||
this._startLoadingTextRotation(loadingMessage)
|
this._startLoadingTextRotation(loadingMessage)
|
||||||
|
|
||||||
@@ -856,10 +857,10 @@ async loadChatHistoryIfExist() {
|
|||||||
id: ++this.messageId,
|
id: ++this.messageId,
|
||||||
role: 'ai',
|
role: 'ai',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
content: '抱歉,AI服务暂时不可用,请稍后重试。',
|
content: this.$lang('serviceUnavailable'),
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
actions: [
|
actions: [
|
||||||
{ id: 1, text: '重试', type: 'retry' }
|
{ id: 1, text: this.$lang('retry'), type: 'retry' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
this.shouldScrollToBottom = true
|
this.shouldScrollToBottom = true
|
||||||
@@ -940,7 +941,7 @@ async sendStreamMessage(userMessage) {
|
|||||||
this.isStreaming = true
|
this.isStreaming = true
|
||||||
const loadingMsg = this.messages[loadingIdx]
|
const loadingMsg = this.messages[loadingIdx]
|
||||||
this._stopLoadingTextRotation(loadingMsg)
|
this._stopLoadingTextRotation(loadingMsg)
|
||||||
loadingMsg.loadingText = '正在接收回复'
|
loadingMsg.loadingText = this.$lang('receivingReply')
|
||||||
|
|
||||||
let transformed = false
|
let transformed = false
|
||||||
|
|
||||||
@@ -1006,15 +1007,16 @@ async sendStreamMessage(userMessage) {
|
|||||||
id: ++this.messageId,
|
id: ++this.messageId,
|
||||||
role: 'ai',
|
role: 'ai',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
content: '抱歉,服务暂时不可用,请稍后重试。',
|
content: this.$lang('serviceTemporaryUnavailable'),
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
actions: [{ id: 1, text: '重试', type: 'retry' }]
|
actions: [{ id: 1, text: this.$lang('retry'), type: 'retry' }]
|
||||||
}
|
}
|
||||||
this.messages.push(errorMsg)
|
this.messages.push(errorMsg)
|
||||||
this.$emit('ai-response', errorMsg)
|
this.$emit('ai-response', errorMsg)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
// 完成流式消息处理(共用逻辑)
|
// 完成流式消息处理(共用逻辑)
|
||||||
_finishStream(msg, result) {
|
_finishStream(msg, result) {
|
||||||
msg.isStreaming = false
|
msg.isStreaming = false
|
||||||
@@ -1051,9 +1053,9 @@ _handleStreamError(msg) {
|
|||||||
id: ++this.messageId,
|
id: ++this.messageId,
|
||||||
role: 'ai',
|
role: 'ai',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
content: '抱歉,服务暂时不可用,请稍后重试。',
|
content: this.$lang('serviceTemporaryUnavailable'),
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
actions: [{ id: 1, text: '重试', type: 'retry' }]
|
actions: [{ id: 1, text: this.$lang('retry'), type: 'retry' }]
|
||||||
}
|
}
|
||||||
this.messages.push(errorMsg)
|
this.messages.push(errorMsg)
|
||||||
this.$emit('ai-response', errorMsg)
|
this.$emit('ai-response', errorMsg)
|
||||||
@@ -1339,9 +1341,9 @@ _handleStreamError(msg) {
|
|||||||
|
|
||||||
// 格式化文件大小
|
// 格式化文件大小
|
||||||
formatFileSize(bytes) {
|
formatFileSize(bytes) {
|
||||||
if (!bytes) return '0B'
|
if (!bytes) return this.$lang('about')
|
||||||
const k = 1024
|
const k = 1024
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
const sizes = [this.$lang('about'), 'KB', 'MB', 'GB']
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + sizes[i]
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + sizes[i]
|
||||||
},
|
},
|
||||||
@@ -1429,7 +1431,7 @@ _handleStreamError(msg) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载历史失败:', error);
|
console.error('加载历史失败:', error);
|
||||||
uni.showToast({ title: '加载历史失败', icon: 'none' });
|
uni.showToast({ title: this.$lang('loadHistoryFail'), icon: 'none' });
|
||||||
this.hasMoreHistory = false;
|
this.hasMoreHistory = false;
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoadingHistory = false;
|
this.isLoadingHistory = false;
|
||||||
@@ -1444,7 +1446,7 @@ _handleStreamError(msg) {
|
|||||||
|
|
||||||
// 预览文件
|
// 预览文件
|
||||||
previewFile(message) {
|
previewFile(message) {
|
||||||
uni.showLoading({ title: '打开文件中...' })
|
uni.showLoading({ title: this.$lang('openingFile') })
|
||||||
|
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
window.open(message.url, '_blank')
|
window.open(message.url, '_blank')
|
||||||
@@ -1454,7 +1456,7 @@ _handleStreamError(msg) {
|
|||||||
plus.runtime.openFile(message.url, {
|
plus.runtime.openFile(message.url, {
|
||||||
error: (e) => {
|
error: (e) => {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '文件打开失败',
|
title: this.$lang('fileOpenFail'),
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1628,7 +1630,7 @@ _handleStreamError(msg) {
|
|||||||
copyText = message.content || ''
|
copyText = message.content || ''
|
||||||
}
|
}
|
||||||
if (!copyText) {
|
if (!copyText) {
|
||||||
uni.showToast({ title: '无可复制的内容', icon: 'none' })
|
uni.showToast({ title: this.$lang('noContentToCopy'), icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1661,12 +1663,12 @@ _handleStreamError(msg) {
|
|||||||
data: copyText,
|
data: copyText,
|
||||||
success: () => {
|
success: () => {
|
||||||
console.log('[AI Chat] 复制成功')
|
console.log('[AI Chat] 复制成功')
|
||||||
uni.showToast({ title: '已复制', icon: 'success' })
|
uni.showToast({ title: this.$lang('copySuccess'), icon: 'success' })
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
console.error('[AI Chat] 复制失败, 详细错误:', JSON.stringify(err))
|
console.error('[AI Chat] 复制失败, 详细错误:', JSON.stringify(err))
|
||||||
console.error('[AI Chat] 复制失败 errMsg:', err.errMsg || err.message || '无错误消息')
|
console.error('[AI Chat] 复制失败 errMsg:', err.errMsg || err.message || '无错误消息')
|
||||||
uni.showToast({ title: '复制失败: ' + (err.errMsg || '未知错误'), icon: 'none', duration: 3000 })
|
uni.showToast({ title: this.$lang('copyFail') + ': ' + (err.errMsg || this.$lang('serviceUnavailable')), icon: 'none', duration: 3000 })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// #endif
|
// #endif
|
||||||
@@ -1687,12 +1689,12 @@ _handleStreamError(msg) {
|
|||||||
const success = document.execCommand('copy')
|
const success = document.execCommand('copy')
|
||||||
document.body.removeChild(textarea)
|
document.body.removeChild(textarea)
|
||||||
if (success) {
|
if (success) {
|
||||||
uni.showToast({ title: '已复制', icon: 'success' })
|
uni.showToast({ title: this.$lang('copySuccess'), icon: 'success' })
|
||||||
} else {
|
} else {
|
||||||
uni.showToast({ title: '复制失败', icon: 'none' })
|
uni.showToast({ title: this.$lang('copyFail'), icon: 'none' })
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
uni.showToast({ title: '复制失败', icon: 'none' })
|
uni.showToast({ title: this.$lang('copyFail'), icon: 'none' })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1795,10 +1797,10 @@ _handleStreamError(msg) {
|
|||||||
type: 'loading',
|
type: 'loading',
|
||||||
content: '',
|
content: '',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
loadingText: 'AI正在思考',
|
loadingText: this.$lang('aiThinking'),
|
||||||
_loadingIndex: 0
|
_loadingIndex: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
this.messages.push(loadingMessage)
|
this.messages.push(loadingMessage)
|
||||||
this._startLoadingTextRotation(loadingMessage)
|
this._startLoadingTextRotation(loadingMessage)
|
||||||
|
|
||||||
@@ -1826,10 +1828,10 @@ _handleStreamError(msg) {
|
|||||||
id: ++this.messageId,
|
id: ++this.messageId,
|
||||||
role: 'ai',
|
role: 'ai',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
content: '抱歉,AI服务暂时不可用,请稍后重试。',
|
content: this.$lang('serviceUnavailable'),
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
actions: [
|
actions: [
|
||||||
{ id: 1, text: '重试', type: 'retry' }
|
{ id: 1, text: this.$lang('retry'), type: 'retry' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<view class="avatar-selector-mask" @click="close"></view>
|
<view class="avatar-selector-mask" @click="close"></view>
|
||||||
<view class="avatar-selector-panel">
|
<view class="avatar-selector-panel">
|
||||||
<view class="avatar-selector-header">
|
<view class="avatar-selector-header">
|
||||||
<text>更换头像</text>
|
<text>{{ $lang('avatarSelectorTitle') }}</text>
|
||||||
<button class="close-btn" @click="close">
|
<button class="close-btn" @click="close">
|
||||||
<text class="iconfont icon-close"></text>
|
<text class="iconfont icon-close"></text>
|
||||||
</button>
|
</button>
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
<view class="avatar-selector-actions">
|
<view class="avatar-selector-actions">
|
||||||
<button class="avatar-select-action" @click="chooseAvatarFromAlbum">
|
<button class="avatar-select-action" @click="chooseAvatarFromAlbum">
|
||||||
<text class="iconfont icon-album"></text>
|
<text class="iconfont icon-album"></text>
|
||||||
<text>从相册选择</text>
|
<text>{{ $lang('chooseFromAlbum') }}</text>
|
||||||
</button>
|
</button>
|
||||||
<button class="avatar-select-action cancel" @click="close">
|
<button class="avatar-select-action cancel" @click="close">
|
||||||
<text>取消</text>
|
<text>{{ $lang('cancel') }}</text>
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -80,7 +80,7 @@ export default {
|
|||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
this.$emit('avatar-change', tempFilePath)
|
this.$emit('avatar-change', tempFilePath)
|
||||||
this.close()
|
this.close()
|
||||||
uni.showToast({ title: '头像更换成功', icon: 'success', duration: 1500 })
|
uni.showToast({ title: this.$lang('avatarChangeSuccess'), icon: 'success', duration: 1500 })
|
||||||
} else {
|
} else {
|
||||||
this.close()
|
this.close()
|
||||||
}
|
}
|
||||||
@@ -99,11 +99,11 @@ export default {
|
|||||||
uni.setStorageSync('user_avatar', tempFilePath)
|
uni.setStorageSync('user_avatar', tempFilePath)
|
||||||
this.$emit('avatar-change', tempFilePath)
|
this.$emit('avatar-change', tempFilePath)
|
||||||
this.close()
|
this.close()
|
||||||
uni.showToast({ title: '头像更换成功', icon: 'success', duration: 1500 })
|
uni.showToast({ title: this.$lang('avatarChangeSuccess'), icon: 'success', duration: 1500 })
|
||||||
},
|
},
|
||||||
fail: (error) => {
|
fail: (error) => {
|
||||||
if (error.errMsg && error.errMsg.includes('deny')) {
|
if (error.errMsg && error.errMsg.includes('deny')) {
|
||||||
uni.showToast({ title: '请开启相册权限', icon: 'none', duration: 2000 })
|
uni.showToast({ title: this.$lang('enableAlbumPermission'), icon: 'none', duration: 2000 })
|
||||||
}
|
}
|
||||||
this.close()
|
this.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<view class="emoji-mask" @click="hideEmojiPanel"></view>
|
<view class="emoji-mask" @click="hideEmojiPanel"></view>
|
||||||
<view class="emoji-panel">
|
<view class="emoji-panel">
|
||||||
<view class="emoji-header">
|
<view class="emoji-header">
|
||||||
<text>表情</text>
|
<text>{{ $lang('emojiTitle') }}</text>
|
||||||
<button class="close-btn" @click="hideEmojiPanel">
|
<button class="close-btn" @click="hideEmojiPanel">
|
||||||
<text>×</text>
|
<text>×</text>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<view class="nickname-editor-mask" @click="close"></view>
|
<view class="nickname-editor-mask" @click="close"></view>
|
||||||
<view class="nickname-editor-panel">
|
<view class="nickname-editor-panel">
|
||||||
<view class="nickname-editor-header">
|
<view class="nickname-editor-header">
|
||||||
<text>修改昵称</text>
|
<text>{{ $lang('editNickname') }}</text>
|
||||||
<button class="close-btn" @click="close">
|
<button class="close-btn" @click="close">
|
||||||
<text class="iconfont icon-close"></text>
|
<text class="iconfont icon-close"></text>
|
||||||
</button>
|
</button>
|
||||||
@@ -13,21 +13,21 @@
|
|||||||
<input
|
<input
|
||||||
v-model="tempNickname"
|
v-model="tempNickname"
|
||||||
class="nickname-input"
|
class="nickname-input"
|
||||||
placeholder="请输入您的新昵称"
|
:placeholder="$lang('newNicknamePlaceholder')"
|
||||||
:maxlength="12"
|
:maxlength="12"
|
||||||
type="text" />
|
type="text" />
|
||||||
</view>
|
</view>
|
||||||
<view class="nickname-tip">昵称长度限制1-12个字符,仅自己可见</view>
|
<view class="nickname-tip">{{ $lang('nicknameTip') }}</view>
|
||||||
<view class="nickname-editor-actions">
|
<view class="nickname-editor-actions">
|
||||||
<button class="nickname-action-btn cancel" @click="close">
|
<button class="nickname-action-btn cancel" @click="close">
|
||||||
<text>取消</text>
|
<text>{{ $lang('cancel') }}</text>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="nickname-action-btn confirm"
|
class="nickname-action-btn confirm"
|
||||||
:class="{ disabled: !tempNickname.trim() }"
|
:class="{ disabled: !tempNickname.trim() }"
|
||||||
:disabled="!tempNickname.trim()"
|
:disabled="!tempNickname.trim()"
|
||||||
@click="saveNickname">
|
@click="saveNickname">
|
||||||
<text>保存</text>
|
<text>{{ $lang('save') }}</text>
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -87,7 +87,7 @@ export default {
|
|||||||
uni.setStorageSync('user_nickname', newNickname)
|
uni.setStorageSync('user_nickname', newNickname)
|
||||||
this.$emit('nickname-change', newNickname)
|
this.$emit('nickname-change', newNickname)
|
||||||
this.close()
|
this.close()
|
||||||
uni.showToast({ title: '昵称修改成功', icon: 'success', duration: 1500 })
|
uni.showToast({ title: this.$lang('nicknameChangeSuccess'), icon: 'success', duration: 1500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="ai-chat-search">
|
<view class="ai-chat-search">
|
||||||
<!-- 搜索入口按钮(浮动在消息区右上角) -->
|
<!-- 搜索入口按钮(浮动在消息区右上角) -->
|
||||||
<view class="search-entrance" v-if="!showSearchBar" @click="toggleSearch">
|
<view class="search-entrance" :class="{ 'icon-only': !showLabel }" v-if="!showSearchBar" @click="toggleSearch">
|
||||||
<text class="iconfont icon-sousuo search-entrance-icon"></text>
|
<text class="iconfont icon-sousuo search-entrance-icon"></text>
|
||||||
<text class="search-entrance-label">搜索</text>
|
<text class="search-entrance-label" v-if="showLabel">{{ $lang('searchEntrance') }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 搜索浮条 -->
|
<!-- 搜索浮条 -->
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<input
|
<input
|
||||||
class="search-bar-input"
|
class="search-bar-input"
|
||||||
v-model="searchKeyword"
|
v-model="searchKeyword"
|
||||||
placeholder="搜索聊天记录"
|
:placeholder="$lang('searchPlaceholder')"
|
||||||
:focus="true"
|
:focus="true"
|
||||||
@input="onSearchInput"
|
@input="onSearchInput"
|
||||||
@confirm="doSearch" />
|
@confirm="doSearch" />
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<text class="search-bar-nav" :class="{ disabled: searchResultIndex >= searchResults.length - 1 }" @click="scrollToNextResult">▼</text>
|
<text class="search-bar-nav" :class="{ disabled: searchResultIndex >= searchResults.length - 1 }" @click="scrollToNextResult">▼</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="search-bar-close" @click="closeSearch">完成</text>
|
<text class="search-bar-close" @click="closeSearch">{{ $lang('searchClose') }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
*
|
*
|
||||||
* Props:
|
* Props:
|
||||||
* messages: Array - 原始消息列表
|
* messages: Array - 原始消息列表
|
||||||
|
* showLabel: Boolean - (默认 true) 是否显示"搜索"文字;设为 false 则只显示搜索图标
|
||||||
*
|
*
|
||||||
* Events:
|
* Events:
|
||||||
* scroll-to: (messageKey: string) => void - 请求父组件滚动到指定消息
|
* scroll-to: (messageKey: string) => void - 请求父组件滚动到指定消息
|
||||||
@@ -53,6 +54,11 @@ export default {
|
|||||||
messages: {
|
messages: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
// 是否显示文字标签;设为 false 则只显示搜索图标
|
||||||
|
showLabel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -66,6 +72,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
// 过滤后的消息列表(与 v-for 保持一致)
|
// 过滤后的消息列表(与 v-for 保持一致)
|
||||||
filteredMessages() {
|
filteredMessages() {
|
||||||
return this.messages.filter(
|
return this.messages.filter(
|
||||||
@@ -150,7 +157,7 @@ export default {
|
|||||||
this._emitScrollTo(filteredIdx)
|
this._emitScrollTo(filteredIdx)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
uni.showToast({ title: '未找到匹配结果', icon: 'none' })
|
uni.showToast({ title: this.$lang('noMatchResult'), icon: 'none' })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -174,7 +181,7 @@ export default {
|
|||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
this.scrollToSearchResult(idx)
|
this.scrollToSearchResult(idx)
|
||||||
} else {
|
} else {
|
||||||
uni.showToast({ title: '已到顶部', icon: 'none' })
|
uni.showToast({ title: this.$lang('searchAtTop'), icon: 'none' })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -183,7 +190,7 @@ export default {
|
|||||||
if (idx < this.searchResults.length) {
|
if (idx < this.searchResults.length) {
|
||||||
this.scrollToSearchResult(idx)
|
this.scrollToSearchResult(idx)
|
||||||
} else {
|
} else {
|
||||||
uni.showToast({ title: '已到底部', icon: 'none' })
|
uni.showToast({ title: this.$lang('searchAtBottom'), icon: 'none' })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -251,7 +258,7 @@ export default {
|
|||||||
// 搜索入口按钮
|
// 搜索入口按钮
|
||||||
.search-entrance {
|
.search-entrance {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 88rpx;
|
top: 44rpx;
|
||||||
right: 16rpx;
|
right: 16rpx;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -277,6 +284,15 @@ export default {
|
|||||||
&:active {
|
&:active {
|
||||||
background: rgba(255, 255, 255, 0.8);
|
background: rgba(255, 255, 255, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 仅显示图标模式(圆形小按钮)
|
||||||
|
&.icon-only {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
padding: 0;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索浮条
|
// 搜索浮条
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<view class="tools-mask" @click="hideToolsPanel"></view>
|
<view class="tools-mask" @click="hideToolsPanel"></view>
|
||||||
<view class="tools-panel">
|
<view class="tools-panel">
|
||||||
<view class="tools-header">
|
<view class="tools-header">
|
||||||
<text>更多功能</text>
|
<text>{{ $lang('moreFunctions') }}</text>
|
||||||
<button class="close-btn" @click="hideToolsPanel">
|
<button class="close-btn" @click="hideToolsPanel">
|
||||||
<text class="iconfont icon-close"></text>
|
<text class="iconfont icon-close"></text>
|
||||||
</button>
|
</button>
|
||||||
@@ -13,25 +13,25 @@
|
|||||||
<view class="tool-icon">
|
<view class="tool-icon">
|
||||||
<text class="iconfont icon-image"></text>
|
<text class="iconfont icon-image"></text>
|
||||||
</view>
|
</view>
|
||||||
<text class="tool-text">图片</text>
|
<text class="tool-text">{{ $lang('image') }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="tool-item" @click="selectFile">
|
<view class="tool-item" @click="selectFile">
|
||||||
<view class="tool-icon">
|
<view class="tool-icon">
|
||||||
<text class="iconfont icon-file"></text>
|
<text class="iconfont icon-file"></text>
|
||||||
</view>
|
</view>
|
||||||
<text class="tool-text">文件</text>
|
<text class="tool-text">{{ $lang('file') }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="tool-item" @click="startRecord">
|
<view class="tool-item" @click="startRecord">
|
||||||
<view class="tool-icon">
|
<view class="tool-icon">
|
||||||
<text class="iconfont icon-voice"></text>
|
<text class="iconfont icon-voice"></text>
|
||||||
</view>
|
</view>
|
||||||
<text class="tool-text">语音</text>
|
<text class="tool-text">{{ $lang('voice') }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="tool-item" @click="selectLocation">
|
<view class="tool-item" @click="selectLocation">
|
||||||
<view class="tool-icon">
|
<view class="tool-icon">
|
||||||
<text class="iconfont icon-location"></text>
|
<text class="iconfont icon-location"></text>
|
||||||
</view>
|
</view>
|
||||||
<text class="tool-text">位置</text>
|
<text class="tool-text">{{ $lang('location') }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<view class="voice-mask" @click="hideVoicePanel"></view>
|
<view class="voice-mask" @click="hideVoicePanel"></view>
|
||||||
<view class="voice-input-panel">
|
<view class="voice-input-panel">
|
||||||
<view class="voice-header">
|
<view class="voice-header">
|
||||||
<text>语音输入</text>
|
<text>{{ $lang('voiceInputTitle') }}</text>
|
||||||
<button class="close-btn" @click="hideVoicePanel">
|
<button class="close-btn" @click="hideVoicePanel">
|
||||||
<text class="iconfont icon-close"></text>
|
<text class="iconfont icon-close"></text>
|
||||||
</button>
|
</button>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
class="wave-bar"
|
class="wave-bar"
|
||||||
:style="{ animationDelay: (i * 0.1) + 's' }"></view>
|
:style="{ animationDelay: (i * 0.1) + 's' }"></view>
|
||||||
</view>
|
</view>
|
||||||
<text class="voice-tip">{{ voiceInputing ? '正在录音,松开结束' : '点击开始录音' }}</text>
|
<text class="voice-tip">{{ voiceInputing ? $lang('recordingTip') : $lang('startRecordTip') }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="voice-actions">
|
<view class="voice-actions">
|
||||||
<button class="voice-btn" @click="toggleVoiceInput">
|
<button class="voice-btn" @click="toggleVoiceInput">
|
||||||
@@ -86,7 +86,7 @@ export default {
|
|||||||
// 录音中,可扩展实时转文字
|
// 录音中,可扩展实时转文字
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
uni.showToast({ title: '语音输入失败', icon: 'none' })
|
uni.showToast({ title: this.$lang('voiceInputFailed'), icon: 'none' })
|
||||||
this.voiceInputing = false
|
this.voiceInputing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<view class="settings-mask" @click="hideSettings"></view>
|
<view class="settings-mask" @click="hideSettings"></view>
|
||||||
<view class="settings-panel">
|
<view class="settings-panel">
|
||||||
<view class="settings-header">
|
<view class="settings-header">
|
||||||
<text class="settings-title">设置</text>
|
<text class="settings-title">{{ $lang('settingsTitle') }}</text>
|
||||||
<view class="close-btn" @click="hideSettings">
|
<view class="close-btn" @click="hideSettings">
|
||||||
<text>✕</text>
|
<text>✕</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -50,8 +50,8 @@
|
|||||||
<text class="iconfont icon-user">📷</text>
|
<text class="iconfont icon-user">📷</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="setting-info">
|
<view class="setting-info">
|
||||||
<text class="setting-label">更换头像</text>
|
<text class="setting-label">{{ $lang('changeAvatar') }}</text>
|
||||||
<text class="setting-desc">自定义您的聊天头像</text>
|
<text class="setting-desc">{{ $lang('changeAvatarDesc') }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="iconfont icon-right">›</text>
|
<text class="iconfont icon-right">›</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -60,8 +60,8 @@
|
|||||||
<text class="iconfont icon-edit">✏️</text>
|
<text class="iconfont icon-edit">✏️</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="setting-info">
|
<view class="setting-info">
|
||||||
<text class="setting-label">更换昵称</text>
|
<text class="setting-label">{{ $lang('changeNickname') }}</text>
|
||||||
<text class="setting-desc">自定义您的昵称</text>
|
<text class="setting-desc">{{ $lang('changeNicknameDesc') }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="iconfont icon-right">›</text>
|
<text class="iconfont icon-right">›</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -86,7 +86,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
initialMessages: [],
|
initialMessages: [],
|
||||||
fallbackWelcome: '您好!我是AI智能客服,很高兴为您服务!\n\n我可以帮您:\n• 解答产品相关问题\n• 处理订单问题\n• 提供技术支持\n• 推荐相关商品\n\n请告诉我您需要什么帮助?',
|
fallbackWelcome: this.$lang('fallbackWelcome'),
|
||||||
|
|
||||||
// --- 系统导航栏/状态栏相关 ---
|
// --- 系统导航栏/状态栏相关 ---
|
||||||
navBarHeight: 44, // uni-page-head 系统导航栏,默认高度为44px
|
navBarHeight: 44, // uni-page-head 系统导航栏,默认高度为44px
|
||||||
@@ -132,7 +132,7 @@ export default {
|
|||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
this. $langConfig.title('AI智能客服');
|
this.$langConfig.title(this.$lang('title'));
|
||||||
this.initChat();
|
this.initChat();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -144,12 +144,13 @@ export default {
|
|||||||
console.log('[AI Chat Page] onShow 触发');
|
console.log('[AI Chat Page] onShow 触发');
|
||||||
// 监听键盘高度变化(H5 可能不支持,try-catch 保护)
|
// 监听键盘高度变化(H5 可能不支持,try-catch 保护)
|
||||||
try {
|
try {
|
||||||
uni.onKeyboardHeightChange((res) => {
|
this._keyboardChangeHandler = (res) => {
|
||||||
this.keyboardHeight = res.height;
|
this.keyboardHeight = res.height;
|
||||||
if (this.$refs.chat && this.$refs.chat.updateKeyboardHeight) {
|
if (this.$refs.chat && this.$refs.chat.updateKeyboardHeight) {
|
||||||
this.$refs.chat.updateKeyboardHeight(res.height);
|
this.$refs.chat.updateKeyboardHeight(res.height);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
uni.onKeyboardHeightChange(this._keyboardChangeHandler);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('onKeyboardHeightChange 不可用:', e.message);
|
console.warn('onKeyboardHeightChange 不可用:', e.message);
|
||||||
}
|
}
|
||||||
@@ -163,7 +164,10 @@ export default {
|
|||||||
|
|
||||||
onHide() {
|
onHide() {
|
||||||
// 取消监听键盘高度变化
|
// 取消监听键盘高度变化
|
||||||
uni.offKeyboardHeightChange();
|
if (this._keyboardChangeHandler) {
|
||||||
|
uni.offKeyboardHeightChange(this._keyboardChangeHandler);
|
||||||
|
this._keyboardChangeHandler = null;
|
||||||
|
}
|
||||||
this.keyboardHeight = 0;
|
this.keyboardHeight = 0;
|
||||||
if (this.$refs.chat && this.$refs.chat.updateKeyboardHeight) {
|
if (this.$refs.chat && this.$refs.chat.updateKeyboardHeight) {
|
||||||
this.$refs.chat.updateKeyboardHeight(0);
|
this.$refs.chat.updateKeyboardHeight(0);
|
||||||
@@ -233,7 +237,7 @@ export default {
|
|||||||
component: this. $options.name
|
component: this. $options.name
|
||||||
});
|
});
|
||||||
|
|
||||||
this.showError('操作失败,请重试');
|
this.showError(this.$lang('operationFailed'));
|
||||||
},
|
},
|
||||||
|
|
||||||
// ========== 初始化页面 ==========
|
// ========== 初始化页面 ==========
|
||||||
@@ -287,6 +291,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
|
if (this._keyboardChangeHandler) {
|
||||||
|
uni.offKeyboardHeightChange(this._keyboardChangeHandler);
|
||||||
|
this._keyboardChangeHandler = null;
|
||||||
|
}
|
||||||
this.safeEventHandlers.forEach((handler, eventType) => {
|
this.safeEventHandlers.forEach((handler, eventType) => {
|
||||||
this. $off(eventType, handler);
|
this. $off(eventType, handler);
|
||||||
});
|
});
|
||||||
@@ -309,7 +317,7 @@ export default {
|
|||||||
// 显示菜单
|
// 显示菜单
|
||||||
showMenu() {
|
showMenu() {
|
||||||
uni.showActionSheet({
|
uni.showActionSheet({
|
||||||
itemList: ['清空聊天', '导出记录', '设置', '帮助'],
|
itemList: [this.$lang('menuClearChat'), this.$lang('menuExport'), this.$lang('menuSettings'), this.$lang('menuHelp')],
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
switch (res.tapIndex) {
|
switch (res.tapIndex) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -331,8 +339,8 @@ export default {
|
|||||||
|
|
||||||
clearChat() {
|
clearChat() {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '提示',
|
title: this.$lang('prompt'),
|
||||||
content: '确定要清空聊天记录吗?',
|
content: this.$lang('confirmClearChat'),
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
this. $refs.chat.clearMessages();
|
this. $refs.chat.clearMessages();
|
||||||
@@ -340,7 +348,7 @@ export default {
|
|||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
role: 'ai',
|
role: 'ai',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
content: '聊天记录已清空,有什么可以帮助您的吗?',
|
content: this.$lang('chatCleared'),
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -350,7 +358,7 @@ export default {
|
|||||||
|
|
||||||
exportChat() {
|
exportChat() {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '功能开发中',
|
title: this.$lang('featureDeveloping'),
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -439,10 +447,10 @@ export default {
|
|||||||
onActionClick({ action, message }) {
|
onActionClick({ action, message }) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'like':
|
case 'like':
|
||||||
uni.showToast({ title: '感谢您的反馈!', icon: 'success' });
|
uni.showToast({ title: this.$lang('thanksFeedback'), icon: 'success' });
|
||||||
break;
|
break;
|
||||||
case 'dislike':
|
case 'dislike':
|
||||||
uni.showToast({ title: '我们会改进服务', icon: 'none' });
|
uni.showToast({ title: this.$lang('improveService'), icon: 'none' });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -451,7 +459,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onFilePreview(message) {
|
onFilePreview(message) {
|
||||||
uni.showToast({ title: '打开文件: ' + message.fileName, icon: 'none' });
|
uni.showToast({ title: this.$lang('openingFile') + ' ' + message.fileName, icon: 'none' });
|
||||||
},
|
},
|
||||||
|
|
||||||
onAudioPlay(message) {
|
onAudioPlay(message) {
|
||||||
@@ -480,8 +488,8 @@ export default {
|
|||||||
|
|
||||||
onLinkOpen(message) {
|
onLinkOpen(message) {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '打开链接',
|
title: this.$lang('openLink'),
|
||||||
content: `确定要打开链接: $ {message.url} 吗?`,
|
content: this.$lang('confirmOpenLink') + message.url + '?',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
@@ -496,7 +504,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onProductView(message) {
|
onProductView(message) {
|
||||||
uni.showToast({ title: '查看商品: ' + message.title, icon: 'none' });
|
uni.showToast({ title: this.$lang('openingFile') + ': ' + message.title, icon: 'none' });
|
||||||
},
|
},
|
||||||
|
|
||||||
onInputChange(value) {
|
onInputChange(value) {
|
||||||
@@ -524,7 +532,7 @@ export default {
|
|||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
if (currentType !== 'aikefu') {
|
if (currentType !== 'aikefu') {
|
||||||
uni.showToast({ title: '当前客服类型不支持此页面', icon: 'none' });
|
uni.showToast({ title: this.$lang('serviceTypeNotSupported'), icon: 'none' });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const pages = getCurrentPages();
|
const pages = getCurrentPages();
|
||||||
if (pages.length > 1) {
|
if (pages.length > 1) {
|
||||||
|
|||||||
Reference in New Issue
Block a user