康心伴Logo
康心伴WellAlly
心理健康应用

构建 CBT 认知行为疗法应用:React 状态管理与数据结构设计

深度教程:使用 React 构建专业的认知行为疗法(CBT)应用,重点讲解状态管理和数据结构设计。包含思维记录模块、情绪追踪、自动思维识别和认知重构功能的完整实现。

W
WellAlly 开发团队
2026-03-08
26 分钟阅读

关键要点

  • 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 应用:

  1. 设计 CBT 核心数据模型
  2. 使用 Zustand + Immer 管理复杂状态
  3. 实现思维记录和认知重构功能
  4. 创建进度追踪和可视化
  5. 确保敏感数据的安全存储

扩展功能

  • 添加行为实验模块
  • 实现暴露疗法练习
  • 集成正念冥想引导
  • 添加与治疗师的安全沟通功能

参考资料

常见问题

Q: 如何处理用户的危机情况?

A: 应实现危机检测机制(如检测到自杀意念),提供紧急资源、危机热线,并在必要时触发自动通知给预设的支持联系人。

Q: 数据如何符合医疗隐私法规(如 HIPAA)?

A: 需要实现端到端加密、安全的云存储、访问审计日志,并确保用户数据不被用于未经授权的用途。

相关文章

#

文章标签

react
状态管理
数据结构
cbt
心理健康

觉得这篇文章有帮助?

立即体验康心伴,开始您的健康管理之旅