refactor(ai-chat): 重构AI聊天国际化文件结构
- 将 ai-chat 语言文件从 ai/ 迁移至 ai-chat/ 目录 - 重构消息组件及多个子组件
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
<ai-chat-search
|
||||
ref="searchRef"
|
||||
:messages="messages"
|
||||
:show-label="false"
|
||||
@scroll-to="onSearchScrollTo"
|
||||
@active-change="onSearchActiveChange"
|
||||
@result-change="onSearchResultChange"
|
||||
@@ -73,7 +74,7 @@
|
||||
<view class="message-content">
|
||||
<!-- AI昵称固定,不可修改 - 左移到气泡上方 -->
|
||||
<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>
|
||||
</view>
|
||||
<view class="message-bubble"
|
||||
@@ -118,7 +119,7 @@
|
||||
<view v-else-if="message.type === 'audio'" class="audio-content">
|
||||
<view class="audio-player">
|
||||
<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>
|
||||
</view>
|
||||
<view class="audio-controls">
|
||||
@@ -156,7 +157,7 @@
|
||||
@pause="onVideoPause(message)">
|
||||
</video>
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
@@ -186,7 +187,7 @@
|
||||
<text class="dot"></text>
|
||||
</view>
|
||||
<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="ellipsis-dot">.</text>
|
||||
<text class="ellipsis-dot">.</text>
|
||||
@@ -231,7 +232,7 @@
|
||||
<textarea
|
||||
v-model="inputText"
|
||||
class="message-input"
|
||||
placeholder="请输入您的问题..."
|
||||
:placeholder="$lang('inputPlaceholder')"
|
||||
:maxlength="500"
|
||||
:auto-height="true"
|
||||
:show-confirm-bar="false"
|
||||
@@ -257,7 +258,7 @@
|
||||
:class="{ disabled: !canSendMessage }"
|
||||
@click="sendMessage"
|
||||
:disabled="!canSendMessage">
|
||||
<text>发送</text>
|
||||
<text>{{ $lang('send') }}</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -282,7 +283,7 @@
|
||||
<view class="expand-editor-mask" @click="closeExpandEditor"></view>
|
||||
<view class="expand-editor-panel">
|
||||
<view class="expand-editor-header">
|
||||
<text>编辑内容</text>
|
||||
<text>{{ $lang('expandEditorTitle') }}</text>
|
||||
<button class="close-btn" @click="closeExpandEditor">
|
||||
<text>×</text>
|
||||
</button>
|
||||
@@ -290,15 +291,15 @@
|
||||
<textarea
|
||||
v-model="expandInputText"
|
||||
class="expand-editor-input"
|
||||
placeholder="请输入您的问题..."
|
||||
:placeholder="$lang('inputPlaceholder')"
|
||||
:maxlength="500"
|
||||
:auto-height="true"
|
||||
:show-confirm-bar="false"
|
||||
:adjust-position="true"
|
||||
:focus="true" />
|
||||
<view class="expand-editor-footer">
|
||||
<button class="expand-cancel-btn" @click="closeExpandEditor">取消</button>
|
||||
<button class="expand-send-btn" @click="sendFromExpandEditor" :disabled="!expandInputText.trim()">发送</button>
|
||||
<button class="expand-cancel-btn" @click="closeExpandEditor">{{ $lang('cancel') }}</button>
|
||||
<button class="expand-send-btn" @click="sendFromExpandEditor" :disabled="!expandInputText.trim()">{{ $lang('send') }}</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -396,7 +397,7 @@ export default {
|
||||
scrollTop: 0,
|
||||
scrollIntoViewId: null,
|
||||
hasMoreHistory: false,
|
||||
loadingText: '加载更多历史消息',
|
||||
loadingText: this.$lang('loadMoreHistory'),
|
||||
voiceInputing: false,
|
||||
audioContext: null,
|
||||
currentAudio: null,
|
||||
@@ -416,7 +417,7 @@ export default {
|
||||
localUserAvatar: '',
|
||||
showAvatarSelector: false, // 头像选择弹窗控制
|
||||
localUserNickname: '', // 当前用户昵称
|
||||
defaultNickname: '我', // 默认昵称
|
||||
defaultNickname: this.$lang('defaultNickname'), // 默认昵称
|
||||
showNicknameEditor: false, // 昵称编辑弹窗控制
|
||||
// AI头像错误状态
|
||||
avatarError: false,
|
||||
@@ -823,12 +824,12 @@ async loadChatHistoryIfExist() {
|
||||
type: 'loading',
|
||||
content: '',
|
||||
timestamp: Date.now(),
|
||||
loadingText: 'AI正在思考',
|
||||
loadingText: this.$lang('aiThinking'),
|
||||
_loadingIndex: 0
|
||||
}
|
||||
this.shouldScrollToBottom = true
|
||||
this.messages.push(loadingMessage)
|
||||
|
||||
|
||||
// 启动处理状态文字轮换
|
||||
this._startLoadingTextRotation(loadingMessage)
|
||||
|
||||
@@ -856,10 +857,10 @@ async loadChatHistoryIfExist() {
|
||||
id: ++this.messageId,
|
||||
role: 'ai',
|
||||
type: 'text',
|
||||
content: '抱歉,AI服务暂时不可用,请稍后重试。',
|
||||
content: this.$lang('serviceUnavailable'),
|
||||
timestamp: Date.now(),
|
||||
actions: [
|
||||
{ id: 1, text: '重试', type: 'retry' }
|
||||
{ id: 1, text: this.$lang('retry'), type: 'retry' }
|
||||
]
|
||||
}
|
||||
this.shouldScrollToBottom = true
|
||||
@@ -940,7 +941,7 @@ async sendStreamMessage(userMessage) {
|
||||
this.isStreaming = true
|
||||
const loadingMsg = this.messages[loadingIdx]
|
||||
this._stopLoadingTextRotation(loadingMsg)
|
||||
loadingMsg.loadingText = '正在接收回复'
|
||||
loadingMsg.loadingText = this.$lang('receivingReply')
|
||||
|
||||
let transformed = false
|
||||
|
||||
@@ -1006,15 +1007,16 @@ async sendStreamMessage(userMessage) {
|
||||
id: ++this.messageId,
|
||||
role: 'ai',
|
||||
type: 'text',
|
||||
content: '抱歉,服务暂时不可用,请稍后重试。',
|
||||
content: this.$lang('serviceTemporaryUnavailable'),
|
||||
timestamp: Date.now(),
|
||||
actions: [{ id: 1, text: '重试', type: 'retry' }]
|
||||
actions: [{ id: 1, text: this.$lang('retry'), type: 'retry' }]
|
||||
}
|
||||
this.messages.push(errorMsg)
|
||||
this.$emit('ai-response', errorMsg)
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// 完成流式消息处理(共用逻辑)
|
||||
_finishStream(msg, result) {
|
||||
msg.isStreaming = false
|
||||
@@ -1051,9 +1053,9 @@ _handleStreamError(msg) {
|
||||
id: ++this.messageId,
|
||||
role: 'ai',
|
||||
type: 'text',
|
||||
content: '抱歉,服务暂时不可用,请稍后重试。',
|
||||
content: this.$lang('serviceTemporaryUnavailable'),
|
||||
timestamp: Date.now(),
|
||||
actions: [{ id: 1, text: '重试', type: 'retry' }]
|
||||
actions: [{ id: 1, text: this.$lang('retry'), type: 'retry' }]
|
||||
}
|
||||
this.messages.push(errorMsg)
|
||||
this.$emit('ai-response', errorMsg)
|
||||
@@ -1339,9 +1341,9 @@ _handleStreamError(msg) {
|
||||
|
||||
// 格式化文件大小
|
||||
formatFileSize(bytes) {
|
||||
if (!bytes) return '0B'
|
||||
if (!bytes) return this.$lang('about')
|
||||
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))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + sizes[i]
|
||||
},
|
||||
@@ -1429,7 +1431,7 @@ _handleStreamError(msg) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载历史失败:', error);
|
||||
uni.showToast({ title: '加载历史失败', icon: 'none' });
|
||||
uni.showToast({ title: this.$lang('loadHistoryFail'), icon: 'none' });
|
||||
this.hasMoreHistory = false;
|
||||
} finally {
|
||||
this.isLoadingHistory = false;
|
||||
@@ -1444,7 +1446,7 @@ _handleStreamError(msg) {
|
||||
|
||||
// 预览文件
|
||||
previewFile(message) {
|
||||
uni.showLoading({ title: '打开文件中...' })
|
||||
uni.showLoading({ title: this.$lang('openingFile') })
|
||||
|
||||
// #ifdef H5
|
||||
window.open(message.url, '_blank')
|
||||
@@ -1454,7 +1456,7 @@ _handleStreamError(msg) {
|
||||
plus.runtime.openFile(message.url, {
|
||||
error: (e) => {
|
||||
uni.showToast({
|
||||
title: '文件打开失败',
|
||||
title: this.$lang('fileOpenFail'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
@@ -1628,7 +1630,7 @@ _handleStreamError(msg) {
|
||||
copyText = message.content || ''
|
||||
}
|
||||
if (!copyText) {
|
||||
uni.showToast({ title: '无可复制的内容', icon: 'none' })
|
||||
uni.showToast({ title: this.$lang('noContentToCopy'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1661,12 +1663,12 @@ _handleStreamError(msg) {
|
||||
data: copyText,
|
||||
success: () => {
|
||||
console.log('[AI Chat] 复制成功')
|
||||
uni.showToast({ title: '已复制', icon: 'success' })
|
||||
uni.showToast({ title: this.$lang('copySuccess'), icon: 'success' })
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[AI Chat] 复制失败, 详细错误:', JSON.stringify(err))
|
||||
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
|
||||
@@ -1687,12 +1689,12 @@ _handleStreamError(msg) {
|
||||
const success = document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
if (success) {
|
||||
uni.showToast({ title: '已复制', icon: 'success' })
|
||||
uni.showToast({ title: this.$lang('copySuccess'), icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '复制失败', icon: 'none' })
|
||||
uni.showToast({ title: this.$lang('copyFail'), icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '复制失败', icon: 'none' })
|
||||
uni.showToast({ title: this.$lang('copyFail'), icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1795,10 +1797,10 @@ _handleStreamError(msg) {
|
||||
type: 'loading',
|
||||
content: '',
|
||||
timestamp: Date.now(),
|
||||
loadingText: 'AI正在思考',
|
||||
loadingText: this.$lang('aiThinking'),
|
||||
_loadingIndex: 0
|
||||
}
|
||||
|
||||
|
||||
this.messages.push(loadingMessage)
|
||||
this._startLoadingTextRotation(loadingMessage)
|
||||
|
||||
@@ -1826,10 +1828,10 @@ _handleStreamError(msg) {
|
||||
id: ++this.messageId,
|
||||
role: 'ai',
|
||||
type: 'text',
|
||||
content: '抱歉,AI服务暂时不可用,请稍后重试。',
|
||||
content: this.$lang('serviceUnavailable'),
|
||||
timestamp: Date.now(),
|
||||
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-panel">
|
||||
<view class="avatar-selector-header">
|
||||
<text>更换头像</text>
|
||||
<text>{{ $lang('avatarSelectorTitle') }}</text>
|
||||
<button class="close-btn" @click="close">
|
||||
<text class="iconfont icon-close"></text>
|
||||
</button>
|
||||
@@ -15,10 +15,10 @@
|
||||
<view class="avatar-selector-actions">
|
||||
<button class="avatar-select-action" @click="chooseAvatarFromAlbum">
|
||||
<text class="iconfont icon-album"></text>
|
||||
<text>从相册选择</text>
|
||||
<text>{{ $lang('chooseFromAlbum') }}</text>
|
||||
</button>
|
||||
<button class="avatar-select-action cancel" @click="close">
|
||||
<text>取消</text>
|
||||
<text>{{ $lang('cancel') }}</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
reader.readAsDataURL(file)
|
||||
this.$emit('avatar-change', tempFilePath)
|
||||
this.close()
|
||||
uni.showToast({ title: '头像更换成功', icon: 'success', duration: 1500 })
|
||||
uni.showToast({ title: this.$lang('avatarChangeSuccess'), icon: 'success', duration: 1500 })
|
||||
} else {
|
||||
this.close()
|
||||
}
|
||||
@@ -99,11 +99,11 @@ export default {
|
||||
uni.setStorageSync('user_avatar', tempFilePath)
|
||||
this.$emit('avatar-change', tempFilePath)
|
||||
this.close()
|
||||
uni.showToast({ title: '头像更换成功', icon: 'success', duration: 1500 })
|
||||
uni.showToast({ title: this.$lang('avatarChangeSuccess'), icon: 'success', duration: 1500 })
|
||||
},
|
||||
fail: (error) => {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<view class="emoji-mask" @click="hideEmojiPanel"></view>
|
||||
<view class="emoji-panel">
|
||||
<view class="emoji-header">
|
||||
<text>表情</text>
|
||||
<text>{{ $lang('emojiTitle') }}</text>
|
||||
<button class="close-btn" @click="hideEmojiPanel">
|
||||
<text>×</text>
|
||||
</button>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<view class="nickname-editor-mask" @click="close"></view>
|
||||
<view class="nickname-editor-panel">
|
||||
<view class="nickname-editor-header">
|
||||
<text>修改昵称</text>
|
||||
<text>{{ $lang('editNickname') }}</text>
|
||||
<button class="close-btn" @click="close">
|
||||
<text class="iconfont icon-close"></text>
|
||||
</button>
|
||||
@@ -13,21 +13,21 @@
|
||||
<input
|
||||
v-model="tempNickname"
|
||||
class="nickname-input"
|
||||
placeholder="请输入您的新昵称"
|
||||
:placeholder="$lang('newNicknamePlaceholder')"
|
||||
:maxlength="12"
|
||||
type="text" />
|
||||
</view>
|
||||
<view class="nickname-tip">昵称长度限制1-12个字符,仅自己可见</view>
|
||||
<view class="nickname-tip">{{ $lang('nicknameTip') }}</view>
|
||||
<view class="nickname-editor-actions">
|
||||
<button class="nickname-action-btn cancel" @click="close">
|
||||
<text>取消</text>
|
||||
<text>{{ $lang('cancel') }}</text>
|
||||
</button>
|
||||
<button
|
||||
class="nickname-action-btn confirm"
|
||||
:class="{ disabled: !tempNickname.trim() }"
|
||||
:disabled="!tempNickname.trim()"
|
||||
@click="saveNickname">
|
||||
<text>保存</text>
|
||||
<text>{{ $lang('save') }}</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -87,7 +87,7 @@ export default {
|
||||
uni.setStorageSync('user_nickname', newNickname)
|
||||
this.$emit('nickname-change', newNickname)
|
||||
this.close()
|
||||
uni.showToast({ title: '昵称修改成功', icon: 'success', duration: 1500 })
|
||||
uni.showToast({ title: this.$lang('nicknameChangeSuccess'), icon: 'success', duration: 1500 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<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="search-entrance-label">搜索</text>
|
||||
<text class="search-entrance-label" v-if="showLabel">{{ $lang('searchEntrance') }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索浮条 -->
|
||||
@@ -14,7 +14,7 @@
|
||||
<input
|
||||
class="search-bar-input"
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索聊天记录"
|
||||
:placeholder="$lang('searchPlaceholder')"
|
||||
:focus="true"
|
||||
@input="onSearchInput"
|
||||
@confirm="doSearch" />
|
||||
@@ -27,7 +27,7 @@
|
||||
<text class="search-bar-nav" :class="{ disabled: searchResultIndex >= searchResults.length - 1 }" @click="scrollToNextResult">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="search-bar-close" @click="closeSearch">完成</text>
|
||||
<text class="search-bar-close" @click="closeSearch">{{ $lang('searchClose') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -39,6 +39,7 @@
|
||||
*
|
||||
* Props:
|
||||
* messages: Array - 原始消息列表
|
||||
* showLabel: Boolean - (默认 true) 是否显示"搜索"文字;设为 false 则只显示搜索图标
|
||||
*
|
||||
* Events:
|
||||
* scroll-to: (messageKey: string) => void - 请求父组件滚动到指定消息
|
||||
@@ -53,6 +54,11 @@ export default {
|
||||
messages: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 是否显示文字标签;设为 false 则只显示搜索图标
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -66,6 +72,7 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
// 过滤后的消息列表(与 v-for 保持一致)
|
||||
filteredMessages() {
|
||||
return this.messages.filter(
|
||||
@@ -150,7 +157,7 @@ export default {
|
||||
this._emitScrollTo(filteredIdx)
|
||||
})
|
||||
} else {
|
||||
uni.showToast({ title: '未找到匹配结果', icon: 'none' })
|
||||
uni.showToast({ title: this.$lang('noMatchResult'), icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -174,7 +181,7 @@ export default {
|
||||
if (idx >= 0) {
|
||||
this.scrollToSearchResult(idx)
|
||||
} 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) {
|
||||
this.scrollToSearchResult(idx)
|
||||
} else {
|
||||
uni.showToast({ title: '已到底部', icon: 'none' })
|
||||
uni.showToast({ title: this.$lang('searchAtBottom'), icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -251,7 +258,7 @@ export default {
|
||||
// 搜索入口按钮
|
||||
.search-entrance {
|
||||
position: absolute;
|
||||
top: 88rpx;
|
||||
top: 44rpx;
|
||||
right: 16rpx;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
@@ -277,6 +284,15 @@ export default {
|
||||
&:active {
|
||||
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-panel">
|
||||
<view class="tools-header">
|
||||
<text>更多功能</text>
|
||||
<text>{{ $lang('moreFunctions') }}</text>
|
||||
<button class="close-btn" @click="hideToolsPanel">
|
||||
<text class="iconfont icon-close"></text>
|
||||
</button>
|
||||
@@ -13,25 +13,25 @@
|
||||
<view class="tool-icon">
|
||||
<text class="iconfont icon-image"></text>
|
||||
</view>
|
||||
<text class="tool-text">图片</text>
|
||||
<text class="tool-text">{{ $lang('image') }}</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="selectFile">
|
||||
<view class="tool-icon">
|
||||
<text class="iconfont icon-file"></text>
|
||||
</view>
|
||||
<text class="tool-text">文件</text>
|
||||
<text class="tool-text">{{ $lang('file') }}</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="startRecord">
|
||||
<view class="tool-icon">
|
||||
<text class="iconfont icon-voice"></text>
|
||||
</view>
|
||||
<text class="tool-text">语音</text>
|
||||
<text class="tool-text">{{ $lang('voice') }}</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="selectLocation">
|
||||
<view class="tool-icon">
|
||||
<text class="iconfont icon-location"></text>
|
||||
</view>
|
||||
<text class="tool-text">位置</text>
|
||||
<text class="tool-text">{{ $lang('location') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<view class="voice-mask" @click="hideVoicePanel"></view>
|
||||
<view class="voice-input-panel">
|
||||
<view class="voice-header">
|
||||
<text>语音输入</text>
|
||||
<text>{{ $lang('voiceInputTitle') }}</text>
|
||||
<button class="close-btn" @click="hideVoicePanel">
|
||||
<text class="iconfont icon-close"></text>
|
||||
</button>
|
||||
@@ -16,7 +16,7 @@
|
||||
class="wave-bar"
|
||||
:style="{ animationDelay: (i * 0.1) + 's' }"></view>
|
||||
</view>
|
||||
<text class="voice-tip">{{ voiceInputing ? '正在录音,松开结束' : '点击开始录音' }}</text>
|
||||
<text class="voice-tip">{{ voiceInputing ? $lang('recordingTip') : $lang('startRecordTip') }}</text>
|
||||
</view>
|
||||
<view class="voice-actions">
|
||||
<button class="voice-btn" @click="toggleVoiceInput">
|
||||
@@ -86,7 +86,7 @@ export default {
|
||||
// 录音中,可扩展实时转文字
|
||||
})
|
||||
.catch(error => {
|
||||
uni.showToast({ title: '语音输入失败', icon: 'none' })
|
||||
uni.showToast({ title: this.$lang('voiceInputFailed'), icon: 'none' })
|
||||
this.voiceInputing = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<view class="settings-mask" @click="hideSettings"></view>
|
||||
<view class="settings-panel">
|
||||
<view class="settings-header">
|
||||
<text class="settings-title">设置</text>
|
||||
<text class="settings-title">{{ $lang('settingsTitle') }}</text>
|
||||
<view class="close-btn" @click="hideSettings">
|
||||
<text>✕</text>
|
||||
</view>
|
||||
@@ -50,8 +50,8 @@
|
||||
<text class="iconfont icon-user">📷</text>
|
||||
</view>
|
||||
<view class="setting-info">
|
||||
<text class="setting-label">更换头像</text>
|
||||
<text class="setting-desc">自定义您的聊天头像</text>
|
||||
<text class="setting-label">{{ $lang('changeAvatar') }}</text>
|
||||
<text class="setting-desc">{{ $lang('changeAvatarDesc') }}</text>
|
||||
</view>
|
||||
<text class="iconfont icon-right">›</text>
|
||||
</view>
|
||||
@@ -60,8 +60,8 @@
|
||||
<text class="iconfont icon-edit">✏️</text>
|
||||
</view>
|
||||
<view class="setting-info">
|
||||
<text class="setting-label">更换昵称</text>
|
||||
<text class="setting-desc">自定义您的昵称</text>
|
||||
<text class="setting-label">{{ $lang('changeNickname') }}</text>
|
||||
<text class="setting-desc">{{ $lang('changeNicknameDesc') }}</text>
|
||||
</view>
|
||||
<text class="iconfont icon-right">›</text>
|
||||
</view>
|
||||
@@ -86,7 +86,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
initialMessages: [],
|
||||
fallbackWelcome: '您好!我是AI智能客服,很高兴为您服务!\n\n我可以帮您:\n• 解答产品相关问题\n• 处理订单问题\n• 提供技术支持\n• 推荐相关商品\n\n请告诉我您需要什么帮助?',
|
||||
fallbackWelcome: this.$lang('fallbackWelcome'),
|
||||
|
||||
// --- 系统导航栏/状态栏相关 ---
|
||||
navBarHeight: 44, // uni-page-head 系统导航栏,默认高度为44px
|
||||
@@ -132,7 +132,7 @@ export default {
|
||||
// return;
|
||||
// }
|
||||
|
||||
this. $langConfig.title('AI智能客服');
|
||||
this.$langConfig.title(this.$lang('title'));
|
||||
this.initChat();
|
||||
},
|
||||
|
||||
@@ -144,12 +144,13 @@ export default {
|
||||
console.log('[AI Chat Page] onShow 触发');
|
||||
// 监听键盘高度变化(H5 可能不支持,try-catch 保护)
|
||||
try {
|
||||
uni.onKeyboardHeightChange((res) => {
|
||||
this._keyboardChangeHandler = (res) => {
|
||||
this.keyboardHeight = res.height;
|
||||
if (this.$refs.chat && this.$refs.chat.updateKeyboardHeight) {
|
||||
this.$refs.chat.updateKeyboardHeight(res.height);
|
||||
}
|
||||
});
|
||||
};
|
||||
uni.onKeyboardHeightChange(this._keyboardChangeHandler);
|
||||
} catch (e) {
|
||||
console.warn('onKeyboardHeightChange 不可用:', e.message);
|
||||
}
|
||||
@@ -163,7 +164,10 @@ export default {
|
||||
|
||||
onHide() {
|
||||
// 取消监听键盘高度变化
|
||||
uni.offKeyboardHeightChange();
|
||||
if (this._keyboardChangeHandler) {
|
||||
uni.offKeyboardHeightChange(this._keyboardChangeHandler);
|
||||
this._keyboardChangeHandler = null;
|
||||
}
|
||||
this.keyboardHeight = 0;
|
||||
if (this.$refs.chat && this.$refs.chat.updateKeyboardHeight) {
|
||||
this.$refs.chat.updateKeyboardHeight(0);
|
||||
@@ -233,7 +237,7 @@ export default {
|
||||
component: this. $options.name
|
||||
});
|
||||
|
||||
this.showError('操作失败,请重试');
|
||||
this.showError(this.$lang('operationFailed'));
|
||||
},
|
||||
|
||||
// ========== 初始化页面 ==========
|
||||
@@ -287,6 +291,10 @@ export default {
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
if (this._keyboardChangeHandler) {
|
||||
uni.offKeyboardHeightChange(this._keyboardChangeHandler);
|
||||
this._keyboardChangeHandler = null;
|
||||
}
|
||||
this.safeEventHandlers.forEach((handler, eventType) => {
|
||||
this. $off(eventType, handler);
|
||||
});
|
||||
@@ -309,7 +317,7 @@ export default {
|
||||
// 显示菜单
|
||||
showMenu() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['清空聊天', '导出记录', '设置', '帮助'],
|
||||
itemList: [this.$lang('menuClearChat'), this.$lang('menuExport'), this.$lang('menuSettings'), this.$lang('menuHelp')],
|
||||
success: (res) => {
|
||||
switch (res.tapIndex) {
|
||||
case 0:
|
||||
@@ -331,8 +339,8 @@ export default {
|
||||
|
||||
clearChat() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要清空聊天记录吗?',
|
||||
title: this.$lang('prompt'),
|
||||
content: this.$lang('confirmClearChat'),
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this. $refs.chat.clearMessages();
|
||||
@@ -340,7 +348,7 @@ export default {
|
||||
id: Date.now(),
|
||||
role: 'ai',
|
||||
type: 'text',
|
||||
content: '聊天记录已清空,有什么可以帮助您的吗?',
|
||||
content: this.$lang('chatCleared'),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
@@ -350,7 +358,7 @@ export default {
|
||||
|
||||
exportChat() {
|
||||
uni.showToast({
|
||||
title: '功能开发中',
|
||||
title: this.$lang('featureDeveloping'),
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
@@ -439,10 +447,10 @@ export default {
|
||||
onActionClick({ action, message }) {
|
||||
switch (action.type) {
|
||||
case 'like':
|
||||
uni.showToast({ title: '感谢您的反馈!', icon: 'success' });
|
||||
uni.showToast({ title: this.$lang('thanksFeedback'), icon: 'success' });
|
||||
break;
|
||||
case 'dislike':
|
||||
uni.showToast({ title: '我们会改进服务', icon: 'none' });
|
||||
uni.showToast({ title: this.$lang('improveService'), icon: 'none' });
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -451,7 +459,7 @@ export default {
|
||||
},
|
||||
|
||||
onFilePreview(message) {
|
||||
uni.showToast({ title: '打开文件: ' + message.fileName, icon: 'none' });
|
||||
uni.showToast({ title: this.$lang('openingFile') + ' ' + message.fileName, icon: 'none' });
|
||||
},
|
||||
|
||||
onAudioPlay(message) {
|
||||
@@ -480,8 +488,8 @@ export default {
|
||||
|
||||
onLinkOpen(message) {
|
||||
uni.showModal({
|
||||
title: '打开链接',
|
||||
content: `确定要打开链接: $ {message.url} 吗?`,
|
||||
title: this.$lang('openLink'),
|
||||
content: this.$lang('confirmOpenLink') + message.url + '?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// #ifdef H5
|
||||
@@ -496,7 +504,7 @@ export default {
|
||||
},
|
||||
|
||||
onProductView(message) {
|
||||
uni.showToast({ title: '查看商品: ' + message.title, icon: 'none' });
|
||||
uni.showToast({ title: this.$lang('openingFile') + ': ' + message.title, icon: 'none' });
|
||||
},
|
||||
|
||||
onInputChange(value) {
|
||||
@@ -524,7 +532,7 @@ export default {
|
||||
// #endif
|
||||
|
||||
if (currentType !== 'aikefu') {
|
||||
uni.showToast({ title: '当前客服类型不支持此页面', icon: 'none' });
|
||||
uni.showToast({ title: this.$lang('serviceTypeNotSupported'), icon: 'none' });
|
||||
setTimeout(() => {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
|
||||
Reference in New Issue
Block a user