Merge branch 'temp221' of http://git.aigc-quickapp.com/Uniapp/lucky_shop into temp221
This commit is contained in:
@@ -13,7 +13,9 @@
|
||||
|
||||
<!-- 顶部栏: 标题 + 新对话 -->
|
||||
<view class="chat-topbar">
|
||||
<text class="chat-title">{{ $lang('ai-chat.index.aiNickname') }}</text>
|
||||
<!-- PR-CHAT-UI-CONFIG-V2 (2026-09-24): 智能体显示名走后端配置, 空则回落 i18n 默认
|
||||
PR-CHAT-UI-CONFIG-V3 (2026-09-24): ui_show_assistant_name 控制顶栏名称显隐 -->
|
||||
<text class="chat-title" v-if="chatUiConfig.ui_show_assistant_name">{{ chatUiConfig.ui_assistant_name || $lang('ai-chat.index.aiNickname') }}</text>
|
||||
<view class="new-chat-btn" @click="onNewChat">
|
||||
<!-- 气泡+加号线性图标 (base64 SVG: 小程序不支持内联 SVG) -->
|
||||
<view class="new-chat-icon"></view>
|
||||
@@ -137,7 +139,8 @@
|
||||
|
||||
<!-- AI消息 -->
|
||||
<view v-else class="ai-message">
|
||||
<view class="avatar">
|
||||
<!-- PR-CHAT-UI-CONFIG-V3 (2026-09-24): ui_show_assistant_avatar 控制头像显隐; 自定义头像在 aiAvatarUrl computed 优先 -->
|
||||
<view class="avatar" v-if="chatUiConfig.ui_show_assistant_avatar">
|
||||
<!-- 智能客服头像 -->
|
||||
<image
|
||||
v-if="aiAvatarUrl && !avatarError"
|
||||
@@ -163,7 +166,7 @@
|
||||
</view>
|
||||
|
||||
<!-- AI 回答上方只显示时间(昵称"智能助手"已按需求移除) -->
|
||||
<view class="message-nickname ai-nickname">
|
||||
<view class="message-nickname ai-nickname" v-if="chatUiConfig.ui_show_message_time">
|
||||
<text class="message-time">{{ formatTime(message.timestamp) }}</text>
|
||||
</view>
|
||||
<view class="message-bubble">
|
||||
@@ -612,6 +615,13 @@ export default {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// PR-CHAT-UI-CONFIG-PRELOAD (2026-09-24): 父页面渲染前预取的聊天 UI 配置
|
||||
// 传入后 created() 立即用作首帧值, 消除"默认值渲染 → 异步配置到达跳变"的闪烁;
|
||||
// 之后 _loadChatUiConfig() 仍会拉最新值覆盖 (与父页面共享 60s TTL 缓存)
|
||||
preloadedUiConfig: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
// 初始用户头像(外部传入,可选)
|
||||
initUserAvatar: {
|
||||
type: String,
|
||||
@@ -737,7 +747,16 @@ export default {
|
||||
ui_enable_attachment_types: ['image', 'file', 'audio', 'video'],
|
||||
ui_input_max_length: 500,
|
||||
ui_available_models: ['Auto'],
|
||||
ui_enable_feedback: true
|
||||
ui_enable_feedback: true,
|
||||
// PR-CHAT-UI-CONFIG-V2 (2026-09-24): 与后端 Config::DEFAULT_UI_CONFIG 同步
|
||||
ui_assistant_name: '',
|
||||
// PR-CHAT-UI-CONFIG-V3 (2026-09-24): 与后端 Config::DEFAULT_UI_CONFIG 同步
|
||||
ui_show_assistant_name: true,
|
||||
ui_show_assistant_avatar: true,
|
||||
ui_assistant_avatar: '',
|
||||
ui_show_message_time: true,
|
||||
ui_time_format: 'auto',
|
||||
ui_time_humanize: false
|
||||
},
|
||||
chatUiConfigLoaded: false,
|
||||
|
||||
@@ -768,13 +787,18 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// PR-CHAT-UI-CONFIG-V3 (2026-09-24): 后台配置了自定义头像则优先 ($util.img 兼容 pic_path 与完整 URL)
|
||||
aiAvatar() {
|
||||
const custom = this.chatUiConfig && this.chatUiConfig.ui_assistant_avatar;
|
||||
if (custom) return this.$util.img(custom);
|
||||
return this.$util.img(IMAGE_CONFIG.aiAvatar);
|
||||
},
|
||||
defaultAvatar() {
|
||||
return this.$util.img(IMAGE_CONFIG.userAvatar);
|
||||
},
|
||||
aiAvatarUrl() {
|
||||
const custom = this.chatUiConfig && this.chatUiConfig.ui_assistant_avatar;
|
||||
if (custom) return this.$util.img(custom);
|
||||
return this.$util.img(IMAGE_CONFIG.aiAvatar);
|
||||
},
|
||||
chatMessagesStyle() {
|
||||
@@ -863,6 +887,21 @@ export default {
|
||||
this.currentConversationId = aiService.getConversationId();
|
||||
console.log('[AI Chat] created, currentConversationId:', this.currentConversationId, 'messages.length:', this.messages.length);
|
||||
|
||||
// PR-CHAT-UI-CONFIG-PRELOAD (2026-09-24): 父页面预取配置优先作为首帧值
|
||||
// 父页面已保证渲染本组件时配置就绪 (缓存或预取), 此处同步落位;
|
||||
// _loadChatUiConfig() 随后仍拉最新值 (TTL 缓存命中则同值, 无视觉跳变)
|
||||
if (this.preloadedUiConfig && typeof this.preloadedUiConfig === 'object') {
|
||||
this.chatUiConfig = Object.assign({}, this.chatUiConfig, this.preloadedUiConfig)
|
||||
if (Array.isArray(this.preloadedUiConfig.ui_available_models) && this.preloadedUiConfig.ui_available_models.length) {
|
||||
this.availableModels = this.preloadedUiConfig.ui_available_models.slice()
|
||||
if (!this.availableModels.includes(this.selectedModel)) {
|
||||
this.selectedModel = this.availableModels[0] || 'Auto'
|
||||
}
|
||||
}
|
||||
this.chatUiConfigLoaded = true
|
||||
console.log('[AI Chat] chatUiConfig 使用父页面预取值 (首帧即真实配置)')
|
||||
}
|
||||
|
||||
// 异步获取服务配置(含欢迎语)
|
||||
this._loadWelcomeMessage()
|
||||
// PR-CHAT-UI-CONFIG (2026-09-20): 异步加载 UI 开关 (与欢迎语并行, 不阻塞 created)
|
||||
@@ -2319,7 +2358,10 @@ _handleStreamError(msg) {
|
||||
return html
|
||||
},
|
||||
|
||||
// 格式化时间(人性化显示)
|
||||
// PR-CHAT-UI-CONFIG-V3 (2026-09-24): 时间格式化支持后端配置
|
||||
// ui_time_format = 'auto' 智能分段(默认, 与历史行为一致): 今天 HH:mm / 昨天 / 前天 / 本年 M/D / 跨年 Y/M/D
|
||||
// ui_time_format = 'full' 始终完整 YYYY-MM-DD HH:mm
|
||||
// ui_time_humanize = true 微信式相对时间(优先于 format): 刚刚 / N分钟前 / N小时前 / 昨天 / 前天 / M月D日 / Y年M月D日
|
||||
formatTime(timestamp) {
|
||||
if (!timestamp) return '';
|
||||
|
||||
@@ -2355,6 +2397,19 @@ _handleStreamError(msg) {
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const timeStr = `${hours}:${minutes}`;
|
||||
|
||||
// 人性化相对时间 (开启后优先于 ui_time_format)
|
||||
if (this.chatUiConfig && this.chatUiConfig.ui_time_humanize) {
|
||||
return this.humanizeTime(date, msgDate, today, timeStr);
|
||||
}
|
||||
|
||||
// 完整时间格式: 始终 YYYY-MM-DD HH:mm
|
||||
if (this.chatUiConfig && this.chatUiConfig.ui_time_format === 'full') {
|
||||
const mm = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const dd = date.getDate().toString().padStart(2, '0');
|
||||
return `${date.getFullYear()}-${mm}-${dd} ${timeStr}`;
|
||||
}
|
||||
|
||||
// auto: 智能分段 (原有逻辑)
|
||||
if (msgDate.getTime() === today.getTime()) {
|
||||
return timeStr;
|
||||
}
|
||||
@@ -2376,6 +2431,42 @@ _handleStreamError(msg) {
|
||||
const yearStr = date.getFullYear().toString();
|
||||
return `${yearStr}/${monthStr}/${dayStr} ${timeStr}`;
|
||||
},
|
||||
|
||||
// PR-CHAT-UI-CONFIG-V3 (2026-09-24): 微信式人性化相对时间
|
||||
// 刚刚 / N分钟前 / 今天内N小时前 / 昨天 HH:mm / 前天 HH:mm / 本年 M月D日 HH:mm / 跨年 Y年M月D日 HH:mm
|
||||
humanizeTime(date, msgDate, today, timeStr) {
|
||||
const diff = Date.now() - date.getTime();
|
||||
const oneMin = 60 * 1000;
|
||||
const oneHour = 60 * oneMin;
|
||||
|
||||
if (diff >= 0 && diff < oneMin) {
|
||||
return this.$lang('ai-chat.index.timeJustNow');
|
||||
}
|
||||
if (diff >= 0 && diff < oneHour) {
|
||||
return this.$lang('ai-chat.index.timeMinutesAgo', { n: Math.floor(diff / oneMin) });
|
||||
}
|
||||
// 今天内且已超过 1 小时
|
||||
if (msgDate.getTime() === today.getTime()) {
|
||||
if (diff < 24 * oneHour) {
|
||||
return this.$lang('ai-chat.index.timeHoursAgo', { n: Math.floor(diff / oneHour) });
|
||||
}
|
||||
return timeStr; // 兜底(理论不可达: 同一天 diff 不可能 >= 24h)
|
||||
}
|
||||
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (msgDate.getTime() === yesterday.getTime()) {
|
||||
return `${this.$lang('ai-chat.index.timeFormatYesterday')} ${timeStr}`;
|
||||
}
|
||||
const beforeYesterday = new Date(today.getTime() - 2 * 24 * 60 * 60 * 1000);
|
||||
if (msgDate.getTime() === beforeYesterday.getTime()) {
|
||||
return `${this.$lang('ai-chat.index.timeFormatBeforeYesterday')} ${timeStr}`;
|
||||
}
|
||||
const m = date.getMonth() + 1;
|
||||
const d = date.getDate();
|
||||
if (date.getFullYear() === new Date().getFullYear()) {
|
||||
return `${this.$lang('ai-chat.index.timeMonthDay', { m, d })} ${timeStr}`;
|
||||
}
|
||||
return `${this.$lang('ai-chat.index.timeYearMonthDay', { y: date.getFullYear(), m, d })} ${timeStr}`;
|
||||
},
|
||||
|
||||
// 格式化文件大小
|
||||
formatFileSize(bytes) {
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
<!-- AI 智能页始终全屏,不让位给底部 tabBar,聊天区直接占满到底 -->
|
||||
<view class="chat-content" :style="{ bottom: '0rpx' }">
|
||||
<ai-chat-message
|
||||
v-if="uiCfgReady"
|
||||
ref="chat"
|
||||
:preloaded-ui-config="preloadedUiCfg"
|
||||
:initial-messages="initialMessages"
|
||||
:fallback-welcome="fallbackWelcome"
|
||||
:user-avatar="userAvatar"
|
||||
@@ -96,6 +98,12 @@ export default {
|
||||
agentId: 'agent_aikefu',
|
||||
pageTitle: '',
|
||||
|
||||
// PR-CHAT-UI-CONFIG-PRELOAD (2026-09-24): 聊天 UI 配置预取
|
||||
// uiCfgReady 控制聊天组件渲染时机 — 配置就绪(缓存命中或预取完成)才挂载,
|
||||
// 避免"首帧用默认值渲染 → 异步配置到达后名称/头像/时间跳变"的闪烁
|
||||
uiCfgReady: false,
|
||||
preloadedUiCfg: null,
|
||||
|
||||
// --- 系统导航栏/状态栏相关 ---
|
||||
navBarHeight: 44, // uni-page-head 系统导航栏,默认高度为44px
|
||||
statusBarHeight: 0, // 状态栏高度,默认0
|
||||
@@ -140,6 +148,34 @@ export default {
|
||||
async onLoad(options) {
|
||||
this.fallbackWelcome = this.$lang('ai-chat.index.fallbackWelcome');
|
||||
|
||||
// ====== PR-CHAT-UI-CONFIG-PRELOAD (2026-09-24): 配置先行,再渲染聊天组件 ======
|
||||
// 1) 本地持久缓存命中 → 同步就绪, 立即渲染 (零延迟, 用上次会话的真实配置)
|
||||
// 2) 未命中(首次访问) → await 强制拉取 (race 2.5s 超时兜底), 拿到后再渲染
|
||||
// 3) 拉取失败/超时 → 也放行渲染, 组件内部仍有默认值兜底, 不白屏
|
||||
// force 拉取同时预热 /info 缓存, 欢迎语/模型列表等后续请求直接复用
|
||||
try {
|
||||
const cached = aiService.getChatUiConfigSync();
|
||||
if (cached) {
|
||||
this.preloadedUiCfg = cached;
|
||||
this.uiCfgReady = true;
|
||||
console.log('[AI Chat] chatUiConfig 命中本地缓存, 首帧即用真实配置');
|
||||
}
|
||||
} catch (e) { /* 缓存读取失败走预取 */ }
|
||||
if (!this.uiCfgReady) {
|
||||
try {
|
||||
const cfg = await Promise.race([
|
||||
aiService.getChatUiConfig(true),
|
||||
new Promise(resolve => setTimeout(() => resolve(null), 6000))
|
||||
]);
|
||||
if (cfg) this.preloadedUiCfg = cfg;
|
||||
else console.warn('[AI Chat] chatUiConfig 预取超时(6s), 先渲染后由组件异步刷新');
|
||||
} catch (e) { /* 失败兜底默认值 */ }
|
||||
this.uiCfgReady = true;
|
||||
console.log('[AI Chat] chatUiConfig 预取完成, 渲染聊天组件:', !!this.preloadedUiCfg);
|
||||
}
|
||||
// 最新值刷新: 组件 created 的 _loadChatUiConfig() 会异步拉取,
|
||||
// 与此处 force 拉取共享 60s TTL 内存缓存, 不会产生二次 /info 请求
|
||||
|
||||
// ====== AI Agent 通用入参(链接系统 -> 通用 agent_id/title) ======
|
||||
// 老客服入口不传 options 也能跑(智能客服是首个 agent,默认 agent_aikefu)。
|
||||
// 新增 AI 销售/HR/...智能体只需在 link.page_params.agent_id 传不同值,
|
||||
|
||||
Reference in New Issue
Block a user