This commit is contained in:
2026-09-24 17:05:26 +08:00
6 changed files with 370 additions and 12 deletions

View File

@@ -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) {

View File

@@ -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 传不同值,