AI助手交互组件的架构设计
·
一、引言
在健康管理系统中,AI小助手是核心交互模块之一。它不仅需要提供流畅的聊天体验,还需要支持头像自定义、角色形态切换、历史记录管理等复杂功能。本文将从架构设计角度,深入分析AI助手组件的实现细节。
二、组件架构概览
2.1 整体架构设计
AI助手组件采用 状态机模式 管理复杂的交互状态,主要包含以下几个状态:
| 状态 | 描述 | 触发条件 |
|---|---|---|
| isChatOpen | 聊天窗口打开状态 | 点击头像 |
| isSettingsOpen | 设置弹窗打开状态 | 点击设置按钮 |
| isHistoryOpen | 历史记录面板打开状态 | 点击历史按钮 |
| isEditMode | 设置编辑状态 | 点击编辑按钮 |
| isCropModalOpen | 头像裁剪弹窗状态 | 上传头像 |
| isTyping | AI正在回复状态 | 发送消息后 |
2.2 Vue模板结构
核心HTML结构 :
<template>
<div class="ai-assistant">
<!-- 小助手头像 -->
<div class="ai-assistant__avatar-container" @click.stop="toggleChat">
<div class="ai-assistant__avatar">
<div class="ai-assistant__avatar-image">
<div v-if="avatarType === 'icon'" class="ai-assistant__avatar-icon">...</div>
<img v-else :src="avatarUrl" />
<div class="ai-assistant__status" :class="{ 'ai-assistant__status--online': isOnline }"></div>
</div>
<div class="ai-assistant__name">{{ assistantName }}</div>
</div>
</div>
<!-- 聊天窗口 -->
<div class="ai-assistant__chat" :class="{ 'ai-assistant__chat--open': isChatOpen }">
<!-- 头部 -->
<div class="ai-assistant__chat-header">...</div>
<!-- 内容区 -->
<div class="ai-assistant__chat-content">...</div>
<!-- 输入区 -->
<div class="ai-assistant__chat-input">...</div>
</div>
<!-- 设置弹窗 -->
<div class="ai-assistant__settings" v-if="isSettingsOpen">...</div>
<!-- 头像裁剪组件 -->
<ImageCropper ... />
</div>
</template>
设计要点 :
- 使用条件渲染控制不同状态的UI展示
- 通过动态class绑定实现状态切换动画
- 组件采用嵌套结构,职责清晰
三、消息渲染与打字效果
3.1 Markdown消息渲染
JavaScript实现 :
import { marked } from 'marked'
const formatMessage = (content) => {
if (!content) return ''
return marked.parse(content)
}
CSS样式:
.ai-assistant__message-content {
padding: 10px 14px;
border-radius: 18px;
background: rgba(var(--theme-color-rgb), 0.1);
font-size: 14px;
line-height: 1.4;
}
/* Markdown样式支持 */
.ai-assistant__message-content h1, .ai-assistant__message-content h2 {
margin: 8px 0 6px;
font-weight: 600;
}
.ai-assistant__message-content blockquote {
margin: 8px 0;
padding: 8px 12px;
border-left: 3px solid var(--theme-color);
background: rgba(var(--theme-color-rgb), 0.08);
border-radius: 0 8px 8px 0;
}
.ai-assistant__message-content code {
padding: 2px 6px;
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
font-family: monospace;
}
3.2 打字指示器动画
Vue模板 :
<div v-if="isTyping" class="ai-assistant__message">
<div class="ai-assistant__message-avatar">...</div>
<div class="ai-assistant__message-content-wrapper">
<div class="ai-assistant__message-content ai-assistant__message-content--typing">
<div class="ai-assistant__typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
CSS动画:
.ai-assistant__typing-indicator {
display: flex;
gap: 4px;
}
.ai-assistant__typing-indicator span {
width: 8px;
height: 8px;
border-radius: 50%;
background: #999;
animation: typing 1.4s infinite ease-in-out both;
}
.ai-assistant__typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
.ai-assistant__typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
@keyframes typing {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
四、多形态角色系统
4.1 角色配置
JavaScript实现 :
const personalities = ref([
{ id: '温柔', name: '温柔', description: '温柔体贴,耐心解答' },
{ id: '毒舌', name: '毒舌', description: '犀利幽默,直言不讳' },
{ id: '萝莉', name: '萝莉', description: '可爱活泼,充满活力' },
{ id: '御姐', name: '御姐', description: '成熟优雅,气场强大' }
])
const getWelcomeMessage = (name, personalityId) => {
const welcomeMessages = {
温柔: `你好呀!我是${name},你的专属健康小助手~`,
毒舌: `哟,终于想起我来了?我是${name},你的健康顾问。`,
萝莉: `嗨嗨~!我是${name}呀!✨ 今天也要元气满满地保持健康哦!`,
御姐: `你好,我是${name}。你的健康状况,我会帮你打理得井井有条。`,
default: `你好!我是${name},你的AI健康小助手。`
}
return welcomeMessages[personalityId] || welcomeMessages.default
}
4.2 形态选择UI
Vue模板 :
<div class="ai-assistant__settings-personality">
<label>小助手形态</label>
<div class="ai-assistant__personality-options">
<div
v-for="personality in personalities"
:key="personality.id"
class="ai-assistant__personality-option"
:class="{ 'ai-assistant__personality-option--active': selectedPersonality === personality.id }"
@click.stop="handlePersonalityClick(personality.id)"
>
<div class="ai-assistant__personality-info">
<div class="ai-assistant__personality-name">{{ personality.name }}</div>
<div class="ai-assistant__personality-desc">{{ personality.description }}</div>
</div>
<div v-if="selectedPersonality === personality.id" class="ai-assistant__personality-check">
<svg>...</svg>
</div>
</div>
</div>
</div>
CSS样式:
.ai-assistant__personality-options {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.ai-assistant__personality-option {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 12px;
border: 2px solid rgba(0, 0, 0, 0.1);
border-radius: 16px;
background: rgba(255, 255, 255, 0.6);
cursor: pointer;
transition: all 0.3s ease;
}
.ai-assistant__personality-option--active {
background: linear-gradient(135deg, rgba(var(--theme-color-rgb), 0.15), rgba(100, 149, 237, 0.15));
border-color: rgba(var(--theme-color-rgb), 0.5);
box-shadow: 0 4px 15px rgba(var(--theme-color-rgb), 0.2);
}
五、头像上传与裁剪集成
5.1 头像上传流程
JavaScript实现 :
const avatarInput = ref(null)
const triggerAvatarUpload = () => {
if (avatarInput.value) {
avatarInput.value.click()
}
}
const handleAvatarUpload = (event) => {
const file = event.target.files[0]
if (file) {
const reader = new FileReader()
reader.onload = (e) => {
cropImageUrl.value = e.target.result
isCropModalOpen.value = true
}
reader.readAsDataURL(file)
}
}
const handleCropResult = async (croppedFile) => {
// 本地预览
const reader = new FileReader()
reader.onload = (e) => {
avatarUrl.value = e.target.result
avatarType.value = 'custom'
}
reader.readAsDataURL(croppedFile)
// 上传到服务器
try {
const username = localStorage.getItem('username')
if (username) {
const response = await healthService.aiAssistantSettings.uploadAvatar(username, croppedFile)
if (response && response.success && response.avatarData) {
const contentType = response.avatarContentType || 'image/jpeg'
avatarUrl.value = `data:${contentType};base64,${response.avatarData}`
localStorage.setItem('aiAssistantAvatarUrl', avatarUrl.value)
}
}
} catch (error) {
// 上传失败但仍保留本地预览
}
}
5.2 头像区域UI
Vue模板 :
<div class="ai-assistant__settings-avatar">
<div class="ai-assistant__settings-avatar-image" @click.stop="handleAvatarClick">
<div v-if="avatarType === 'icon'" class="ai-assistant__settings-avatar-icon">...</div>
<img v-else :src="avatarUrl" class="ai-assistant__settings-avatar-custom" />
<div class="ai-assistant__settings-avatar-edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</div>
</div>
<input type="file" ref="avatarInput" @change="handleAvatarUpload" accept="image/*" class="ai-assistant__settings-avatar-input" />
</div>
CSS样式:
.ai-assistant__settings-avatar-image {
position: relative;
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(135deg, var(--theme-color), var(--theme-dark));
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(var(--theme-color-rgb), 0.3);
}
.ai-assistant__settings-avatar-image:hover {
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(var(--theme-color-rgb), 0.5);
}
.ai-assistant__settings-avatar-edit {
position: absolute;
bottom: 0;
right: 0;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--theme-color);
display: flex;
align-items: center;
justify-content: center;
color: white;
border: 2px solid rgba(255, 255, 255, 0.8);
transition: all 0.3s ease;
}
.ai-assistant__settings-avatar-edit:hover {
background: var(--theme-light);
transform: scale(1.1);
}
.ai-assistant__settings-avatar-input {
display: none;
}
六、历史记录管理
6.1 历史记录数据同步
JavaScript实现 :
// 保存历史记录到数据库
const saveChatHistoryToDB = async (session) => {
try {
const username = localStorage.getItem('username')
if (!username) return
await healthService.aiAssistant.saveHistory(
username,
session.id,
session.title,
JSON.stringify(session.messages)
)
} catch (error) {
// 保存失败静默处理
}
}
// 从数据库加载历史记录
const loadChatHistoryFromDB = async () => {
try {
const username = localStorage.getItem('username')
if (!username) {
chatHistory.value = []
return
}
const response = await healthService.aiAssistant.getHistoryList(username)
if (response && Array.isArray(response)) {
chatHistory.value = response.map(item => ({
id: item.sessionId,
title: item.title,
timestamp: item.createdAt,
messages: []
}))
}
} catch (error) {
chatHistory.value = []
}
}
// 定期同步(每5分钟)
let syncInterval = null
onMounted(() => {
syncInterval = setInterval(() => {
const currentUsername = localStorage.getItem('username')
if (currentUsername) {
syncSettingsFromDB()
}
}, 5 * 60 * 1000)
})
onUnmounted(() => {
if (syncInterval) {
clearInterval(syncInterval)
}
})
6.2 历史记录列表UI
Vue模板 :
<div v-if="isHistoryOpen" class="ai-assistant__history-panel">
<div class="ai-assistant__history-panel-inner">
<div class="ai-assistant__history-header">
<span>历史记录</span>
<button @click.stop="closeHistoryPanel" class="ai-assistant__history-close">×</button>
</div>
<div v-if="chatHistory.length === 0" class="ai-assistant__history-empty">
<svg>...</svg>
<p>暂无历史记录</p>
</div>
<div v-else class="ai-assistant__history-list">
<div
v-for="(session, index) in chatHistory"
:key="session.id"
class="ai-assistant__history-item"
@click="loadChatSession(session)"
>
<div class="ai-assistant__history-item-icon">
<svg>...</svg>
</div>
<div class="ai-assistant__history-item-content">
<div class="ai-assistant__history-item-title">{{ session.title }}</div>
<div class="ai-assistant__history-item-time">{{ formatHistoryTime(session.timestamp) }}</div>
</div>
<button class="ai-assistant__history-item-delete" @click.stop="deleteChatSession(index)">
<svg>...</svg>
</button>
</div>
</div>
</div>
</div>
CSS样式:
.ai-assistant__history-panel {
position: absolute;
top: 0;
left: 0;
width: 320px;
height: 400px;
background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);
border-radius: 16px;
animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.ai-assistant__history-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 44px 14px 14px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(248, 250, 252, 0.9) 100%);
border-radius: 14px;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.ai-assistant__history-item:hover {
background: linear-gradient(135deg, rgba(255, 255, 255, 1) 0%, rgba(var(--theme-color-rgb), 0.1) 100%);
box-shadow: 0 6px 20px rgba(var(--theme-color-rgb), 0.12);
transform: translateX(4px);
}
.ai-assistant__history-item-delete {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
width: 26px;
height: 26px;
border: none;
background: rgba(239, 68, 68, 0.08);
border-radius: 50%;
cursor: pointer;
color: #ef4444;
opacity: 0;
transition: all 0.25s ease;
}
.ai-assistant__history-item:hover .ai-assistant__history-item-delete {
opacity: 1;
}
七、状态管理与生命周期
7.1 用户隔离机制
JavaScript实现 :
const loadCurrentChat = () => {
const username = localStorage.getItem('username')
const savedChat = localStorage.getItem('aiAssistantCurrentChat')
const savedUser = localStorage.getItem('aiAssistantChatUser')
// 检查是否是同一用户
if (!username || username !== savedUser) {
clearChatStorage()
initNewChat()
return
}
if (savedChat) {
try {
messages.value = JSON.parse(savedChat)
currentChatId.value = savedChatId || Date.now()
} catch (e) {
initNewChat()
}
} else {
initNewChat()
}
}
const clearChatStorage = () => {
localStorage.removeItem('aiAssistantCurrentChat')
localStorage.removeItem('aiAssistantCurrentChatId')
localStorage.removeItem('aiAssistantChatUser')
}
7.2 组件生命周期
JavaScript实现 :
onMounted(async () => {
const username = localStorage.getItem('username')
const savedUser = localStorage.getItem('aiAssistantChatUser')
// 优先从localStorage加载设置
loadSettingsFromLocal()
// 如果没有加载到设置,使用默认值
if (!assistantName.value) {
resetToDefaultSettings()
}
// 检查用户是否未登录或切换了用户
if (!username || username !== savedUser) {
clearChatStorage()
chatHistory.value = []
initNewChat()
if (username) {
localStorage.setItem('aiAssistantChatUser', username)
}
} else {
// 同一用户已登录,异步从数据库同步最新设置
syncSettingsFromDB()
loadCurrentChat()
await loadChatHistoryFromDB()
}
checkAiStatus()
// 设置定期同步
syncInterval = setInterval(() => {
const currentUsername = localStorage.getItem('username')
if (currentUsername) {
syncSettingsFromDB()
}
}, 5 * 60 * 1000)
})
onUnmounted(() => {
if (syncInterval) {
clearInterval(syncInterval)
syncInterval = null
}
})
八、总结
AI助手组件是一个复杂的交互系统,其架构设计体现了以下几个关键原则:
- 状态机模式 :通过多个布尔状态管理复杂的UI交互流程
- 数据持久化 :本地存储与数据库同步相结合,确保数据安全
- 模块化设计 :将头像裁剪、图片查看等功能分离为独立组件
- 用户隔离 :通过localStorage用户标记实现多用户数据隔离
- 定期同步 :后台静默同步机制保持数据最新
这些设计使得AI助手组件既具备良好的用户体验,又保证了数据的安全性和可靠性。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)