关键要点
- CBT 核心概念数据建模:思维记录、情绪追踪、认知重构的数据结构设计
- 状态管理选型:使用 Zustand 轻量级管理复杂应用状态,支持持久化和中间件
- 不可变数据更新模式:使用 Immer 简化嵌套状态更新,确保状态可预测性
- 数据持久化策略:加密存储敏感心理健康数据,确保隐私安全
- 时间序列数据优化:高效存储和查询情绪、思维记录的历史数据
认知行为疗法(CBT)是心理健康领域最广泛使用的循证治疗方法之一。本教程将教你如何构建一个功能完整的 CBT 应用,帮助用户识别和改变消极思维模式。
前置条件:
- React 18+ 和 TypeScript 基础
- 了解基本的状态管理概念
- 熟悉 CBT 基本原理(可选)
CBT 应用核心数据模型
思维记录 (Thought Record)
code
// types/cbt.ts
export interface ThoughtRecord {
id: string;
userId: string;
createdAt: Date;
updatedAt: Date;
// 触发情境
situation: {
description: string;
context: string; // 'work' | 'home' | 'social' | 'other'
timestamp: Date;
intensity: number; // 1-10
};
// 自动思维
automaticThoughts: AutomaticThought[];
// 情绪
emotions: EmotionEntry[];
// 身体感觉
bodySensations?: string[];
// 行为
behaviors: string[];
// 认知重构
cognitiveRestructuring?: {
alternativeThoughts: string[];
evidenceFor?: string[];
evidenceAgainst?: string[];
balancedThought: string;
beliefInBalancedThought: number; // 0-100%
};
// 结果
outcome?: {
moodBefore: number;
moodAfter: number;
insight?: string;
};
// 标签和分类
tags?: string[];
cognitiveDistortions?: CognitiveDistortion[];
}
export interface AutomaticThought {
id: string;
thought: string;
beliefDegree: number; // 0-100%
type?: 'hot' | 'intermediate' | 'core';
}
export interface EmotionEntry {
emotion: EmotionType;
intensity: number; // 0-100
beforeIntervention?: number;
}
export type EmotionType =
| 'anxiety'
| 'sadness'
| 'anger'
| 'shame'
| 'guilt'
| 'joy'
| 'calm'
| 'other';
export type CognitiveDistortion =
| 'all_or_nothing'
| 'overgeneralization'
| 'mental_filter'
| 'disqualifying_positive'
| 'jumping_conclusions'
| 'magnification'
| 'emotional_reasoning'
| 'should_statements'
| 'labeling'
| 'personalization'
| 'catastrophizing';
export interface EmotionLog {
id: string;
userId: string;
timestamp: Date;
emotions: EmotionEntry[];
triggers?: string[];
activities?: string[];
notes?: string;
}
export interface CBTExercise {
id: string;
type: 'thought_record' | 'behavioral_experiment' | 'exposure' | 'relaxation';
title: string;
description: string;
steps: string[];
duration?: number;
completedAt?: Date;
}
export interface ProgressTracking {
userId: string;
weekStart: Date;
metrics: {
thoughtRecordsCount: number;
averageMoodImprovement: number;
mostCommonDistortions: CognitiveDistortion[];
completedExercises: number;
};
}
Code collapsed
设计高效的数据结构
使用 Immer 简化状态更新
code
// store/ThoughtRecordStore.ts
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { persist } from 'zustand/middleware';
import { ThoughtRecord, AutomaticThought, EmotionEntry } from '../types/cbt';
interface ThoughtRecordState {
records: ThoughtRecord[];
activeRecord: Partial<ThoughtRecord> | null;
filter: {
dateRange?: [Date, Date];
emotions?: EmotionType[];
cognitiveDistortions?: CognitiveDistortion[];
};
// Actions
setActiveRecord: (record: Partial<ThoughtRecord> | null) => void;
updateActiveRecord: (updates: Partial<ThoughtRecord>) => void;
saveActiveRecord: () => void;
deleteRecord: (id: string) => void;
addAutomaticThought: (thought: Omit<AutomaticThought, 'id'>) => void;
updateAutomaticThought: (id: string, updates: Partial<AutomaticThought>) => void;
removeAutomaticThought: (id: string) => void;
addEmotion: (emotion: EmotionEntry) => void;
updateEmotion: (index: number, updates: Partial<EmotionEntry>) => void;
removeEmotion: (index: number) => void;
completeCognitiveRestructuring: (restructuring: ThoughtRecord['cognitiveRestructuring']) => void;
setFilter: (filter: Partial<ThoughtRecordState['filter']>) => void;
}
export const useThoughtRecordStore = create<ThoughtRecordState>()(
persist(
immer((set, get) => ({
records: [],
activeRecord: null,
filter: {},
setActiveRecord: (record) => {
set((state) => {
state.activeRecord = record;
});
},
updateActiveRecord: (updates) => {
set((state) => {
if (!state.activeRecord) {
state.activeRecord = { id: crypto.randomUUID(), createdAt: new Date(), updatedAt: new Date() };
}
Object.assign(state.activeRecord!, updates);
state.activeRecord!.updatedAt = new Date();
});
},
saveActiveRecord: () => {
set((state) => {
if (!state.activeRecord) return;
const existingIndex = state.records.findIndex(
(r) => r.id === state.activeRecord!.id
);
if (existingIndex >= 0) {
state.records[existingIndex] = state.activeRecord as ThoughtRecord;
} else {
state.records.push(state.activeRecord as ThoughtRecord);
}
state.activeRecord = null;
});
},
deleteRecord: (id) => {
set((state) => {
state.records = state.records.filter((r) => r.id !== id);
});
},
addAutomaticThought: (thought) => {
set((state) => {
if (!state.activeRecord) {
state.activeRecord = { id: crypto.randomUUID(), createdAt: new Date(), updatedAt: new Date() };
}
if (!state.activeRecord.automaticThoughts) {
state.activeRecord.automaticThoughts = [];
}
state.activeRecord.automaticThoughts.push({
...thought,
id: crypto.randomUUID(),
});
});
},
updateAutomaticThought: (id, updates) => {
set((state) => {
if (!state.activeRecord?.automaticThoughts) return;
const thought = state.activeRecord.automaticThoughts.find((t) => t.id === id);
if (thought) {
Object.assign(thought, updates);
}
});
},
removeAutomaticThought: (id) => {
set((state) => {
if (!state.activeRecord?.automaticThoughts) return;
state.activeRecord.automaticThoughts = state.activeRecord.automaticThoughts.filter(
(t) => t.id !== id
);
});
},
addEmotion: (emotion) => {
set((state) => {
if (!state.activeRecord) {
state.activeRecord = { id: crypto.randomUUID(), createdAt: new Date(), updatedAt: new Date() };
}
if (!state.activeRecord.emotions) {
state.activeRecord.emotions = [];
}
state.activeRecord.emotions.push(emotion);
});
},
updateEmotion: (index, updates) => {
set((state) => {
if (!state.activeRecord?.emotions) return;
if (state.activeRecord.emotions[index]) {
Object.assign(state.activeRecord.emotions[index], updates);
}
});
},
removeEmotion: (index) => {
set((state) => {
if (!state.activeRecord?.emotions) return;
state.activeRecord.emotions = state.activeRecord.emotions.filter((_, i) => i !== index);
});
},
completeCognitiveRestructuring: (restructuring) => {
set((state) => {
if (!state.activeRecord) return;
state.activeRecord.cognitiveRestructuring = restructuring;
// 记录干预后的情绪变化
if (state.activeRecord.emotions && restructuring) {
state.activeRecord.outcome = {
moodBefore: state.activeRecord.emotions.reduce((sum, e) => sum + e.intensity, 0) / state.activeRecord.emotions.length,
moodAfter: state.activeRecord.emotions.reduce((sum, e) => sum + (e.beforeIntervention || e.intensity), 0) / state.activeRecord.emotions.length,
};
}
});
},
setFilter: (filter) => {
set((state) => {
state.filter = { ...state.filter, ...filter };
});
},
})),
{
name: 'thought-record-storage',
partialize: (state) => ({
records: state.records,
filter: state.filter,
}),
}
)
);
Code collapsed
创建情绪追踪组件
code
// components/EmotionTracker.tsx
'use client';
import React, { useState } from 'react';
import { useThoughtRecordStore } from '@/store/ThoughtRecordStore';
import { EmotionType, EmotionEntry } from '@/types/cbt';
const EMOTION_CONFIG: Record<EmotionType, { label: string; color: string; icon: string }> = {
anxiety: { label: '焦虑', color: 'bg-yellow-100 text-yellow-800', icon: '😰' },
sadness: { label: '悲伤', color: 'bg-blue-100 text-blue-800', icon: '😢' },
anger: { label: '愤怒', color: 'bg-red-100 text-red-800', icon: '😠' },
shame: { label: '羞耻', color: 'bg-purple-100 text-purple-800', icon: '😳' },
guilt: { label: '内疚', color: 'bg-green-100 text-green-800', icon: '😔' },
joy: { label: '快乐', color: 'bg-pink-100 text-pink-800', icon: '😊' },
calm: { label: '平静', color: 'bg-teal-100 text-teal-800', icon: '😌' },
other: { label: '其他', color: 'bg-gray-100 text-gray-800', icon: '🤔' },
};
export function EmotionTracker() {
const { activeRecord, addEmotion, updateEmotion, removeEmotion } = useThoughtRecordStore();
const emotions = activeRecord?.emotions || [];
const [selectedEmotion, setSelectedEmotion] = useState<EmotionType | null>(null);
const [intensity, setIntensity] = useState(50);
const handleAddEmotion = () => {
if (!selectedEmotion) return;
const emotionEntry: EmotionEntry = {
emotion: selectedEmotion,
intensity,
beforeIntervention: intensity,
};
addEmotion(emotionEntry);
setSelectedEmotion(null);
setIntensity(50);
};
return (
<div className: "space-y-6">
{/* 情绪选择器 */}
<div>
<h3 className: "text-lg font-semibold text-gray-900 mb-3">当前情绪</h3>
<div className: "grid grid-cols-4 gap-3">
{(Object.keys(EMOTION_CONFIG) as EmotionType[]).map((emotion) => {
const config = EMOTION_CONFIG[emotion];
const isSelected = selectedEmotion === emotion;
const isAdded = emotions.some((e) => e.emotion === emotion);
return (
<button
key={emotion}
onClick={() => setSelectedEmotion(emotion)}
className={`
relative p-3 rounded-lg border-2 transition-all
${isSelected ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300'}
${isAdded ? 'ring-2 ring-green-500' : ''}
`}
>
<div className: "text-3xl mb-1">{config.icon}</div>
<div className: "text-sm font-medium text-gray-900">{config.label}</div>
{isAdded && (
<div className: "absolute top-1 right-1 w-4 h-4 bg-green-500 rounded-full flex items-center justify-center">
<svg className: "w-3 h-3 text-white" fill: "none" viewBox: "0 0 24 24" stroke: "currentColor">
<path strokeLinecap: "round" strokeLinejoin: "round" strokeWidth={3} d: "M5 13l4 4L19 7" />
</svg>
</div>
)}
</button>
);
})}
</div>
</div>
{/* 强度滑块 */}
{selectedEmotion && (
<div className: "bg-gray-50 rounded-lg p-4">
<div className: "flex items-center justify-between mb-2">
<span className: "text-sm font-medium text-gray-700">
{EMOTION_CONFIG[selectedEmotion].label} 强度
</span>
<span className: "text-2xl font-bold text-gray-900">{intensity}%</span>
</div>
<input
type: "range"
min: "0"
max: "100"
value={intensity}
onChange={(e) => setIntensity(parseInt(e.target.value))}
className: "w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
/>
<div className: "flex justify-between text-xs text-gray-500 mt-1">
<span>轻微</span>
<span>中等</span>
<span>强烈</span>
</div>
<button
onClick={handleAddEmotion}
className: "w-full mt-4 bg-blue-600 text-white py-2 rounded-lg font-medium hover:bg-blue-700 transition"
>
添加情绪记录
</button>
</div>
)}
{/* 已添加情绪列表 */}
{emotions.length > 0 && (
<div>
<h3 className: "text-lg font-semibold text-gray-900 mb-3">已记录情绪</h3>
<div className: "space-y-2">
{emotions.map((entry, index) => {
const config = EMOTION_CONFIG[entry.emotion];
return (
<div
key={index}
className={`flex items-center justify-between p-3 rounded-lg ${config.color}`}
>
<div className: "flex items-center gap-3">
<span className: "text-2xl">{config.icon}</span>
<div>
<div className: "font-medium">{config.label}</div>
<div className: "text-sm opacity-75">强度: {entry.intensity}%</div>
</div>
</div>
<button
onClick={() => removeEmotion(index)}
className: "text-red-600 hover:text-red-800 p-1"
>
<svg className: "w-5 h-5" fill: "none" viewBox: "0 0 24 24" stroke: "currentColor">
<path strokeLinecap: "round" strokeLinejoin: "round" strokeWidth={2} d: "M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
);
})}
</div>
</div>
)}
</div>
);
}
Code collapsed
创建自动思维记录组件
code
// components/AutomaticThoughtRecorder.tsx
'use client';
import React, { useState } from 'react';
import { useThoughtRecordStore } from '@/store/ThoughtRecordStore';
import { AutomaticThought } from '@/types/cbt';
export function AutomaticThoughtRecorder() {
const {
activeRecord,
addAutomaticThought,
updateAutomaticThought,
removeAutomaticThought,
} = useThoughtRecordStore();
const thoughts = activeRecord?.automaticThoughts || [];
const [newThought, setNewThought] = useState('');
const [beliefDegree, setBeliefDegree] = useState(50);
const handleAddThought = () => {
if (!newThought.trim()) return;
addAutomaticThought({
thought: newThought.trim(),
beliefDegree,
type: 'hot',
});
setNewThought('');
setBeliefDegree(50);
};
return (
<div className: "space-y-6">
{/* 提示信息 */}
<div className: "bg-blue-50 border-l-4 border-blue-400 p-4">
<div className: "flex">
<div className: "flex-shrink-0">
<svg className: "h-5 w-5 text-blue-400" viewBox: "0 0 20 20" fill: "currentColor">
<path fillRule: "evenodd" d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule: "evenodd" />
</svg>
</div>
<div className: "ml-3">
<h3 className: "text-sm font-medium text-blue-800">什么是自动思维?</h3>
<div className: "mt-2 text-sm text-blue-700">
自动思维是在特定情境下迅速出现的想法或图像。它们可能是积极的、消极的或中性的。
识别这些想法是改变它们的第一步。
</div>
</div>
</div>
</div>
{/* 输入区域 */}
<div className: "space-y-4">
<div>
<label className: "block text-sm font-medium text-gray-700 mb-2">
我当时在想什么?
</label>
<textarea
value={newThought}
onChange={(e) => setNewThought(e.target.value)}
placeholder: "例如:我一定会搞砸这个演讲..."
rows={3}
className: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className: "block text-sm font-medium text-gray-700 mb-2">
我对这个想法的相信程度:{beliefDegree}%
</label>
<input
type: "range"
min: "0"
max: "100"
value={beliefDegree}
onChange={(e) => setBeliefDegree(parseInt(e.target.value))}
className: "w-full"
/>
<div className: "flex justify-between text-xs text-gray-500 mt-1">
<span>完全不相信</span>
<span>完全相信</span>
</div>
</div>
<button
onClick={handleAddThought}
disabled={!newThought.trim()}
className: "w-full bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
添加这个想法
</button>
</div>
{/* 已记录的思维列表 */}
{thoughts.length > 0 && (
<div className: "space-y-3">
<h3 className: "text-lg font-semibold text-gray-900">已记录的想法</h3>
{thoughts.map((thought) => (
<ThoughtItem
key={thought.id}
thought={thought}
onUpdate={(updates) => updateAutomaticThought(thought.id, updates)}
onRemove={() => removeAutomaticThought(thought.id)}
/>
))}
</div>
)}
</div>
);
}
interface ThoughtItemProps {
thought: AutomaticThought;
onUpdate: (updates: Partial<AutomaticThought>) => void;
onRemove: () => void;
}
function ThoughtItem({ thought, onUpdate, onRemove }: ThoughtItemProps) {
const [isEditing, setIsEditing] = useState(false);
return (
<div className: "bg-white border border-gray-200 rounded-lg p-4">
{isEditing ? (
<div className: "space-y-3">
<textarea
value={thought.thought}
onChange={(e) => onUpdate({ thought: e.target.value })}
rows={2}
className: "w-full px-3 py-2 border border-gray-300 rounded-lg"
/>
<div>
<label className: "text-sm text-gray-600">相信程度</label>
<input
type: "range"
min: "0"
max: "100"
value={thought.beliefDegree}
onChange={(e) => onUpdate({ beliefDegree: parseInt(e.target.value) })}
className: "w-full"
/>
</div>
<div className: "flex gap-2">
<button
onClick={() => setIsEditing(false)}
className: "px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700"
>
保存
</button>
<button
onClick={() => setIsEditing(false)}
className: "px-4 py-2 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300"
>
取消
</button>
</div>
</div>
) : (
<div className: "flex justify-between items-start">
<div className: "flex-1">
<p className: "text-gray-900">{thought.thought}</p>
<div className: "mt-2 flex items-center gap-2">
<span className: "text-sm text-gray-500">相信程度: {thought.beliefDegree}%</span>
{thought.type && (
<span className: "px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded">
{thought.type === 'hot' ? '热思维' : thought.type === 'intermediate' ? '中间信念' : '核心信念'}
</span>
)}
</div>
</div>
<div className: "flex gap-2 ml-4">
<button
onClick={() => setIsEditing(true)}
className: "text-gray-400 hover:text-gray-600"
>
<svg className: "w-5 h-5" fill: "none" viewBox: "0 0 24 24" stroke: "currentColor">
<path strokeLinecap: "round" strokeLinejoin: "round" strokeWidth={2} d: "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
onClick={onRemove}
className: "text-red-400 hover:text-red-600"
>
<svg className: "w-5 h-5" fill: "none" viewBox: "0 0 24 24" stroke: "currentColor">
<path strokeLinecap: "round" strokeLinejoin: "round" strokeWidth={2} d: "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
)}
</div>
);
}
Code collapsed
创建认知重构组件
code
// components/CognitiveRestructuring.tsx
'use client';
import React, { useState } from 'react';
import { useThoughtRecordStore } from '@/store/ThoughtRecordStore';
export function CognitiveRestructuring() {
const { activeRecord, completeCognitiveRestructuring } = useThoughtRecordStore();
const [alternativeThoughts, setAlternativeThoughts] = useState<string[]>(['', '']);
const [evidenceFor, setEvidenceFor] = useState<string[]>(['', '']);
const [evidenceAgainst, setEvidenceAgainst] = useState<string[]>(['', '']);
const [balancedThought, setBalancedThought] = useState('');
const [beliefInBalanced, setBeliefInBalanced] = useState(50);
const handleComplete = () => {
completeCognitiveRestructuring({
alternativeThoughts: alternativeThoughts.filter((t) => t.trim()),
evidenceFor: evidenceFor.filter((e) => e.trim()),
evidenceAgainst: evidenceAgainst.filter((e) => e.trim()),
balancedThought: balancedThought.trim(),
beliefInBalancedThought: beliefInBalanced,
});
};
return (
<div className: "space-y-8">
<div>
<h2 className: "text-2xl font-bold text-gray-900 mb-4">认知重构</h2>
<p className: "text-gray-600">
通过寻找替代想法和证据来重新评估你的自动思维,形成更平衡的观点。
</p>
</div>
{/* 替代思维 */}
<section>
<h3 className: "text-lg font-semibold text-gray-900 mb-3">
1. 有什么其他方式可以看待这个情况?
</h3>
<p className: "text-sm text-gray-500 mb-4">
想象你的朋友遇到了同样的情况,你会对他们说什么?
</p>
<div className: "space-y-3">
{alternativeThoughts.map((thought, index) => (
<textarea
key={index}
value={thought}
onChange={(e) => {
const newThoughts = [...alternativeThoughts];
newThoughts[index] = e.target.value;
setAlternativeThoughts(newThoughts);
}}
placeholder={`替代想法 ${index + 1}`}
rows={2}
className: "w-full px-4 py-2 border border-gray-300 rounded-lg"
/>
))}
<button
onClick={() => setAlternativeThoughts([...alternativeThoughts, ''])}
className: "text-blue-600 hover:text-blue-800 text-sm font-medium"
>
+ 添加更多替代想法
</button>
</div>
</section>
{/* 支持证据 */}
<section>
<h3 className: "text-lg font-semibold text-gray-900 mb-3">
2. 有什么证据支持这个自动思维?
</h3>
<p className: "text-sm text-gray-500 mb-4">
客观地考虑事实,而不是感受。
</p>
<div className: "space-y-3">
{evidenceFor.map((evidence, index) => (
<textarea
key={index}
value={evidence}
onChange={(e) => {
const newEvidence = [...evidenceFor];
newEvidence[index] = e.target.value;
setEvidenceFor(newEvidence);
}}
placeholder={`支持证据 ${index + 1}`}
rows={2}
className: "w-full px-4 py-2 border border-gray-300 rounded-lg"
/>
))}
<button
onClick={() => setEvidenceFor([...evidenceFor, ''])}
className: "text-blue-600 hover:text-blue-800 text-sm font-medium"
>
+ 添加更多证据
</button>
</div>
</section>
{/* 反对证据 */}
<section>
<h3 className: "text-lg font-semibold text-gray-900 mb-3">
3. 有什么证据反对这个自动思维?
</h3>
<p className: "text-sm text-gray-500 mb-4">
考虑过去的经历、其他观点或客观事实。
</p>
<div className: "space-y-3">
{evidenceAgainst.map((evidence, index) => (
<textarea
key={index}
value={evidence}
onChange={(e) => {
const newEvidence = [...evidenceAgainst];
newEvidence[index] = e.target.value;
setEvidenceAgainst(newEvidence);
}}
placeholder={`反对证据 ${index + 1}`}
rows={2}
className: "w-full px-4 py-2 border border-gray-300 rounded-lg"
/>
))}
<button
onClick={() => setEvidenceAgainst([...evidenceAgainst, ''])}
className: "text-blue-600 hover:text-blue-800 text-sm font-medium"
>
+ 添加更多证据
</button>
</div>
</section>
{/* 平衡想法 */}
<section>
<h3 className: "text-lg font-semibold text-gray-900 mb-3">
4. 基于以上分析,形成更平衡的想法
</h3>
<p className: "text-sm text-gray-500 mb-4">
综合所有证据,创建一个更准确、平衡的想法。
</p>
<textarea
value={balancedThought}
onChange={(e) => setBalancedThought(e.target.value)}
placeholder: "综合考虑所有因素后,我认为..."
rows={4}
className: "w-full px-4 py-2 border border-gray-300 rounded-lg"
/>
</section>
{/* 相信程度 */}
<section>
<h3 className: "text-lg font-semibold text-gray-900 mb-3">
5. 你对这个平衡想法的相信程度是多少?
</h3>
<div className: "bg-gray-50 rounded-lg p-4">
<input
type: "range"
min: "0"
max: "100"
value={beliefInBalanced}
onChange={(e) => setBeliefInBalanced(parseInt(e.target.value))}
className: "w-full"
/>
<div className: "text-center mt-2">
<span className: "text-3xl font-bold text-blue-600">{beliefInBalanced}%</span>
</div>
</div>
</section>
{/* 完成按钮 */}
<button
onClick={handleComplete}
disabled={!balancedThought.trim()}
className: "w-full bg-green-600 text-white py-4 rounded-lg font-semibold text-lg hover:bg-green-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
完成认知重构
</button>
</div>
);
}
Code collapsed
创建进度追踪组件
code
// components/ProgressDashboard.tsx
'use client';
import React, { useMemo } from 'react';
import { useThoughtRecordStore } from '@/store/ThoughtRecordStore';
import { format, subDays, startOfDay } from 'date-fns';
import { zhCN } from 'date-fns/locale';
export function ProgressDashboard() {
const { records } = useThoughtRecordStore();
// 计算过去7天的数据
const weeklyData = useMemo(() => {
const today = startOfDay(new Date());
const weekData = [];
for (let i = 6; i >= 0; i--) {
const date = subDays(today, i);
const dayRecords = records.filter(
(r) => startOfDay(new Date(r.createdAt)).getTime() === date.getTime()
);
weekData.push({
date,
formatted: format(date, 'M月d日 EEEE', { locale: zhCN }),
count: dayRecords.length,
avgMoodImprovement: dayRecords.length > 0
? dayRecords.reduce((sum, r) => {
const improvement = r.outcome
? r.outcome.moodAfter - r.outcome.moodBefore
: 0;
return sum + improvement;
}, 0) / dayRecords.length
: 0,
});
}
return weekData;
}, [records]);
// 分析最常见的认知扭曲
const commonDistortions = useMemo(() => {
const distortionCounts = new Map<string, number>();
records.forEach((record) => {
record.cognitiveDistortions?.forEach((distortion) => {
distortionCounts.set(
distortion,
(distortionCounts.get(distortion) || 0) + 1
);
});
});
return Array.from(distortionCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
}, [records]);
return (
<div className: "space-y-8">
{/* 每周活动概览 */}
<section>
<h2 className: "text-2xl font-bold text-gray-900 mb-4">本周活动</h2>
<div className: "bg-white rounded-xl shadow-sm p-6">
<div className: "grid grid-cols-7 gap-2">
{weeklyData.map((day) => (
<div key={day.formatted} className: "text-center">
<div className: "text-xs text-gray-500 mb-2">{day.formatted}</div>
<div
className={`
h-20 rounded-lg flex items-end justify-center pb-2
${day.count > 0 ? 'bg-blue-100' : 'bg-gray-100'}
`}
>
{day.count > 0 && (
<div
className: "bg-blue-600 w-full rounded-t"
style={{ height: `${Math.min(day.count * 20, 100)}%` }}
/>
)}
</div>
<div className: "text-sm font-medium mt-2">{day.count}</div>
</div>
))}
</div>
</div>
</section>
{/* 认知扭曲分析 */}
{commonDistortions.length > 0 && (
<section>
<h2 className: "text-2xl font-bold text-gray-900 mb-4">
最常见的认知扭曲模式
</h2>
<div className: "grid grid-cols-1 md:grid-cols-2 gap-4">
{commonDistortions.map(([distortion, count]) => (
<div key={distortion} className: "bg-white rounded-lg shadow-sm p-4 flex items-center">
<div className: "flex-1">
<div className: "font-medium text-gray-900">
{getDistortionLabel(distortion)}
</div>
<div className: "text-sm text-gray-500">出现 {count} 次</div>
</div>
<div className: "w-16 h-16 relative">
<svg className: "transform -rotate-90 w-16 h-16">
<circle
cx: "32"
cy: "32"
r: "28"
stroke: "#e5e7eb"
strokeWidth: "4"
fill: "none"
/>
<circle
cx: "32"
cy: "32"
r: "28"
stroke: "#3b82f6"
strokeWidth: "4"
fill: "none"
strokeDasharray={`${(count / records.length) * 175.93} 175.93`}
/>
</svg>
<div className: "absolute inset-0 flex items-center justify-center text-sm font-medium">
{Math.round((count / records.length) * 100)}%
</div>
</div>
</div>
))}
</div>
</section>
)}
{/* 总体进展 */}
<section>
<h2 className: "text-2xl font-bold text-gray-900 mb-4">总体进展</h2>
<div className: "grid grid-cols-1 md:grid-cols-3 gap-6">
<MetricCard
title: "完成记录"
value={records.length}
unit: "条"
trend: "positive"
/>
<MetricCard
title: "平均改善"
value={weeklyData.reduce((sum, d) => sum + d.avgMoodImprovement, 0) / 7}
unit: "分"
trend: "neutral"
/>
<MetricCard
title: "活跃天数"
value={weeklyData.filter((d) => d.count > 0).length}
unit: "天"
trend: "positive"
/>
</div>
</section>
</div>
);
}
function getDistortionLabel(distortion: string): string {
const labels: Record<string, string> = {
all_or_nothing: '非黑即白思维',
overgeneralization: '过度概括',
mental_filter: '心理过滤',
disqualifying_positive: '否定积极事物',
jumping_conclusions: '妄下结论',
magnification: '放大或缩小',
emotional_reasoning: '情绪推理',
should_statements: '应该陈述',
labeling: '标签化',
personalization: '个人化',
catastrophizing: '灾难化',
};
return labels[distortion] || distortion;
}
interface MetricCardProps {
title: string;
value: number;
unit: string;
trend: 'positive' | 'neutral' | 'negative';
}
function MetricCard({ title, value, unit, trend }: MetricCardProps) {
const trendColors = {
positive: 'text-green-600',
neutral: 'text-gray-600',
negative: 'text-red-600',
};
return (
<div className: "bg-white rounded-lg shadow-sm p-6">
<h3 className: "text-sm font-medium text-gray-600 mb-2">{title}</h3>
<div className: "flex items-baseline gap-2">
<span className: "text-3xl font-bold text-gray-900">
{value.toFixed(1)}
</span>
<span className: "text-sm text-gray-500">{unit}</span>
</div>
</div>
);
}
Code collapsed
数据安全与加密
code
// lib/encryption.ts
import CryptoJS from 'crypto-js';
const SECRET_KEY = process.env.NEXT_PUBLIC_ENCRYPTION_KEY || 'default-key';
export function encryptData(data: any): string {
const jsonString = JSON.stringify(data);
return CryptoJS.AES.encrypt(jsonString, SECRET_KEY).toString();
}
export function decryptData<T>(encryptedData: string): T | null {
try {
const bytes = CryptoJS.AES.decrypt(encryptedData, SECRET_KEY);
const jsonString = bytes.toString(CryptoJS.enc.Utf8);
return JSON.parse(jsonString) as T;
} catch (error) {
console.error('Decryption failed:', error);
return null;
}
}
// 持久化存储中间件
export const encryptedPersist = (config: any) => (set: any, get: any, api: any) => {
const storedState = localStorage.getItem(config.name);
if (storedState) {
const decrypted = decryptData<Partial<ReturnType<typeof get>>>(storedState);
if (decrypted) {
set(decrypted);
}
}
api.subscribe((state: any) => {
const partialState = config.partialize(state);
const encrypted = encryptData(partialState);
localStorage.setItem(config.name, encrypted);
});
};
Code collapsed
总结
通过本教程,你学会了如何构建一个专业的 CBT 应用:
- 设计 CBT 核心数据模型
- 使用 Zustand + Immer 管理复杂状态
- 实现思维记录和认知重构功能
- 创建进度追踪和可视化
- 确保敏感数据的安全存储
扩展功能
- 添加行为实验模块
- 实现暴露疗法练习
- 集成正念冥想引导
- 添加与治疗师的安全沟通功能
参考资料
常见问题
Q: 如何处理用户的危机情况?
A: 应实现危机检测机制(如检测到自杀意念),提供紧急资源、危机热线,并在必要时触发自动通知给预设的支持联系人。
Q: 数据如何符合医疗隐私法规(如 HIPAA)?
A: 需要实现端到端加密、安全的云存储、访问审计日志,并确保用户数据不被用于未经授权的用途。