页面-心语精灵对话界面
字数
5674 字
阅读时间
30 分钟
版本:v1.0.0 | 更新日期:2025-10-08 页面路径:
miniprogram/independent/heartchat/index分包类型:独立分包 | 依赖云函数:askMessageManager
页面概述
心语精灵对话界面是MindGuard项目的核心AI交互功能,为用户提供7×24小时的智能心理陪伴服务。该页面集成了先进的多Agent对话系统、实时情绪分析、文化洞察和风险评估功能,通过自然语言交互为用户提供个性化的心理支持。
设计目标
- 自然交互:提供流畅、自然的对话体验,降低用户使用门槛
- 智能分析:实时分析用户情绪状态,提供深度心理洞察
- 安全保障:集成风险评估机制,确保用户安全
- 个性化服务:基于用户特点提供定制化回应和建议
- 隐私保护:严格的数据隐私保护,让用户放心交流
页面架构
技术架构
┌─────────────────────────────────────────────────────────────┐
│ 心语精灵对话界面 │
├─────────────────────────────────────────────────────────────┤
│ 展示层 (Presentation Layer) │
│ ├─ Agent选择面板 (AgentSelector) │
│ ├─ 对话消息列表 (MessageList) │
│ ├─ AI分析结果卡片 (AnalysisCards) │
│ ├─ 输入区域 (InputArea) │
│ └─ 功能控制栏 (ControlBar) │
├─────────────────────────────────────────────────────────────┤
│ 逻辑层 (Logic Layer) │
│ ├─ 对话状态管理 (ConversationManager) │
│ ├─ 消息处理 (MessageProcessor) │
│ ├─ 分析结果展示 (AnalysisRenderer) │
│ ├─ 安全监控 (SafetyMonitor) │
│ └─ 缓存管理 (CacheManager) │
├─────────────────────────────────────────────────────────────┤
│ 数据层 (Data Layer) │
│ ├─ 本地存储 (LocalStorage) │
│ ├─ 云函数调用 (CloudFunction) │
│ ├─ 分析数据存储 (AnalysisData) │
│ └─ 用户配置 (UserPreferences) │
└─────────────────────────────────────────────────────────────┘组件结构
HeartChatPage
├── AgentSelector (Agent选择面板)
│ ├── AgentCard (Agent卡片)
│ ├── AgentDescription (Agent介绍)
│ └── AgentHistory (对话历史)
├── MessageList (消息列表)
│ ├── UserMessage (用户消息)
│ ├── AgentMessage (AI回复)
│ ├── AnalysisCard (分析结果卡片)
│ └── SystemMessage (系统消息)
├── InputArea (输入区域)
│ ├── TextInput (文本输入)
│ ├── VoiceInput (语音输入)
│ ├── EmotionPicker (情绪选择)
│ └── QuickActions (快捷操作)
├── ControlBar (控制栏)
│ ├── SettingsButton (设置按钮)
│ ├── HistoryButton (历史记录)
│ ├── ShareButton (分享功能)
│ └── HelpButton (帮助中心)
└── SafetyPanel (安全面板) [按需显示]
├── RiskIndicator (风险指示器)
├── EmergencyContacts (紧急联系)
└── ProfessionalResources (专业资源)核心功能设计
1. Agent选择系统
Agent类型定义
javascript
const AgentTypes = {
cbt_specialist: {
name: 'CBT认知专家',
avatar: '/assets/agents/cbt-specialist.png',
description: '擅长认知行为疗法,帮助你识别和改变非理性思维模式',
specialties: ['认知重构', '情绪管理', '行为激活'],
suitable_for: ['焦虑情绪', '负面思维', '行为问题']
},
emotion_companion: {
name: '情绪陪伴师',
avatar: '/assets/agents/emotion-companion.png',
description: '温暖的倾听者,提供情绪支持和共情理解',
specialties: ['情绪支持', '共情倾听', '心理陪伴'],
suitable_for: ['情绪低落', '孤独感', '需要陪伴']
},
stress_counselor: {
name: '压力管理顾问',
avatar: '/assets/agents/stress-counselor.png',
description: '专业的压力管理专家,提供实用的减压技巧',
specialties: ['压力评估', '放松训练', '时间管理'],
suitable_for: ['学业压力', '工作压力', '生活压力']
},
relationship_guide: {
name: '关系指导师',
avatar: '/assets/agents/relationship-guide.png',
description: '人际关系专家,帮助你改善人际沟通技巧',
specialties: ['沟通技巧', '冲突解决', '关系建立'],
suitable_for: ['人际困扰', '沟通问题', '关系改善']
},
wellness_coach: {
name: '健康生活教练',
avatar: '/assets/agents/wellness-coach.png',
description: '全方位健康生活指导,关注身心协调发展',
specialties: ['生活习惯', '运动建议', '营养指导'],
suitable_for: ['生活规划', '健康咨询', '习惯养成']
}
};Agent选择界面
html
<!-- Agent选择面板 -->
<view class="agent-selector">
<view class="selector-header">
<text class="title">选择你的心语精灵</text>
<text class="subtitle">每个精灵都有独特的专长,找到最适合你的陪伴</text>
</view>
<scroll-view class="agent-list" scroll-x="{{true}}">
<view class="agent-cards">
<view
class="agent-card {{selectedAgent === agent.type ? 'selected' : ''}}"
wx:for="{{availableAgents}}"
wx:key="type"
bindtap="selectAgent"
data-agent="{{item}}"
>
<image class="agent-avatar" src="{{item.avatar}}" />
<text class="agent-name">{{item.name}}</text>
<text class="agent-desc">{{item.description}}</text>
<view class="agent-tags">
<text
class="tag"
wx:for="{{item.specialties}}"
wx:key="*this"
>{{item}}</text>
</view>
</view>
</view>
</scroll-view>
<view class="selector-actions">
<button class="btn-primary" bindtap="startConversation">
开始对话
</button>
</view>
</view>2. 对话消息系统
消息类型定义
javascript
const MessageTypes = {
USER: 'user', // 用户消息
AGENT: 'agent', // AI回复消息
SYSTEM: 'system', // 系统消息
ANALYSIS: 'analysis', // 分析结果卡片
EMERGENCY: 'emergency' // 紧急提示消息
};
const MessageStatus = {
SENDING: 'sending', // 发送中
SENT: 'sent', // 已发送
DELIVERED: 'delivered', // 已送达
READ: 'read', // 已读
FAILED: 'failed' // 发送失败
};消息列表组件
html
<!-- 消息列表 -->
<scroll-view
class="message-list"
scroll-y="{{true}}"
scroll-into-view="{{scrollToMessage}}"
bindscrolltolower="loadMoreMessages"
>
<view class="message-container">
<!-- 系统欢迎消息 -->
<view class="message system-message" wx:if="{{showWelcome}}">
<view class="message-content">
<text class="welcome-text">
欢迎来到心语精灵!我是{{selectedAgent.name}},{{selectedAgent.description}}
</text>
</view>
</view>
<!-- 消息循环 -->
<view
class="message {{item.type}}-message {{item.status}}"
wx:for="{{messageList}}"
wx:key="id"
id="message-{{item.id}}"
>
<!-- 用户消息 -->
<view class="user-message" wx:if="{{item.type === 'user'}}">
<view class="message-content">
<text class="message-text">{{item.content}}</text>
<view class="message-time">{{item.time}}</view>
</view>
<image class="user-avatar" src="{{userInfo.avatar}}" />
</view>
<!-- AI回复消息 -->
<view class="agent-message" wx:if="{{item.type === 'agent'}}">
<image class="agent-avatar" src="{{selectedAgent.avatar}}" />
<view class="message-content">
<text class="message-text">{{item.content}}</text>
<view class="message-actions">
<button class="btn-text" bindtap="likeMessage" data-id="{{item.id}}">
👍 有帮助
</button>
<button class="btn-text" bindtap="regenerateResponse" data-id="{{item.id}}">
🔄 重新生成
</button>
</view>
<view class="message-time">{{item.time}}</view>
</view>
</view>
<!-- 分析结果卡片 -->
<view class="analysis-card" wx:if="{{item.type === 'analysis'}}">
<AnalysisCard
analysis-data="{{item.analysisData}}"
on-expand="onAnalysisCardExpand"
/>
</view>
<!-- 系统消息 -->
<view class="system-message" wx:if="{{item.type === 'system'}}">
<view class="message-content">
<text class="system-text">{{item.content}}</text>
</view>
</view>
<!-- 紧急提示 -->
<view class="emergency-message" wx:if="{{item.type === 'emergency'}}">
<view class="emergency-content">
<view class="emergency-icon">⚠️</view>
<text class="emergency-text">{{item.content}}</text>
<view class="emergency-actions">
<button class="btn-emergency" bindtap="handleEmergency">
立即寻求帮助
</button>
</view>
</view>
</view>
</view>
<!-- 输入状态指示器 -->
<view class="typing-indicator" wx:if="{{isAgentTyping}}">
<image class="agent-avatar" src="{{selectedAgent.avatar}}" />
<view class="typing-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
</view>
</view>
</scroll-view>3. AI分析结果展示
分析结果卡片组件
html
<!-- 分析结果卡片 -->
<view class="analysis-card {{isExpanded ? 'expanded' : 'collapsed'}}">
<!-- 卡片头部 -->
<view class="card-header" bindtap="toggleCard">
<view class="analysis-icon">
<image
class="icon"
src="/assets/icons/analysis-{{analysisData.type}}.png"
/>
</view>
<view class="analysis-title">
<text class="title">{{getAnalysisTitle(analysisData.type)}}</text>
<text class="subtitle">{{getAnalysisSubtitle(analysisData.type)}}</text>
</view>
<view class="expand-icon">
<text class="icon">{{isExpanded ? '▲' : '▼'}}</text>
</view>
</view>
<!-- 卡片内容 -->
<view class="card-content" wx:if="{{isExpanded}}">
<!-- 情感分析结果 -->
<view class="emotion-analysis" wx:if="{{analysisData.type === 'emotion'}}">
<view class="emotion-summary">
<view class="primary-emotion">
<text class="emotion-emoji">{{getEmotionEmoji(analysisData.primary_emotion)}}</text>
<text class="emotion-text">{{analysisData.primary_emotion}}</text>
<text class="emotion-intensity">{{analysisData.intensity * 100}}%</text>
</view>
<view class="secondary-emotions" wx:if="{{analysisData.secondary_emotions.length > 0}}">
<text class="label">其他情绪:</text>
<view class="emotion-tags">
<text
class="emotion-tag"
wx:for="{{analysisData.secondary_emotions}}"
wx:key="*this"
>{{item}}</text>
</view>
</view>
</view>
<!-- 情绪雷达图 -->
<view class="radar-chart-container">
<canvas
class="emotion-radar"
canvas-id="emotionRadar{{cardId}}"
bind:tap="showRadarDetail"
/>
</view>
<!-- 情绪建议 -->
<view class="emotion-suggestions">
<text class="suggestions-title">💡 情绪建议</text>
<view class="suggestions-list">
<text
class="suggestion-item"
wx:for="{{getEmotionSuggestions(analysisData)}}"
wx:key="*this"
>• {{item}}</text>
</view>
</view>
</view>
<!-- 文化洞察结果 -->
<view class="culture-analysis" wx:if="{{analysisData.type === 'culture'}}">
<view class="culture-keywords">
<text class="keywords-title">🏮 文化关键词</text>
<view class="keywords-list">
<text
class="keyword-item"
wx:for="{{analysisData.topic_keywords}}"
wx:key="*this"
>{{item}}</text>
</view>
</view>
<view class="culture-triggers">
<text class="triggers-title">💭 情绪触发点</text>
<view class="triggers-list">
<text
class="trigger-item"
wx:for="{{analysisData.emotion_triggers}}"
wx:key="*this"
>{{item}}</text>
</view>
</view>
<view class="culture-context">
<text class="context-title">📖 文化背景解读</text>
<text class="context-text">{{analysisData.cultural_context_notes}}</text>
</view>
<view class="culture-advice">
<text class="advice-title">🎯 个性化建议</text>
<view class="advice-list">
<text
class="advice-item"
wx:for="{{getCulturalSuggestions(analysisData)}}"
wx:key="*this"
>• {{item}}</text>
</view>
</view>
</view>
<!-- 风险评估结果 -->
<view class="risk-analysis" wx:if="{{analysisData.type === 'risk'}}">
<view class="risk-level {{getRiskLevelClass(analysisData.risk_level)}}">
<view class="risk-icon">{{getRiskIcon(analysisData.risk_level)}}</view>
<view class="risk-info">
<text class="risk-title">{{getRiskTitle(analysisData.risk_level)}}</text>
<text class="risk-description">{{getRiskDescription(analysisData.risk_level)}}</text>
</view>
</view>
<view class="risk-summary" wx:if="{{analysisData.summary}}">
<text class="summary-title">📋 风险评估摘要</text>
<text class="summary-text">{{analysisData.summary}}</text>
</view>
<!-- 高风险时显示紧急资源 -->
<view class="emergency-resources" wx:if="{{analysisData.risk_level >= 3}}">
<text class="resources-title">🚨 紧急支持资源</text>
<view class="resources-list">
<view
class="resource-item"
wx:for="{{getEmergencyResources()}}"
wx:key="name"
bindtap="contactResource"
data-resource="{{item}}"
>
<text class="resource-name">{{item.name}}</text>
<text class="resource-contact">{{item.contact}}</text>
</view>
</view>
</view>
</view>
</view>
</view>情绪雷达图实现
javascript
// emotion-radar.js
Component({
properties: {
analysisData: {
type: Object,
value: {}
}
},
data: {
radarData: null,
animationProgress: 0
},
lifetimes: {
attached() {
this.initRadarChart();
}
},
methods: {
initRadarChart() {
const { radar_dimensions } = this.properties.analysisData;
if (!radar_dimensions) return;
// 转换数据格式
this.radarData = {
trust: radar_dimensions.trust * 100,
openness: radar_dimensions.openness * 100,
resistance: radar_dimensions.resistance * 100,
stress: radar_dimensions.stress * 100,
control: radar_dimensions.control * 100
};
// 启动动画
this.animateRadar();
},
animateRadar() {
const animationDuration = 1000; // 1秒动画
const startTime = Date.now();
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / animationDuration, 1);
// 使用缓动函数
const easeProgress = this.easeInOutCubic(progress);
this.setData({ animationProgress: easeProgress });
if (progress < 1) {
this.animationFrameId = requestAnimationFrame(animate);
} else {
this.drawRadarChart();
}
};
animate();
},
drawRadarChart() {
const ctx = wx.createCanvasContext('emotionRadar');
const centerX = 100;
const centerY = 100;
const radius = 80;
// 绘制网格
this.drawGrid(ctx, centerX, centerY, radius);
// 绘制数据
this.drawData(ctx, centerX, centerY, radius, this.radarData);
ctx.draw();
},
drawGrid(ctx, centerX, centerY, radius) {
const levels = 5;
const dimensions = 5;
// 绘制同心圆
for (let i = 1; i <= levels; i++) {
const levelRadius = (radius / levels) * i;
ctx.beginPath();
for (let j = 0; j <= dimensions; j++) {
const angle = (Math.PI * 2 / dimensions) * j - Math.PI / 2;
const x = centerX + Math.cos(angle) * levelRadius;
const y = centerY + Math.sin(angle) * levelRadius;
if (j === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.closePath();
ctx.setStrokeStyle('rgba(200, 200, 200, 0.3)');
ctx.stroke();
}
// 绘制轴线
for (let i = 0; i < dimensions; i++) {
const angle = (Math.PI * 2 / dimensions) * i - Math.PI / 2;
const x = centerX + Math.cos(angle) * radius;
const y = centerY + Math.sin(angle) * radius;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(x, y);
ctx.setStrokeStyle('rgba(150, 150, 150, 0.5)');
ctx.stroke();
}
},
drawData(ctx, centerX, centerY, radius, data) {
const dimensions = Object.keys(data);
const angleStep = (Math.PI * 2) / dimensions;
// 绘制数据多边形
ctx.beginPath();
dimensions.forEach((dimension, index) => {
const angle = angleStep * index - Math.PI / 2;
const value = data[dimension] / 100; // 归一化到0-1
const distance = value * radius * this.data.animationProgress;
const x = centerX + Math.cos(angle) * distance;
const y = centerY + Math.sin(angle) * distance;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.closePath();
// 填充和描边
ctx.setFillStyle('rgba(74, 144, 226, 0.3)');
ctx.fill();
ctx.setStrokeStyle('#4a90e2');
ctx.stroke();
// 绘制数据点
dimensions.forEach((dimension, index) => {
const angle = angleStep * index - Math.PI / 2;
const value = data[dimension] / 100;
const distance = value * radius * this.data.animationProgress;
const x = centerX + Math.cos(angle) * distance;
const y = centerY + Math.sin(angle) * distance;
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.setFillStyle('#4a90e2');
ctx.fill();
ctx.setStrokeStyle('white');
ctx.stroke();
});
},
easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
},
showRadarDetail() {
this.triggerEvent('showDetail', {
radarData: this.radarData
});
}
}
});4. 输入系统设计
多模态输入组件
html
<!-- 输入区域 -->
<view class="input-area">
<!-- 快捷情绪选择 -->
<view class="emotion-picker" wx:if="{{showEmotionPicker}}">
<scroll-view class="emotion-list" scroll-x="{{true}}">
<view class="emotion-items">
<view
class="emotion-item {{selectedEmotion === item.type ? 'selected' : ''}}"
wx:for="{{emotionOptions}}"
wx:key="type"
bindtap="selectEmotion"
data-emotion="{{item}}"
>
<text class="emotion-emoji">{{item.emoji}}</text>
<text class="emotion-label">{{item.label}}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 主输入框 -->
<view class="input-container">
<!-- 语音输入按钮 -->
<button
class="voice-btn {{isRecording ? 'recording' : ''}}"
bindtouchstart="startRecording"
bindtouchend="stopRecording"
wx:if="{{!inputText}}"
>
<text class="voice-icon">🎤</text>
</button>
<!-- 文本输入框 -->
<input
class="text-input"
type="text"
placeholder="分享你的想法..."
value="{{inputText}}"
bindinput="onInputChange"
bindconfirm="sendMessage"
focus="{{inputFocused}}"
maxlength="500"
/>
<!-- 表情按钮 -->
<button
class="emoji-btn"
bindtap="toggleEmotionPicker"
wx:if="{{!inputText}}"
>
<text class="emoji-icon">😊</text>
</button>
<!-- 发送按钮 -->
<button
class="send-btn {{inputText ? 'active' : ''}}"
bindtap="sendMessage"
disabled="{{!inputText}}"
>
<text class="send-icon">➤</text>
</button>
</view>
<!-- 快捷操作栏 -->
<view class="quick-actions">
<button
class="quick-action"
wx:for="{{quickActions}}"
wx:key="type"
bindtap="sendQuickAction"
data-action="{{item}}"
>
<text class="action-icon">{{item.icon}}</text>
<text class="action-label">{{item.label}}</text>
</button>
</view>
</view>输入处理逻辑
javascript
// input-handler.js
Page({
data: {
inputText: '',
isRecording: false,
showEmotionPicker: false,
selectedEmotion: null,
emotionOptions: [
{ type: 'happy', emoji: '😊', label: '开心' },
{ type: 'sad', emoji: '😢', label: '难过' },
{ type: 'angry', emoji: '😠', label: '生气' },
{ type: 'anxious', emoji: '😰', label: '焦虑' },
{ type: 'confused', emoji: '😕', label: '困惑' },
{ type: 'calm', emoji: '😌', label: '平静' }
],
quickActions: [
{ type: 'mood_report', icon: '📊', label: '心情报告' },
{ type: 'breathing', icon: '🫁', label: '呼吸练习' },
{ type: 'gratitude', icon: '🙏', label: '感恩日记' },
{ type: 'goal_setting', icon: '🎯', label: '目标设定' }
]
},
// 文本输入处理
onInputChange(e) {
this.setData({
inputText: e.detail.value
});
},
// 发送消息
async sendMessage() {
const { inputText, selectedEmotion } = this.data;
if (!inputText.trim()) return;
// 构建消息内容
const messageContent = this.buildMessageContent(inputText, selectedEmotion);
// 添加用户消息到列表
this.addUserMessage(messageContent);
// 清空输入
this.setData({
inputText: '',
selectedEmotion: null,
showEmotionPicker: false
});
// 发送到AI处理
await this.processMessage(messageContent);
},
// 构建消息内容
buildMessageContent(text, emotion) {
let content = text;
if (emotion) {
content = `[${emotion.label}] ${content}`;
}
return content;
},
// 处理消息(调用云函数)
async processMessage(content) {
try {
// 显示AI正在输入
this.setData({ isAgentTyping: true });
// 调用云函数
const result = await wx.cloud.callFunction({
name: 'askMessageManager',
data: {
action: 'sendMessage',
message: content,
sessionId: this.data.currentSessionId,
agentType: this.data.selectedAgent.type
}
});
const { response, analysis } = result.result;
// 添加AI回复消息
this.addAgentMessage(response);
// 添加分析结果卡片
if (analysis && analysis.length > 0) {
this.addAnalysisCards(analysis);
}
} catch (error) {
console.error('消息处理失败:', error);
this.addSystemMessage('抱歉,我现在无法回复。请稍后再试。');
} finally {
this.setData({ isAgentTyping: false });
}
},
// 语音输入处理
startRecording(e) {
if (!this.checkRecordPermission()) {
return;
}
this.setData({ isRecording: true });
// 开始录音
wx.startRecord({
success: (res) => {
console.log('录音开始');
},
fail: (err) => {
console.error('录音失败:', err);
this.setData({ isRecording: false });
}
});
},
stopRecording() {
if (!this.data.isRecording) return;
wx.stopRecord({
success: (res) => {
const tempFilePath = res.tempFilePath;
this.processVoiceMessage(tempFilePath);
},
fail: (err) => {
console.error('录音结束失败:', err);
}
});
this.setData({ isRecording: false });
},
// 处理语音消息
async processVoiceMessage(filePath) {
try {
// 语音转文字
const transcription = await this.transcribeVoice(filePath);
if (transcription) {
this.setData({ inputText: transcription });
// 自动发送语音转文字的结果
await this.sendMessage();
}
} catch (error) {
console.error('语音处理失败:', error);
this.addSystemMessage('语音识别失败,请重试或使用文字输入。');
}
},
// 语音转文字
transcribeVoice(filePath) {
return new Promise((resolve, reject) => {
wx.cloud.callFunction({
name: 'askMessageManager',
data: {
action: 'transcribeVoice',
filePath: filePath
},
success: (res) => {
resolve(res.result.text);
},
fail: (err) => {
reject(err);
}
});
});
},
// 情绪选择处理
selectEmotion(e) {
const emotion = e.currentTarget.dataset.emotion;
this.setData({
selectedEmotion: emotion,
showEmotionPicker: false
});
},
// 快捷操作处理
sendQuickAction(e) {
const action = e.currentTarget.dataset.action;
const quickMessages = {
mood_report: '我想了解一下我的情绪状态',
breathing: '我现在压力很大,想做一些呼吸练习',
gratitude: '今天有什么值得感恩的事情吗?',
goal_setting: '帮我设定一个小目标'
};
const message = quickMessages[action.type];
if (message) {
this.setData({ inputText: message });
this.sendMessage();
}
}
});5. 安全监控系统
风险监控组件
html
<!-- 安全监控面板 -->
<view class="safety-panel {{riskLevel > 0 ? 'visible' : 'hidden'}}">
<!-- 风险指示器 -->
<view class="risk-indicator level-{{riskLevel}}">
<view class="risk-icon">{{getRiskIcon(riskLevel)}}</view>
<view class="risk-info">
<text class="risk-title">{{getRiskTitle(riskLevel)}}</text>
<text class="risk-description">{{getRiskDescription(riskLevel)}}</text>
</view>
</view>
<!-- 紧急操作(高风险时显示) -->
<view class="emergency-actions" wx:if="{{riskLevel >= 3}}">
<button
class="emergency-contact"
bindtap="contactEmergencyService"
>
📞 联系心理热线
</button>
<button
class="emergency-location"
bindtap="shareLocation"
>
📍 分享位置
</button>
<button
class="emergency-contact-person"
bindtap="contactEmergencyPerson"
>
👥 联系紧急联系人
</button>
</view>
<!-- 安抚资源(低中风险时显示) -->
<view class="calming-resources" wx:if="{{riskLevel < 3}}">
<button
class="calming-action"
bindtap="startBreathingExercise"
>
🫁 呼吸练习
</button>
<button
class="calming-action"
bindtap="playCalmingMusic"
>
🎵 放松音乐
</button>
<button
class="calming-action"
bindtap="showPositiveThoughts"
>
💭 积极思考
</button>
</view>
</view>安全监控逻辑
javascript
// safety-monitor.js
const SafetyMonitor = {
// 风险等级评估
assessRiskLevel(analysisResults) {
let maxRiskLevel = 0;
let riskFactors = [];
analysisResults.forEach(analysis => {
if (analysis.type === 'risk') {
maxRiskLevel = Math.max(maxRiskLevel, analysis.risk_level);
if (analysis.risk_level >= 2) {
riskFactors.push({
type: 'risk_assessment',
level: analysis.risk_level,
details: analysis.summary
});
}
}
});
return {
level: maxRiskLevel,
factors: riskFactors,
requiresIntervention: maxRiskLevel >= 3
};
},
// 触发安全响应
triggerSafetyResponse(riskAssessment) {
const { level, factors, requiresIntervention } = riskAssessment;
switch (level) {
case 0:
// 无风险,正常监控
this.startNormalMonitoring();
break;
case 1:
// 轻度风险,增加关注
this.startEnhancedMonitoring();
break;
case 2:
// 中度风险,主动干预
this.startProactiveIntervention();
break;
case 3:
// 高度风险,紧急干预
this.startEmergencyIntervention();
break;
case 4:
// 紧急危机,立即响应
this.triggerCrisisResponse();
break;
}
},
// 紧急干预流程
startEmergencyIntervention() {
// 显示紧急资源面板
this.showEmergencyPanel();
// 记录安全事件
this.logSafetyEvent({
level: 3,
timestamp: new Date().toISOString(),
actions: ['show_emergency_resources', 'increase_monitoring']
});
// 通知安全团队(如果是生产环境)
if (this.isProduction()) {
this.notifySafetyTeam();
}
},
// 危机响应流程
triggerCrisisResponse() {
// 立即联系紧急服务
this.contactEmergencyServices();
// 通知紧急联系人
this.notifyEmergencyContacts();
// 保持用户在线
this.keepUserOnline();
// 记录危机事件
this.logCrisisEvent({
level: 4,
timestamp: new Date().toISOString(),
actions: ['contact_emergency_services', 'notify_contacts', 'keep_online']
});
},
// 联系紧急服务
async contactEmergencyServices() {
const emergencyNumbers = [
{ name: '心理援助热线', phone: '400-161-9995' },
{ name: '危机干预中心', phone: '010-82951332' }
];
for (const service of emergencyNumbers) {
try {
await wx.makePhoneCall({
phoneNumber: service.phone
});
break; // 成功拨打电话后退出循环
} catch (error) {
console.error(`拨打${service.name}失败:`, error);
}
}
},
// 分享位置信息
async shareLocation() {
try {
const location = await this.getCurrentLocation();
// 发送位置给紧急联系人
await this.sendLocationToContacts(location);
// 显示位置信息给用户
this.showLocationInfo(location);
} catch (error) {
console.error('获取位置失败:', error);
wx.showToast({
title: '位置获取失败',
icon: 'none'
});
}
},
// 获取当前位置
getCurrentLocation() {
return new Promise((resolve, reject) => {
wx.getLocation({
type: 'gcj02',
success: (res) => {
resolve({
latitude: res.latitude,
longitude: res.longitude,
accuracy: res.accuracy,
timestamp: new Date().toISOString()
});
},
fail: reject
});
});
}
};样式设计
主要样式规范
scss
// 心语精灵对话界面样式
.heartchat-page {
height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
// Agent选择面板
.agent-selector {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
margin: 16px;
padding: 20px;
.selector-header {
text-align: center;
margin-bottom: 20px;
.title {
font-size: 18px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
color: #7f8c8d;
line-height: 1.4;
}
}
.agent-cards {
display: flex;
gap: 16px;
padding: 0 8px;
.agent-card {
min-width: 120px;
padding: 16px;
border-radius: 12px;
background: white;
border: 2px solid transparent;
transition: all 0.3s ease;
&.selected {
border-color: #4a90e2;
background: #f0f8ff;
}
.agent-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
margin: 0 auto 12px;
}
.agent-name {
font-size: 14px;
font-weight: 600;
color: #2c3e50;
text-align: center;
display: block;
margin-bottom: 8px;
}
.agent-desc {
font-size: 12px;
color: #7f8c8d;
line-height: 1.3;
text-align: center;
display: block;
margin-bottom: 12px;
}
.agent-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
.tag {
font-size: 10px;
padding: 2px 6px;
background: #e8f4fd;
color: #4a90e2;
border-radius: 4px;
}
}
}
}
}
// 消息列表
.message-list {
flex: 1;
padding: 0 16px;
.message {
margin-bottom: 16px;
&.user-message {
display: flex;
justify-content: flex-end;
.message-content {
max-width: 70%;
background: white;
border-radius: 16px 16px 4px 16px;
padding: 12px 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.message-text {
font-size: 15px;
color: #2c3e50;
line-height: 1.4;
}
.message-time {
font-size: 11px;
color: #bdc3c7;
margin-top: 4px;
text-align: right;
}
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
margin-left: 8px;
}
}
&.agent-message {
display: flex;
justify-content: flex-start;
.agent-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
margin-right: 8px;
}
.message-content {
max-width: 70%;
background: #f8f9fa;
border-radius: 16px 16px 16px 4px;
padding: 12px 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.message-text {
font-size: 15px;
color: #2c3e50;
line-height: 1.4;
}
.message-actions {
margin-top: 8px;
display: flex;
gap: 12px;
.btn-text {
font-size: 12px;
color: #4a90e2;
background: transparent;
border: none;
padding: 0;
&:active {
opacity: 0.6;
}
}
}
.message-time {
font-size: 11px;
color: #bdc3c7;
margin-top: 8px;
}
}
}
&.analysis-card {
margin: 20px 0;
}
}
// 输入状态指示器
.typing-indicator {
display: flex;
align-items: center;
margin-bottom: 16px;
.agent-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
margin-right: 8px;
}
.typing-dots {
background: #f8f9fa;
border-radius: 16px;
padding: 12px 16px;
display: flex;
gap: 4px;
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #bdc3c7;
animation: typing 1.4s infinite;
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
}
}
}
// 输入区域
.input-area {
background: white;
border-radius: 20px 20px 0 0;
padding: 16px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
.emotion-picker {
margin-bottom: 12px;
.emotion-items {
display: flex;
gap: 12px;
.emotion-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
border-radius: 8px;
background: #f8f9fa;
transition: all 0.2s ease;
&.selected {
background: #e8f4fd;
border: 1px solid #4a90e2;
}
.emotion-emoji {
font-size: 20px;
margin-bottom: 4px;
}
.emotion-label {
font-size: 10px;
color: #7f8c8d;
}
}
}
}
.input-container {
display: flex;
align-items: center;
gap: 8px;
background: #f8f9fa;
border-radius: 25px;
padding: 8px 16px;
.voice-btn, .emoji-btn {
width: 32px;
height: 32px;
border-radius: 50%;
background: transparent;
border: none;
display: flex;
align-items: center;
justify-content: center;
&.recording {
background: #e74c3c;
animation: pulse 1s infinite;
}
}
.text-input {
flex: 1;
background: transparent;
border: none;
font-size: 15px;
color: #2c3e50;
padding: 8px 0;
}
.send-btn {
width: 32px;
height: 32px;
border-radius: 50%;
background: #4a90e2;
border: none;
color: white;
display: flex;
align-items: center;
justify-content: center;
&:not(.active) {
background: #bdc3c7;
}
}
}
.quick-actions {
display: flex;
gap: 8px;
margin-top: 12px;
.quick-action {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
background: #f8f9fa;
border-radius: 8px;
border: none;
.action-icon {
font-size: 16px;
margin-bottom: 4px;
}
.action-label {
font-size: 10px;
color: #7f8c8d;
}
}
}
}
// 安全面板
.safety-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border-radius: 16px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
z-index: 1000;
min-width: 300px;
.risk-indicator {
display: flex;
align-items: center;
margin-bottom: 16px;
.risk-icon {
font-size: 24px;
margin-right: 12px;
}
.risk-info {
.risk-title {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 4px;
}
.risk-description {
font-size: 14px;
color: #7f8c8d;
}
}
}
.emergency-actions, .calming-resources {
display: flex;
flex-direction: column;
gap: 8px;
button {
padding: 12px 16px;
border-radius: 8px;
border: none;
font-size: 14px;
transition: all 0.2s ease;
&:active {
opacity: 0.8;
}
}
}
.emergency-actions {
button {
background: #e74c3c;
color: white;
}
}
.calming-resources {
button {
background: #3498db;
color: white;
}
}
}
}
// 动画定义
@keyframes typing {
0%, 60%, 100% {
opacity: 0.3;
}
30% {
opacity: 1;
}
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(231, 76, 60, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(231, 76, 60, 0);
}
}数据流设计
页面数据结构
javascript
// 页面数据结构
const pageData = {
// Agent相关
selectedAgent: null,
availableAgents: [],
agentHistory: [],
// 会话相关
currentSessionId: null,
messageList: [],
isLoading: false,
isAgentTyping: false,
// 输入相关
inputText: '',
selectedEmotion: null,
showEmotionPicker: false,
isRecording: false,
// 分析结果
analysisResults: [],
currentRiskLevel: 0,
safetyPanelVisible: false,
// 用户信息
userInfo: {
avatar: '',
nickname: ''
},
// 配置
scrollToMessage: '',
showWelcome: true,
// 缓存
messageCache: new Map(),
analysisCache: new Map()
};消息数据结构
javascript
// 消息数据结构
const messageStructure = {
id: 'unique_message_id',
type: 'user|agent|system|analysis|emergency',
content: 'message_content',
timestamp: '2025-10-08T15:30:00Z',
status: 'sending|sent|delivered|read|failed',
// 用户消息特有
userInfo: {
avatar: 'avatar_url',
nickname: 'user_nickname'
},
// Agent消息特有
agentInfo: {
type: 'cbt_specialist',
name: 'CBT认知专家',
avatar: 'agent_avatar_url'
},
// 分析结果特有
analysisData: {
type: 'emotion|culture|risk',
data: { /* 分析结果数据 */ },
confidence: 0.95
},
// 元数据
metadata: {
sessionId: 'session_id',
messageId: 'original_message_id',
processingTime: 250,
modelVersion: 'v2.1.0'
}
};性能优化策略
1. 消息列表优化
- 虚拟滚动:大量消息时使用虚拟滚动技术
- 图片懒加载:头像和图片懒加载
- 消息缓存:本地缓存消息数据,减少网络请求
2. 分析结果优化
- 异步加载:分析结果异步加载和渲染
- 数据压缩:压缩分析数据传输量
- 缓存策略:智能缓存常用分析结果
3. 输入体验优化
- 输入预测:基于历史数据提供输入建议
- 语音识别优化:优化语音识别准确率
- 快捷操作:提供常用短语快捷输入
测试策略
功能测试
javascript
// Agent选择测试
describe('Agent Selection', () => {
test('should select agent successfully', () => {
const agent = { type: 'cbt_specialist', name: 'CBT认知专家' };
page.selectAgent(agent);
expect(page.data.selectedAgent).toEqual(agent);
});
test('should load agent history', async () => {
await page.loadAgentHistory();
expect(page.data.agentHistory.length).toBeGreaterThan(0);
});
});
// 消息发送测试
describe('Message Sending', () => {
test('should send text message successfully', async () => {
page.setData({ inputText: 'Hello, I need help' });
await page.sendMessage();
const lastMessage = page.data.messageList[page.data.messageList.length - 1];
expect(lastMessage.type).toBe('user');
expect(lastMessage.content).toContain('Hello, I need help');
});
test('should handle voice message input', async () => {
// Mock voice recording
const mockVoiceData = 'mock_voice_data';
await page.processVoiceMessage(mockVoiceData);
expect(page.data.inputText).toBeDefined();
});
});
// 分析结果展示测试
describe('Analysis Display', () => {
test('should display emotion analysis correctly', () => {
const analysisData = {
type: 'emotion',
primary_emotion: 'happy',
radar_dimensions: {
trust: 0.8,
openness: 0.7,
resistance: 0.2,
stress: 0.3,
control: 0.9
}
};
page.addAnalysisCards([analysisData]);
const analysisCard = page.data.messageList.find(msg => msg.type === 'analysis');
expect(analysisCard).toBeDefined();
expect(analysisCard.analysisData.type).toBe('emotion');
});
});性能测试
javascript
// 性能测试
describe('Performance Tests', () => {
test('should handle large message list efficiently', () => {
const startTime = performance.now();
// 添加1000条消息
for (let i = 0; i < 1000; i++) {
page.addMockMessage();
}
const endTime = performance.now();
const renderTime = endTime - startTime;
expect(renderTime).toBeLessThan(1000); // 1秒内完成渲染
});
test('should cache analysis results effectively', () => {
const analysisData = { type: 'emotion', data: {} };
const startTime = performance.now();
page.cacheAnalysisResult('test_key', analysisData);
const cachedResult = page.getCachedAnalysisResult('test_key');
const endTime = performance.now();
const cacheTime = endTime - startTime;
expect(cacheTime).toBeLessThan(10); // 10ms内完成缓存操作
expect(cachedResult).toEqual(analysisData);
});
});相关文档
版本记录
| 版本 | 日期 | 变更内容 | 作者 |
|---|---|---|---|
| v1.0.0 | 2025-10-08 | 初始版本,完成心语精灵对话界面核心设计 | 哈雷酱 |