《PySide6 GUI开发指南:QML核心与实践》 第十篇:综合实战——构建完整的跨平台个人管理应用

前言:从学到用的蜕变

恭喜你!经过前九篇的学习,你已经掌握了QML开发的完整知识体系。现在是时候将这些知识融会贯通,构建一个真正的完整应用了。本篇我们将创建一个个人知识管理应用,它不仅功能完整,而且能体现现代应用开发的最佳实践。

项目预览:智慧笔记 (SmartNotes)

我们将构建一个名为"智慧笔记"的个人知识管理应用,它包含以下核心功能:

  1. 知识卡片:创建、编辑、组织知识卡片

  2. 智能标签:自动分类和组织内容

  3. 知识图谱:可视化知识之间的关系

  4. 搜索与过滤:快速找到所需信息

  5. 数据同步:跨设备同步笔记

  6. 统计面板:了解学习进度和知识结构

第一部分:项目架构设计

1.1 技术栈选择

bash 复制代码
技术栈选择:
├── 前端框架
│   ├── Qt 6.5 + QML
│   ├── Qt Quick Controls 2
│   └── Qt Quick 3D (用于知识图谱)
├── 后端语言
│   ├── Python 3.10 (业务逻辑)
│   └── SQLite (数据库)
├── 开发工具
│   ├── Qt Creator IDE
│   ├── Git 版本控制
│   └── CMake 构建系统
└── 质量保证
    ├── 单元测试 (pytest)
    ├── 集成测试
    └── 性能分析工具

1.2 项目目录结构

bash 复制代码
smartnotes/
├── src/                    # 源代码
│   ├── frontend/          # 前端代码
│   │   ├── qml/          # QML文件
│   │   ├── resources/    # 资源文件
│   │   └── styles/       # 样式文件
│   ├── backend/          # 后端代码
│   │   ├── core/         # 核心逻辑
│   │   ├── database/     # 数据库操作
│   │   ├── services/     # 业务服务
│   │   └── utils/        # 工具函数
│   └── shared/           # 共享代码
│       ├── models/       # 数据模型
│       ├── constants/    # 常量定义
│       └── types/        # 类型定义
├── tests/                # 测试代码
│   ├── unit/            # 单元测试
│   ├── integration/     # 集成测试
│   └── ui/              # UI测试
├── docs/                # 文档
│   ├── api/            # API文档
│   ├── guides/         # 使用指南
│   └── design/         # 设计文档
├── scripts/            # 脚本文件
│   ├── build/         # 构建脚本
│   ├── deploy/        # 部署脚本
│   └── tools/         # 开发工具
└── platform/          # 平台特定配置
    ├── windows/       # Windows配置
    ├── macos/         # macOS配置
    ├── linux/         # Linux配置
    ├── android/       # Android配置
    └── ios/          # iOS配置

第二部分:核心功能实现

2.1 数据模型设计

让我们从最核心的数据模型开始。我们将创建几个简单的数据类来表示知识卡片、标签和关系。

python 复制代码
# src/shared/models/__init__.py
"""
数据模型定义
这个文件定义了应用的核心数据模型
"""

from dataclasses import dataclass, field, asdict
from datetime import datetime
from typing import List, Dict, Any, Optional, Set
from enum import Enum
import uuid


class CardType(Enum):
    """知识卡片类型枚举"""
    NOTE = "note"        # 笔记
    IDEA = "idea"       # 想法
    TASK = "task"       # 任务
    QUESTION = "question"  # 问题
    ANSWER = "answer"   # 答案
    REFERENCE = "reference"  # 参考


class CardStatus(Enum):
    """卡片状态枚举"""
    DRAFT = "draft"      # 草稿
    ACTIVE = "active"    # 活跃
    ARCHIVED = "archived"  # 归档
    DELETED = "deleted"  # 删除


@dataclass
class KnowledgeCard:
    """知识卡片基础模型"""
    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    title: str = ""
    content: str = ""
    type: CardType = CardType.NOTE
    status: CardStatus = CardStatus.DRAFT
    tags: List[str] = field(default_factory=list)
    created_at: datetime = field(default_factory=datetime.now)
    updated_at: datetime = field(default_factory=datetime.now)
    
    # 元数据
    priority: int = 0  # 优先级 0-10
    difficulty: int = 0  # 难度 0-10
    importance: int = 0  # 重要性 0-10
    
    def to_dict(self) -> Dict[str, Any]:
        """转换为字典"""
        data = asdict(self)
        data['type'] = self.type.value
        data['status'] = self.status.value
        data['created_at'] = self.created_at.isoformat()
        data['updated_at'] = self.updated_at.isoformat()
        return data
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'KnowledgeCard':
        """从字典创建实例"""
        data = data.copy()
        data['type'] = CardType(data.get('type', 'note'))
        data['status'] = CardStatus(data.get('status', 'draft'))
        
        # 转换时间字符串
        for time_key in ['created_at', 'updated_at']:
            if time_key in data and isinstance(data[time_key], str):
                data[time_key] = datetime.fromisoformat(data[time_key])
        
        return cls(**data)
    
    def update(self, **kwargs):
        """更新卡片属性"""
        for key, value in kwargs.items():
            if hasattr(self, key):
                setattr(self, key, value)
        self.updated_at = datetime.now()
    
    def get_summary(self, max_length: int = 100) -> str:
        """获取内容摘要"""
        if not self.content:
            return ""
        if len(self.content) <= max_length:
            return self.content
        return self.content[:max_length] + "..."


@dataclass
class Tag:
    """标签模型"""
    id: str
    name: str
    color: str = "#2196F3"  # 默认蓝色
    description: str = ""
    created_at: datetime = field(default_factory=datetime.now)
    usage_count: int = 0
    
    def to_dict(self) -> Dict[str, Any]:
        data = asdict(self)
        data['created_at'] = self.created_at.isoformat()
        return data


@dataclass
class CardRelationship:
    """卡片关系模型"""
    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    source_card_id: str = ""
    target_card_id: str = ""
    relationship_type: str = "related"  # related, depends_on, answers, etc.
    strength: float = 1.0  # 关系强度 0.0-1.0
    created_at: datetime = field(default_factory=datetime.now)


@dataclass
class SmartTag:
    """智能标签(自动分类)"""
    id: str
    name: str
    keywords: List[str]  # 关键词列表
    description: str = ""
    auto_apply: bool = True  # 是否自动应用
    confidence_threshold: float = 0.7  # 置信度阈值
    
    def should_apply(self, card: KnowledgeCard) -> bool:
        """判断是否应该应用此标签"""
        if not self.auto_apply:
            return False
        
        # 简单的关键词匹配算法
        text_to_check = f"{card.title} {card.content}".lower()
        matched_keywords = 0
        
        for keyword in self.keywords:
            if keyword.lower() in text_to_check:
                matched_keywords += 1
        
        confidence = matched_keywords / len(self.keywords) if self.keywords else 0
        return confidence >= self.confidence_threshold

数据关系模型:

2.2 数据库服务

现在让我们实现数据库服务,使用SQLite作为本地存储:

python 复制代码
# src/backend/database/database_service.py
import sqlite3
from typing import List, Dict, Any, Optional, Tuple
from datetime import datetime
import json
from pathlib import Path

from ..shared.models import (
    KnowledgeCard, CardType, CardStatus,
    Tag, CardRelationship, SmartTag
)


class DatabaseService:
    """数据库服务"""
    
    def __init__(self, db_path: str = "smartnotes.db"):
        """
        初始化数据库服务
        
        Args:
            db_path: 数据库文件路径
        """
        self.db_path = db_path
        self._connection = None
        self._init_database()
    
    def _get_connection(self) -> sqlite3.Connection:
        """获取数据库连接"""
        if self._connection is None:
            self._connection = sqlite3.connect(
                self.db_path,
                detect_types=sqlite3.PARSE_DECLTYPES
            )
            self._connection.row_factory = sqlite3.Row
        return self._connection
    
    def _init_database(self):
        """初始化数据库表"""
        conn = self._get_connection()
        cursor = conn.cursor()
        
        # 创建知识卡片表
        cursor.execute("""
        CREATE TABLE IF NOT EXISTS knowledge_cards (
            id TEXT PRIMARY KEY,
            title TEXT NOT NULL,
            content TEXT NOT NULL,
            type TEXT NOT NULL,
            status TEXT NOT NULL,
            tags_json TEXT NOT NULL,
            created_at TIMESTAMP NOT NULL,
            updated_at TIMESTAMP NOT NULL,
            priority INTEGER DEFAULT 0,
            difficulty INTEGER DEFAULT 0,
            importance INTEGER DEFAULT 0
        )
        """)
        
        # 创建标签表
        cursor.execute("""
        CREATE TABLE IF NOT EXISTS tags (
            id TEXT PRIMARY KEY,
            name TEXT UNIQUE NOT NULL,
            color TEXT NOT NULL,
            description TEXT,
            created_at TIMESTAMP NOT NULL,
            usage_count INTEGER DEFAULT 0
        )
        """)
        
        # 创建卡片关系表
        cursor.execute("""
        CREATE TABLE IF NOT EXISTS card_relationships (
            id TEXT PRIMARY KEY,
            source_card_id TEXT NOT NULL,
            target_card_id TEXT NOT NULL,
            relationship_type TEXT NOT NULL,
            strength REAL DEFAULT 1.0,
            created_at TIMESTAMP NOT NULL,
            FOREIGN KEY (source_card_id) REFERENCES knowledge_cards (id) ON DELETE CASCADE,
            FOREIGN KEY (target_card_id) REFERENCES knowledge_cards (id) ON DELETE CASCADE
        )
        """)
        
        # 创建智能标签表
        cursor.execute("""
        CREATE TABLE IF NOT EXISTS smart_tags (
            id TEXT PRIMARY KEY,
            name TEXT UNIQUE NOT NULL,
            keywords_json TEXT NOT NULL,
            description TEXT,
            auto_apply BOOLEAN DEFAULT 1,
            confidence_threshold REAL DEFAULT 0.7
        )
        """)
        
        # 创建索引
        cursor.execute("CREATE INDEX IF NOT EXISTS idx_cards_created ON knowledge_cards(created_at)")
        cursor.execute("CREATE INDEX IF NOT EXISTS idx_cards_status ON knowledge_cards(status)")
        cursor.execute("CREATE INDEX IF NOT EXISTS idx_relationships_source ON card_relationships(source_card_id)")
        cursor.execute("CREATE INDEX IF NOT EXISTS idx_relationships_target ON card_relationships(target_card_id)")
        
        conn.commit()
        
        # 初始化一些默认智能标签
        self._init_default_smart_tags()
    
    def _init_default_smart_tags(self):
        """初始化默认智能标签"""
        default_tags = [
            SmartTag(
                id="tag_question",
                name="问题",
                keywords=["?", "为什么", "如何", "怎么", "什么"],
                description="包含问题的卡片",
                auto_apply=True
            ),
            SmartTag(
                id="tag_important",
                name="重要",
                keywords=["重要", "关键", "核心", "必须", "必要"],
                description="重要的内容",
                auto_apply=True
            ),
            SmartTag(
                id="tag_todo",
                name="待办",
                keywords=["TODO", "待办", "要做", "需要做", "计划"],
                description="待办事项",
                auto_apply=True
            ),
            SmartTag(
                id="tag_reference",
                name="参考",
                keywords=["参考", "来源", "引用", "参考文献", "链接"],
                description="参考资料",
                auto_apply=True
            )
        ]
        
        for tag in default_tags:
            self.save_smart_tag(tag)
    
    # === 知识卡片操作 ===
    
    def save_card(self, card: KnowledgeCard) -> str:
        """保存知识卡片"""
        conn = self._get_connection()
        cursor = conn.cursor()
        
        tags_json = json.dumps(card.tags)
        
        cursor.execute("""
        INSERT OR REPLACE INTO knowledge_cards 
        (id, title, content, type, status, tags_json, created_at, updated_at, 
         priority, difficulty, importance)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        """, (
            card.id, card.title, card.content, card.type.value, card.status.value,
            tags_json, card.created_at, card.updated_at,
            card.priority, card.difficulty, card.importance
        ))
        
        conn.commit()
        
        # 更新标签使用计数
        for tag in card.tags:
            self._increment_tag_usage(tag)
        
        return card.id
    
    def get_card(self, card_id: str) -> Optional[KnowledgeCard]:
        """获取知识卡片"""
        conn = self._get_connection()
        cursor = conn.cursor()
        
        cursor.execute("SELECT * FROM knowledge_cards WHERE id = ?", (card_id,))
        row = cursor.fetchone()
        
        if row is None:
            return None
        
        return self._row_to_card(row)
    
    def get_all_cards(
        self, 
        status_filter: Optional[CardStatus] = None,
        type_filter: Optional[CardType] = None
    ) -> List[KnowledgeCard]:
        """获取所有知识卡片"""
        conn = self._get_connection()
        cursor = conn.cursor()
        
        query = "SELECT * FROM knowledge_cards WHERE 1=1"
        params = []
        
        if status_filter:
            query += " AND status = ?"
            params.append(status_filter.value)
        
        if type_filter:
            query += " AND type = ?"
            params.append(type_filter.value)
        
        query += " ORDER BY updated_at DESC"
        
        cursor.execute(query, params)
        rows = cursor.fetchall()
        
        return [self._row_to_card(row) for row in rows]
    
    def search_cards(
        self, 
        query_text: str,
        limit: int = 50
    ) -> List[KnowledgeCard]:
        """搜索知识卡片"""
        conn = self._get_connection()
        cursor = conn.cursor()
        
        search_term = f"%{query_text}%"
        
        cursor.execute("""
        SELECT * FROM knowledge_cards 
        WHERE title LIKE ? OR content LIKE ?
        ORDER BY 
            CASE 
                WHEN title LIKE ? THEN 1
                ELSE 2
            END,
            updated_at DESC
        LIMIT ?
        """, (search_term, search_term, search_term, limit))
        
        rows = cursor.fetchall()
        return [self._row_to_card(row) for row in rows]
    
    def delete_card(self, card_id: str) -> bool:
        """删除知识卡片"""
        conn = self._get_connection()
        cursor = conn.cursor()
        
        # 先获取卡片的标签,用于更新使用计数
        cursor.execute("SELECT tags_json FROM knowledge_cards WHERE id = ?", (card_id,))
        row = cursor.fetchone()
        if row:
            tags = json.loads(row['tags_json'])
            
            # 删除卡片
            cursor.execute("DELETE FROM knowledge_cards WHERE id = ?", (card_id,))
            conn.commit()
            
            # 更新标签使用计数
            for tag in tags:
                self._decrement_tag_usage(tag)
            
            return True
        
        return False
    
    def _row_to_card(self, row) -> KnowledgeCard:
        """将数据库行转换为KnowledgeCard对象"""
        tags = json.loads(row['tags_json']) if row['tags_json'] else []
        
        return KnowledgeCard(
            id=row['id'],
            title=row['title'],
            content=row['content'],
            type=CardType(row['type']),
            status=CardStatus(row['status']),
            tags=tags,
            created_at=row['created_at'],
            updated_at=row['updated_at'],
            priority=row['priority'],
            difficulty=row['difficulty'],
            importance=row['importance']
        )
    
    # === 标签操作 ===
    
    def save_tag(self, tag: Tag) -> str:
        """保存标签"""
        conn = self._get_connection()
        cursor = conn.cursor()
        
        cursor.execute("""
        INSERT OR REPLACE INTO tags 
        (id, name, color, description, created_at, usage_count)
        VALUES (?, ?, ?, ?, ?, ?)
        """, (
            tag.id, tag.name, tag.color, tag.description,
            tag.created_at, tag.usage_count
        ))
        
        conn.commit()
        return tag.id
    
    def get_all_tags(self) -> List[Tag]:
        """获取所有标签"""
        conn = self._get_connection()
        cursor = conn.cursor()
        
        cursor.execute("SELECT * FROM tags ORDER BY usage_count DESC")
        rows = cursor.fetchall()
        
        return [
            Tag(
                id=row['id'],
                name=row['name'],
                color=row['color'],
                description=row['description'],
                created_at=row['created_at'],
                usage_count=row['usage_count']
            )
            for row in rows
        ]
    
    def _increment_tag_usage(self, tag_name: str):
        """增加标签使用计数"""
        conn = self._get_connection()
        cursor = conn.cursor()
        
        cursor.execute("""
        UPDATE tags 
        SET usage_count = usage_count + 1 
        WHERE name = ?
        """, (tag_name,))
        
        conn.commit()
    
    def _decrement_tag_usage(self, tag_name: str):
        """减少标签使用计数"""
        conn = self._get_connection()
        cursor = conn.cursor()
        
        cursor.execute("""
        UPDATE tags 
        SET usage_count = GREATEST(usage_count - 1, 0)
        WHERE name = ?
        """, (tag_name,))
        
        conn.commit()
    
    # === 智能标签操作 ===
    
    def save_smart_tag(self, smart_tag: SmartTag) -> str:
        """保存智能标签"""
        conn = self._get_connection()
        cursor = conn.cursor()
        
        keywords_json = json.dumps(smart_tag.keywords)
        
        cursor.execute("""
        INSERT OR REPLACE INTO smart_tags 
        (id, name, keywords_json, description, auto_apply, confidence_threshold)
        VALUES (?, ?, ?, ?, ?, ?)
        """, (
            smart_tag.id, smart_tag.name, keywords_json,
            smart_tag.description, smart_tag.auto_apply,
            smart_tag.confidence_threshold
        ))
        
        conn.commit()
        return smart_tag.id
    
    def get_smart_tags(self) -> List[SmartTag]:
        """获取所有智能标签"""
        conn = self._get_connection()
        cursor = conn.cursor()
        
        cursor.execute("SELECT * FROM smart_tags")
        rows = cursor.fetchall()
        
        smart_tags = []
        for row in rows:
            keywords = json.loads(row['keywords_json'])
            smart_tags.append(
                SmartTag(
                    id=row['id'],
                    name=row['name'],
                    keywords=keywords,
                    description=row['description'],
                    auto_apply=bool(row['auto_apply']),
                    confidence_threshold=row['confidence_threshold']
                )
            )
        
        return smart_tags
    
    def apply_smart_tags(self, card: KnowledgeCard) -> KnowledgeCard:
        """自动应用智能标签"""
        smart_tags = self.get_smart_tags()
        
        for smart_tag in smart_tags:
            if smart_tag.should_apply(card) and smart_tag.name not in card.tags:
                card.tags.append(smart_tag.name)
        
        return card
    
    # === 统计信息 ===
    
    def get_statistics(self) -> Dict[str, Any]:
        """获取统计信息"""
        conn = self._get_connection()
        cursor = conn.cursor()
        
        stats = {}
        
        # 卡片总数
        cursor.execute("SELECT COUNT(*) as count FROM knowledge_cards WHERE status != 'deleted'")
        stats['total_cards'] = cursor.fetchone()['count']
        
        # 按类型统计
        cursor.execute("""
        SELECT type, COUNT(*) as count 
        FROM knowledge_cards 
        WHERE status != 'deleted'
        GROUP BY type
        """)
        stats['cards_by_type'] = {row['type']: row['count'] for row in cursor.fetchall()}
        
        # 按状态统计
        cursor.execute("""
        SELECT status, COUNT(*) as count 
        FROM knowledge_cards 
        GROUP BY status
        """)
        stats['cards_by_status'] = {row['status']: row['count'] for row in cursor.fetchall()}
        
        # 最活跃的标签
        cursor.execute("""
        SELECT name, usage_count 
        FROM tags 
        ORDER BY usage_count DESC 
        LIMIT 10
        """)
        stats['top_tags'] = [
            {'name': row['name'], 'count': row['usage_count']}
            for row in cursor.fetchall()
        ]
        
        # 最近更新
        cursor.execute("""
        SELECT COUNT(*) as count 
        FROM knowledge_cards 
        WHERE updated_at > datetime('now', '-7 days')
        """)
        stats['recently_updated'] = cursor.fetchone()['count']
        
        return stats
    
    def close(self):
        """关闭数据库连接"""
        if self._connection:
            self._connection.close()
            self._connection = None

数据库服务架构:

2.3 核心业务逻辑

现在让我们实现应用的核心业务逻辑:

python 复制代码
# src/backend/services/knowledge_service.py
"""
知识管理服务
处理所有与知识卡片相关的业务逻辑
"""

from typing import List, Dict, Any, Optional, Tuple
from datetime import datetime, timedelta
import re
from collections import Counter

from ..shared.models import (
    KnowledgeCard, CardType, CardStatus,
    Tag, CardRelationship, SmartTag
)
from ..database.database_service import DatabaseService


class KnowledgeService:
    """知识管理服务"""
    
    def __init__(self, db_service: DatabaseService):
        self.db = db_service
        
    # === 卡片管理 ===
    
    def create_card(
        self,
        title: str,
        content: str,
        card_type: CardType = CardType.NOTE,
        tags: Optional[List[str]] = None
    ) -> KnowledgeCard:
        """创建新卡片"""
        card = KnowledgeCard(
            title=title,
            content=content,
            type=card_type,
            tags=tags or []
        )
        
        # 应用智能标签
        card = self.db.apply_smart_tags(card)
        
        # 保存到数据库
        self.db.save_card(card)
        
        return card
    
    def update_card(
        self,
        card_id: str,
        **updates
    ) -> Optional[KnowledgeCard]:
        """更新卡片"""
        card = self.db.get_card(card_id)
        if not card:
            return None
        
        # 更新属性
        card.update(**updates)
        
        # 重新应用智能标签
        if 'title' in updates or 'content' in updates:
            card = self.db.apply_smart_tags(card)
        
        # 保存更新
        self.db.save_card(card)
        
        return card
    
    def get_card_with_details(self, card_id: str) -> Optional[Dict[str, Any]]:
        """获取卡片详情(包含相关卡片)"""
        card = self.db.get_card(card_id)
        if not card:
            return None
        
        # 获取相关卡片
        related_cards = self.get_related_cards(card_id)
        
        # 获取建议标签
        suggested_tags = self.suggest_tags_for_card(card)
        
        return {
            'card': card,
            'related_cards': related_cards,
            'suggested_tags': suggested_tags,
            'statistics': self.get_card_statistics(card)
        }
    
    def search_cards_advanced(
        self,
        query: str,
        filters: Optional[Dict[str, Any]] = None
    ) -> List[KnowledgeCard]:
        """高级搜索"""
        filters = filters or {}
        
        # 如果是简单搜索,使用数据库搜索
        if len(query.split()) <= 2:
            return self.db.search_cards(query)
        
        # 高级搜索逻辑
        cards = self.db.get_all_cards()
        
        # 应用过滤器
        if filters.get('type'):
            cards = [c for c in cards if c.type.value == filters['type']]
        
        if filters.get('status'):
            cards = [c for c in cards if c.status.value == filters['status']]
        
        if filters.get('tags'):
            required_tags = set(filters['tags'])
            cards = [
                c for c in cards 
                if required_tags.issubset(set(c.tags))
            ]
        
        # 搜索评分
        scored_cards = []
        for card in cards:
            score = self._calculate_search_score(card, query)
            if score > 0:
                scored_cards.append((score, card))
        
        # 按分数排序
        scored_cards.sort(key=lambda x: x[0], reverse=True)
        
        return [card for _, card in scored_cards[:50]]
    
    def _calculate_search_score(self, card: KnowledgeCard, query: str) -> float:
        """计算搜索相关性分数"""
        score = 0.0
        
        # 标题匹配(权重最高)
        if query.lower() in card.title.lower():
            score += 5.0
        
        # 内容匹配
        content_matches = len(re.findall(
            re.escape(query.lower()), 
            card.content.lower()
        ))
        score += content_matches * 0.5
        
        # 标签匹配
        for tag in card.tags:
            if query.lower() in tag.lower():
                score += 2.0
        
        # 时间衰减(越新的卡片分数越高)
        days_old = (datetime.now() - card.updated_at).days
        time_factor = max(0, 1.0 - (days_old / 365))  # 一年内衰减
        score *= (0.5 + 0.5 * time_factor)
        
        return score
    
    # === 标签管理 ===
    
    def suggest_tags_for_card(self, card: KnowledgeCard) -> List[str]:
        """为卡片建议标签"""
        suggestions = []
        
        # 从内容中提取关键词
        text = f"{card.title} {card.content}"
        words = re.findall(r'\b\w{3,}\b', text.lower())
        
        # 统计词频
        word_counts = Counter(words)
        
        # 排除常见词
        common_words = {'the', 'and', 'for', 'with', 'this', 'that', 'have', 'from'}
        for word in common_words:
            word_counts.pop(word, None)
        
        # 获取高频词作为建议
        for word, count in word_counts.most_common(5):
            if count >= 2:  # 至少出现两次
                suggestions.append(word)
        
        return suggestions
    
    def merge_tags(self, old_tag: str, new_tag: str) -> bool:
        """合并标签"""
        # 获取所有使用旧标签的卡片
        all_cards = self.db.get_all_cards()
        cards_to_update = [
            card for card in all_cards
            if old_tag in card.tags
        ]
        
        # 更新卡片标签
        for card in cards_to_update:
            card.tags = [
                new_tag if tag == old_tag else tag
                for tag in card.tags
            ]
            self.db.save_card(card)
        
        # 删除旧标签
        # 注意:这里简化处理,实际应该更新标签表
        
        return True
    
    # === 卡片关系 ===
    
    def create_relationship(
        self,
        source_card_id: str,
        target_card_id: str,
        relationship_type: str = "related"
    ) -> Optional[CardRelationship]:
        """创建卡片关系"""
        # 验证卡片存在
        source_card = self.db.get_card(source_card_id)
        target_card = self.db.get_card(target_card_id)
        
        if not source_card or not target_card:
            return None
        
        # 检查关系是否已存在
        # 这里简化处理
        
        relationship = CardRelationship(
            source_card_id=source_card_id,
            target_card_id=target_card_id,
            relationship_type=relationship_type
        )
        
        # 保存关系
        # 这里需要实现保存逻辑
        
        return relationship
    
    def get_related_cards(self, card_id: str) -> List[Dict[str, Any]]:
        """获取相关卡片"""
        # 这里简化实现
        all_cards = self.db.get_all_cards()
        
        # 查找有相同标签的卡片
        current_card = self.db.get_card(card_id)
        if not current_card:
            return []
        
        related = []
        for card in all_cards:
            if card.id == card_id:
                continue
            
            # 计算相似度
            similarity = self._calculate_card_similarity(current_card, card)
            if similarity > 0.3:  # 相似度阈值
                related.append({
                    'card': card,
                    'similarity': similarity,
                    'reason': self._get_similarity_reason(current_card, card)
                })
        
        # 按相似度排序
        related.sort(key=lambda x: x['similarity'], reverse=True)
        
        return related[:10]  # 返回前10个
    
    def _calculate_card_similarity(
        self, 
        card1: KnowledgeCard, 
        card2: KnowledgeCard
    ) -> float:
        """计算卡片相似度"""
        similarity = 0.0
        
        # 标签相似度
        tags1 = set(card1.tags)
        tags2 = set(card2.tags)
        if tags1 and tags2:
            tag_similarity = len(tags1.intersection(tags2)) / len(tags1.union(tags2))
            similarity += tag_similarity * 0.4
        
        # 内容相似度(简化版本)
        content_words1 = set(re.findall(r'\b\w{4,}\b', card1.content.lower()))
        content_words2 = set(re.findall(r'\b\w{4,}\b', card2.content.lower()))
        
        if content_words1 and content_words2:
            common_words = content_words1.intersection(content_words2)
            total_words = content_words1.union(content_words2)
            if total_words:
                content_similarity = len(common_words) / len(total_words)
                similarity += content_similarity * 0.6
        
        return similarity
    
    def _get_similarity_reason(
        self, 
        card1: KnowledgeCard, 
        card2: KnowledgeCard
    ) -> str:
        """获取相似原因"""
        common_tags = set(card1.tags).intersection(set(card2.tags))
        if common_tags:
            return f"共享标签: {', '.join(list(common_tags)[:3])}"
        
        return "内容相关"
    
    # === 智能功能 ===
    
    def get_recommendations(self, card_id: str) -> List[Dict[str, Any]]:
        """获取推荐卡片"""
        # 这里实现简单的推荐逻辑
        all_cards = self.db.get_all_cards()
        current_card = self.db.get_card(card_id)
        
        if not current_card:
            return []
        
        recommendations = []
        
        for card in all_cards:
            if card.id == card_id or card.status != CardStatus.ACTIVE:
                continue
            
            # 计算推荐分数
            score = 0.0
            
            # 相同类型加分
            if card.type == current_card.type:
                score += 1.0
            
            # 相似标签加分
            common_tags = len(set(card.tags).intersection(set(current_card.tags)))
            score += common_tags * 0.5
            
            # 最近更新加分
            days_since_update = (datetime.now() - card.updated_at).days
            if days_since_update < 7:
                score += 1.0
            
            if score > 0.5:
                recommendations.append({
                    'card': card,
                    'score': score,
                    'reason': self._get_recommendation_reason(card, current_card)
                })
        
        recommendations.sort(key=lambda x: x['score'], reverse=True)
        return recommendations[:5]
    
    def _get_recommendation_reason(
        self, 
        card: KnowledgeCard, 
        current_card: KnowledgeCard
    ) -> str:
        """获取推荐理由"""
        if card.type == current_card.type:
            return f"相同类型: {card.type.value}"
        
        common_tags = set(card.tags).intersection(set(current_card.tags))
        if common_tags:
            return f"共享标签: {', '.join(list(common_tags)[:2])}"
        
        return "您可能感兴趣"
    
    def generate_knowledge_graph(self) -> Dict[str, Any]:
        """生成知识图谱数据"""
        all_cards = self.db.get_all_cards(CardStatus.ACTIVE)
        
        nodes = []
        links = []
        
        # 创建节点
        for card in all_cards:
            nodes.append({
                'id': card.id,
                'name': card.title,
                'type': card.type.value,
                'value': len(card.tags) + 1,  # 节点大小
                'group': hash(card.type.value) % 5  # 分组
            })
        
        # 创建连接(基于标签)
        for i, card1 in enumerate(all_cards):
            for j, card2 in enumerate(all_cards[i+1:], i+1):
                common_tags = set(card1.tags).intersection(set(card2.tags))
                if common_tags:
                    links.append({
                        'source': card1.id,
                        'target': card2.id,
                        'value': len(common_tags),  # 连接强度
                        'tags': list(common_tags)[:2]
                    })
        
        return {
            'nodes': nodes[:50],  # 限制数量
            'links': links[:100]  # 限制数量
        }
    
    # === 统计和分析 ===
    
    def get_card_statistics(self, card: KnowledgeCard) -> Dict[str, Any]:
        """获取卡片统计信息"""
        return {
            'word_count': len(card.content.split()),
            'tag_count': len(card.tags),
            'age_days': (datetime.now() - card.created_at).days,
            'last_updated_days': (datetime.now() - card.updated_at).days
        }
    
    def get_learning_insights(self) -> Dict[str, Any]:
        """获取学习洞察"""
        all_cards = self.db.get_all_cards()
        
        insights = {
            'total_cards': len(all_cards),
            'active_cards': len([c for c in all_cards if c.status == CardStatus.ACTIVE]),
            'cards_by_type': {},
            'cards_by_week': {},
            'knowledge_growth': []
        }
        
        # 按类型统计
        for card in all_cards:
            card_type = card.type.value
            insights['cards_by_type'][card_type] = (
                insights['cards_by_type'].get(card_type, 0) + 1
            )
        
        # 按周统计
        for card in all_cards:
            week_key = card.created_at.strftime("%Y-%W")
            insights['cards_by_week'][week_key] = (
                insights['cards_by_week'].get(week_key, 0) + 1
            )
        
        # 知识增长趋势
        sorted_weeks = sorted(insights['cards_by_week'].keys())
        cumulative = 0
        for week in sorted_weeks[-12:]:  # 最近12周
            cumulative += insights['cards_by_week'][week]
            insights['knowledge_growth'].append({
                'week': week,
                'new_cards': insights['cards_by_week'][week],
                'cumulative': cumulative
            })
        
        return insights

业务逻辑架构:

第三部分:前端界面实现

3.1 主应用框架

现在让我们实现QML前端界面。首先是应用的主框架:

javascript 复制代码
// src/frontend/qml/main.qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import Qt.labs.platform
import Qt5Compat.GraphicalEffects

ApplicationWindow {
    id: appWindow
    width: 1200
    height: 800
    visible: true
    title: "智慧笔记 - 个人知识管理"
    minimumWidth: 800
    minimumHeight: 600
    
    // 主题管理
    property bool darkMode: false
    property color primaryColor: "#2196F3"
    property color backgroundColor: darkMode ? "#1E1E1E" : "#F5F5F5"
    property color surfaceColor: darkMode ? "#2D2D2D" : "#FFFFFF"
    property color textColor: darkMode ? "#FFFFFF" : "#333333"
    property color textSecondaryColor: darkMode ? "#B0B0B0" : "#666666"
    
    // 平台检测
    readonly property bool isMobile: Qt.platform.os === "android" || Qt.platform.os === "ios"
    readonly property bool isDesktop: !isMobile
    
    // 响应式设计
    property real responsiveScale: Math.min(width / 1200, 1.0)
    property bool showSidebar: width > 800
    property bool showRightPanel: width > 1000
    
    // 初始化
    Component.onCompleted: {
        console.log("智慧笔记启动")
        console.log("平台:", Qt.platform.os)
        console.log("屏幕尺寸:", width, "x", height)
        
        // 加载用户设置
        loadSettings()
    }
    
    // 加载设置
    function loadSettings() {
        // 这里从本地存储加载设置
        darkMode = false
    }
    
    // 保存设置
    function saveSettings() {
        // 这里保存设置到本地存储
    }
    
    // 主布局
    SplitView {
        anchors.fill: parent
        
        // 左侧边栏
        Sidebar {
            id: sidebar
            SplitView.preferredWidth: 280
            SplitView.minimumWidth: 200
            SplitView.maximumWidth: 400
            visible: showSidebar
        }
        
        // 主内容区域
        MainContent {
            id: mainContent
            SplitView.fillWidth: true
        }
        
        // 右侧面板
        RightPanel {
            id: rightPanel
            SplitView.preferredWidth: 320
            SplitView.minimumWidth: 250
            SplitView.maximumWidth: 450
            visible: showRightPanel
        }
    }
    
    // 状态栏
    StatusBar {
        id: statusBar
    }
    
    // 快捷键
    Shortcut {
        sequence: "Ctrl+N"
        onActivated: mainContent.createNewCard()
    }
    
    Shortcut {
        sequence: "Ctrl+F"
        onActivated: mainContent.showSearch()
    }
    
    Shortcut {
        sequence: "Ctrl+," // Ctrl+逗号
        onActivated: settingsDialog.open()
    }
    
    // 设置对话框
    SettingsDialog {
        id: settingsDialog
    }
    
    // 关于对话框
    AboutDialog {
        id: aboutDialog
    }
}

3.2 知识卡片组件

让我们创建一个美观的知识卡片组件:

javascript 复制代码
// src/frontend/qml/components/KnowledgeCard.qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects

Rectangle {
    id: card
    
    // 属性
    property var cardData: ({})
    property bool isSelected: false
    property bool isExpanded: false
    property real cornerRadius: 12
    
    // 信号
    signal clicked()
    signal doubleClicked()
    signal tagClicked(string tag)
    signal editRequested()
    signal deleteRequested()
    
    // 尺寸
    width: ListView.view ? ListView.view.width : 300
    height: isExpanded ? contentColumn.implicitHeight + 40 : 120
    
    // 外观
    color: surfaceColor
    radius: cornerRadius
    border.color: isSelected ? primaryColor : "#E0E0E0"
    border.width: isSelected ? 2 : 1
    
    // 阴影效果
    layer.enabled: true
    layer.effect: DropShadow {
        color: "#20000000"
        radius: 8
        samples: 16
        verticalOffset: 2
    }
    
    // 卡片内容
    ColumnLayout {
        id: contentColumn
        anchors.fill: parent
        anchors.margins: 16
        spacing: 12
        
        // 标题行
        RowLayout {
            Layout.fillWidth: true
            
            // 类型图标
            Rectangle {
                width: 32
                height: 32
                radius: 16
                color: getTypeColor(cardData.type)
                
                Text {
                    text: getTypeIcon(cardData.type)
                    color: "white"
                    font.pixelSize: 16
                    anchors.centerIn: parent
                }
            }
            
            // 标题
            ColumnLayout {
                spacing: 2
                Layout.fillWidth: true
                
                Text {
                    text: cardData.title || "无标题"
                    font.pixelSize: 18
                    font.bold: true
                    color: textColor
                    elide: Text.ElideRight
                    maximumLineCount: 1
                    Layout.fillWidth: true
                }
                
                // 元信息
                RowLayout {
                    spacing: 8
                    
                    Text {
                        text: formatDate(cardData.updated_at)
                        color: textSecondaryColor
                        font.pixelSize: 12
                    }
                    
                    // 状态标签
                    Rectangle {
                        visible: cardData.status && cardData.status !== "active"
                        height: 16
                        radius: 8
                        color: getStatusColor(cardData.status)
                        
                        Text {
                            text: getStatusText(cardData.status)
                            color: "white"
                            font.pixelSize: 10
                            font.bold: true
                            anchors.centerIn: parent
                            anchors.margins: 6
                        }
                    }
                }
            }
            
            // 操作按钮
            MenuButton {
                Layout.alignment: Qt.AlignTop
                
                MenuItem {
                    text: "编辑"
                    onTriggered: card.editRequested()
                }
                
                MenuItem {
                    text: isExpanded ? "收起" : "展开"
                    onTriggered: isExpanded = !isExpanded
                }
                
                MenuItem {
                    text: "复制链接"
                }
                
                MenuSeparator {}
                
                MenuItem {
                    text: "删除"
                    textColor: "red"
                    onTriggered: card.deleteRequested()
                }
            }
        }
        
        // 内容预览
        Text {
            id: contentPreview
            text: cardData.content || ""
            color: textSecondaryColor
            font.pixelSize: 14
            wrapMode: Text.WordWrap
            maximumLineCount: isExpanded ? 0 : 2
            elide: Text.ElideRight
            lineHeight: 1.4
            Layout.fillWidth: true
            visible: text.length > 0
        }
        
        // 标签区域
        Flow {
            id: tagsFlow
            spacing: 6
            Layout.fillWidth: true
            visible: cardData.tags && cardData.tags.length > 0
            
            Repeater {
                model: cardData.tags || []
                
                Rectangle {
                    height: 24
                    radius: 12
                    color: Qt.lighter(primaryColor, 1.8)
                    
                    RowLayout {
                        height: parent.height
                        spacing: 4
                        
                        Text {
                            text: modelData
                            color: primaryColor
                            font.pixelSize: 11
                            font.bold: true
                        }
                        
                        Text {
                            text: "×"
                            color: primaryColor
                            font.pixelSize: 14
                            font.bold: true
                            visible: false // 暂时隐藏删除按钮
                        }
                    }
                    
                    MouseArea {
                        anchors.fill: parent
                        onClicked: card.tagClicked(modelData)
                        cursorShape: Qt.PointingHandCursor
                    }
                }
            }
        }
        
        // 优先级指示器
        RowLayout {
            spacing: 8
            visible: cardData.priority > 0
            
            Text {
                text: "优先级:"
                color: textSecondaryColor
                font.pixelSize: 12
            }
            
            Repeater {
                model: 5
                
                Rectangle {
                    width: 12
                    height: 12
                    radius: 6
                    color: index < cardData.priority ? 
                           getPriorityColor(cardData.priority) : "#E0E0E0"
                }
            }
        }
    }
    
    // 点击效果
    MouseArea {
        anchors.fill: parent
        acceptedButtons: Qt.LeftButton | Qt.RightButton
        
        onClicked: (mouse) => {
            if (mouse.button === Qt.LeftButton) {
                card.clicked()
            }
        }
        
        onDoubleClicked: card.doubleClicked()
        
        // 点击反馈
        Rectangle {
            anchors.fill: parent
            color: "white"
            opacity: parent.pressed ? 0.1 : 0
            radius: card.radius
        }
    }
    
    // 工具函数
    function getTypeColor(type) {
        switch(type) {
        case "note": return "#4CAF50"
        case "idea": return "#2196F3"
        case "task": return "#FF9800"
        case "question": return "#9C27B0"
        case "answer": return "#009688"
        case "reference": return "#795548"
        default: return "#9E9E9E"
        }
    }
    
    function getTypeIcon(type) {
        switch(type) {
        case "note": return "📝"
        case "idea": return "💡"
        case "task": return "✅"
        case "question": return "❓"
        case "answer": return "💬"
        case "reference": return "📚"
        default: return "📄"
        }
    }
    
    function getStatusColor(status) {
        switch(status) {
        case "draft": return "#FF9800"
        case "active": return "#4CAF50"
        case "archived": return "#9E9E9E"
        case "deleted": return "#F44336"
        default: return "#9E9E9E"
        }
    }
    
    function getStatusText(status) {
        switch(status) {
        case "draft": return "草稿"
        case "active": return "活跃"
        case "archived": return "归档"
        case "deleted": return "已删除"
        default: return "未知"
        }
    }
    
    function getPriorityColor(priority) {
        if (priority <= 2) return "#4CAF50"
        if (priority <= 4) return "#FF9800"
        return "#F44336"
    }
    
    function formatDate(dateString) {
        if (!dateString) return ""
        
        var date = new Date(dateString)
        var now = new Date()
        var diffMs = now - date
        var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
        
        if (diffDays === 0) {
            return "今天"
        } else if (diffDays === 1) {
            return "昨天"
        } else if (diffDays < 7) {
            return diffDays + "天前"
        } else if (diffDays < 30) {
            return Math.floor(diffDays / 7) + "周前"
        } else {
            return date.toLocaleDateString()
        }
    }
}

组件交互流程:

.3 知识图谱可视化

让我们实现一个简单的知识图谱可视化组件:

javascript 复制代码
// src/frontend/qml/components/KnowledgeGraph.qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects

Rectangle {
    id: graph
    
    // 属性
    property var graphData: ({ nodes: [], links: [] })
    property var selectedNode: null
    
    // 信号
    signal nodeClicked(var node)
    signal nodeDoubleClicked(var node)
    
    // 外观
    color: "#1E1E1E"
    radius: 8
    
    // 画布
    Canvas {
        id: canvas
        anchors.fill: parent
        
        // 物理模拟属性
        property real gravity: 0.1
        property real springLength: 100
        property real springStrength: 0.002
        property real repulsionStrength: 10000
        property real friction: 0.9
        
        // 节点位置
        property var nodePositions: ({})
        property var nodeVelocities: ({})
        
        onPaint: {
            var ctx = getContext("2d")
            
            // 清空画布
            ctx.clearRect(0, 0, width, height)
            
            // 绘制连接
            ctx.strokeStyle = "#666666"
            ctx.lineWidth = 1
            
            for (var i = 0; i < graphData.links.length; i++) {
                var link = graphData.links[i]
                var source = graphData.nodes.find(n => n.id === link.source)
                var target = graphData.nodes.find(n => n.id === link.target)
                
                if (source && target && nodePositions[source.id] && nodePositions[target.id]) {
                    var pos1 = nodePositions[source.id]
                    var pos2 = nodePositions[target.id]
                    
                    // 绘制连线
                    ctx.beginPath()
                    ctx.moveTo(pos1.x, pos1.y)
                    ctx.lineTo(pos2.x, pos2.y)
                    ctx.stroke()
                    
                    // 绘制连线上的标签
                    if (link.tags && link.tags.length > 0) {
                        var midX = (pos1.x + pos2.x) / 2
                        var midY = (pos1.y + pos2.y) / 2
                        
                        ctx.fillStyle = "#666666"
                        ctx.font = "10px Arial"
                        ctx.fillText(link.tags[0], midX, midY)
                    }
                }
            }
            
            // 绘制节点
            for (var i = 0; i < graphData.nodes.length; i++) {
                var node = graphData.nodes[i]
                var pos = nodePositions[node.id]
                
                if (!pos) continue
                
                // 节点颜色
                var colors = ["#4CAF50", "#2196F3", "#FF9800", "#9C27B0", "#009688"]
                var color = colors[node.group % colors.length]
                
                // 绘制节点
                ctx.beginPath()
                ctx.arc(pos.x, pos.y, 10 + node.value * 2, 0, Math.PI * 2)
                
                if (selectedNode && selectedNode.id === node.id) {
                    // 选中的节点
                    ctx.fillStyle = "white"
                    ctx.fill()
                    
                    ctx.beginPath()
                    ctx.arc(pos.x, pos.y, 12 + node.value * 2, 0, Math.PI * 2)
                    ctx.strokeStyle = "#FFEB3B"
                    ctx.lineWidth = 3
                    ctx.stroke()
                    
                    ctx.fillStyle = color
                    ctx.fill()
                } else {
                    // 普通节点
                    var gradient = ctx.createRadialGradient(
                        pos.x, pos.y, 0,
                        pos.x, pos.y, 20
                    )
                    gradient.addColorStop(0, Qt.lighter(color, 1.5))
                    gradient.addColorStop(1, color)
                    
                    ctx.fillStyle = gradient
                    ctx.fill()
                }
                
                // 绘制节点标签
                ctx.fillStyle = "white"
                ctx.font = "bold 12px Arial"
                ctx.textAlign = "center"
                ctx.fillText(node.name.substring(0, 10), pos.x, pos.y - 20)
                
                // 绘制类型
                ctx.fillStyle = "#AAAAAA"
                ctx.font = "10px Arial"
                ctx.fillText(node.type, pos.x, pos.y + 30)
            }
        }
        
        // 初始化物理模拟
        Component.onCompleted: {
            initializePhysics()
            simulationTimer.start()
        }
        
        // 初始化物理模拟
        function initializePhysics() {
            nodePositions = {}
            nodeVelocities = {}
            
            // 随机初始化位置
            for (var i = 0; i < graphData.nodes.length; i++) {
                var node = graphData.nodes[i]
                nodePositions[node.id] = {
                    x: Math.random() * width,
                    y: Math.random() * height
                }
                nodeVelocities[node.id] = { x: 0, y: 0 }
            }
        }
        
        // 物理模拟
        function simulate() {
            // 计算排斥力
            for (var i = 0; i < graphData.nodes.length; i++) {
                var node1 = graphData.nodes[i]
                var pos1 = nodePositions[node1.id]
                var vel1 = nodeVelocities[node1.id]
                
                for (var j = i + 1; j < graphData.nodes.length; j++) {
                    var node2 = graphData.nodes[j]
                    var pos2 = nodePositions[node2.id]
                    
                    var dx = pos2.x - pos1.x
                    var dy = pos2.y - pos1.y
                    var distance = Math.sqrt(dx * dx + dy * dy) + 0.1
                    
                    // 排斥力
                    var force = repulsionStrength / (distance * distance)
                    var fx = force * dx / distance
                    var fy = force * dy / distance
                    
                    vel1.x -= fx
                    vel1.y -= fy
                    nodeVelocities[node2.id].x += fx
                    nodeVelocities[node2.id].y += fy
                }
            }
            
            // 计算弹簧力
            for (var i = 0; i < graphData.links.length; i++) {
                var link = graphData.links[i]
                var pos1 = nodePositions[link.source]
                var pos2 = nodePositions[link.target]
                var vel1 = nodeVelocities[link.source]
                var vel2 = nodeVelocities[link.target]
                
                if (!pos1 || !pos2) continue
                
                var dx = pos2.x - pos1.x
                var dy = pos2.y - pos1.y
                var distance = Math.sqrt(dx * dx + dy * dy)
                
                // 弹簧力
                var force = springStrength * (distance - springLength)
                var fx = force * dx / distance
                var fy = force * dy / distance
                
                vel1.x += fx
                vel1.y += fy
                vel2.x -= fx
                vel2.y -= fy
            }
            
            // 应用速度和边界
            for (var i = 0; i < graphData.nodes.length; i++) {
                var node = graphData.nodes[i]
                var pos = nodePositions[node.id]
                var vel = nodeVelocities[node.id]
                
                // 更新速度
                vel.x *= friction
                vel.y *= friction
                
                // 更新位置
                pos.x += vel.x
                pos.y += vel.y
                
                // 边界碰撞
                var radius = 10 + node.value * 2
                if (pos.x < radius) {
                    pos.x = radius
                    vel.x = -vel.x * 0.5
                }
                if (pos.x > width - radius) {
                    pos.x = width - radius
                    vel.x = -vel.x * 0.5
                }
                if (pos.y < radius) {
                    pos.y = radius
                    vel.y = -vel.y * 0.5
                }
                if (pos.y > height - radius) {
                    pos.y = height - radius
                    vel.y = -vel.y * 0.5
                }
            }
            
            // 重绘画布
            canvas.requestPaint()
        }
        
        // 模拟定时器
        Timer {
            id: simulationTimer
            interval: 16 // ~60fps
            running: false
            repeat: true
            onTriggered: canvas.simulate()
        }
        
        // 鼠标交互
        MouseArea {
            id: graphMouseArea
            anchors.fill: parent
            hoverEnabled: true
            drag.threshold: 5
            
            property var draggedNode: null
            property point lastMousePos
            
            onPressed: (mouse) => {
                // 检查是否点击了节点
                var clickedNode = getNodeAt(mouse.x, mouse.y)
                if (clickedNode) {
                    draggedNode = clickedNode
                    selectedNode = clickedNode
                    graph.nodeClicked(clickedNode)
                    lastMousePos = Qt.point(mouse.x, mouse.y)
                } else {
                    selectedNode = null
                }
            }
            
            onPositionChanged: (mouse) => {
                if (draggedNode) {
                    // 拖动节点
                    var pos = canvas.nodePositions[draggedNode.id]
                    var vel = canvas.nodeVelocities[draggedNode.id]
                    
                    var dx = mouse.x - lastMousePos.x
                    var dy = mouse.y - lastMousePos.y
                    
                    pos.x += dx
                    pos.y += dy
                    vel.x = dx * 0.5
                    vel.y = dy * 0.5
                    
                    lastMousePos = Qt.point(mouse.x, mouse.y)
                }
            }
            
            onReleased: {
                draggedNode = null
            }
            
            onDoubleClicked: (mouse) => {
                var clickedNode = getNodeAt(mouse.x, mouse.y)
                if (clickedNode) {
                    graph.nodeDoubleClicked(clickedNode)
                }
            }
            
            // 获取鼠标位置的节点
            function getNodeAt(x, y) {
                for (var i = 0; i < graphData.nodes.length; i++) {
                    var node = graphData.nodes[i]
                    var pos = canvas.nodePositions[node.id]
                    
                    if (!pos) continue
                    
                    var dx = x - pos.x
                    var dy = y - pos.y
                    var distance = Math.sqrt(dx * dx + dy * dy)
                    var radius = 10 + node.value * 2
                    
                    if (distance < radius) {
                        return node
                    }
                }
                return null
            }
        }
    }
    
    // 控制面板
    Rectangle {
        anchors.top: parent.top
        anchors.right: parent.right
        anchors.margins: 10
        width: 200
        height: 120
        radius: 6
        color: "#2D2D2D"
        opacity: 0.9
        
        ColumnLayout {
            anchors.fill: parent
            anchors.margins: 10
            spacing: 5
            
            Text {
                text: "知识图谱"
                color: "white"
                font.bold: true
                font.pixelSize: 16
            }
            
            RowLayout {
                Text {
                    text: "节点:"
                    color: "#AAAAAA"
                }
                
                Text {
                    text: graphData.nodes.length
                    color: "white"
                }
                
                Item { Layout.fillWidth: true }
            }
            
            RowLayout {
                Text {
                    text: "连接:"
                    color: "#AAAAAA"
                }
                
                Text {
                    text: graphData.links.length
                    color: "white"
                }
                
                Item { Layout.fillWidth: true }
            }
            
            Button {
                text: "重置布局"
                onClicked: canvas.initializePhysics()
            }
        }
    }
}

知识图谱可视化流程:

第四部分:集成与优化

4.1 应用集成

让我们将所有组件集成在一起:

python 复制代码
# src/backend/main.py
"""
智慧笔记应用主入口
"""

import sys
import os
from pathlib import Path

# 添加项目根目录到Python路径
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))

from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtCore import QUrl

from backend.database.database_service import DatabaseService
from backend.services.knowledge_service import KnowledgeService


class SmartNotesApp:
    """智慧笔记应用主类"""
    
    def __init__(self):
        # 初始化应用
        self.app = QGuiApplication(sys.argv)
        self.app.setApplicationName("智慧笔记")
        self.app.setOrganizationName("SmartNotes")
        self.app.setOrganizationDomain("smartnotes.app")
        
        # 初始化数据库
        self.db_service = DatabaseService()
        
        # 初始化业务服务
        self.knowledge_service = KnowledgeService(self.db_service)
        
        # 初始化QML引擎
        self.engine = QQmlApplicationEngine()
        
    def setup_qml_context(self):
        """设置QML上下文"""
        # 注册Python对象到QML
        context = self.engine.rootContext()
        context.setContextProperty("knowledgeService", self.knowledge_service)
        context.setContextProperty("databaseService", self.db_service)
        
    def load_qml(self):
        """加载QML界面"""
        # 获取QML文件路径
        qml_path = project_root / "src" / "frontend" / "qml" / "main.qml"
        
        if not qml_path.exists():
            print(f"错误: 找不到QML文件: {qml_path}")
            sys.exit(1)
        
        # 加载QML
        self.engine.load(QUrl.fromLocalFile(str(qml_path)))
        
        if not self.engine.rootObjects():
            print("错误: 加载QML失败")
            sys.exit(1)
    
    def run(self):
        """运行应用"""
        self.setup_qml_context()
        self.load_qml()
        
        # 运行应用
        return self.app.exec()
    
    def cleanup(self):
        """清理资源"""
        self.db_service.close()


def main():
    """主函数"""
    app = SmartNotesApp()
    
    try:
        return app.run()
    except KeyboardInterrupt:
        print("\n应用被用户中断")
    except Exception as e:
        print(f"应用错误: {e}")
        import traceback
        traceback.print_exc()
    finally:
        app.cleanup()


if __name__ == "__main__":
    sys.exit(main())

4.2 构建和部署

创建一个简单的构建脚本:

python 复制代码
# scripts/build.py
"""
构建脚本
"""

import sys
import os
import shutil
from pathlib import Path
import subprocess
import argparse


class Builder:
    """应用构建器"""
    
    def __init__(self, project_root=None):
        self.project_root = Path(project_root or Path(__file__).parent.parent)
        self.build_dir = self.project_root / "build"
        
    def clean(self):
        """清理构建目录"""
        if self.build_dir.exists():
            print(f"清理构建目录: {self.build_dir}")
            shutil.rmtree(self.build_dir)
        
    def prepare_build_dir(self):
        """准备构建目录"""
        self.build_dir.mkdir(exist_ok=True)
        print(f"创建构建目录: {self.build_dir}")
    
    def copy_resources(self):
        """复制资源文件"""
        resources_dir = self.project_root / "src" / "frontend" / "resources"
        if resources_dir.exists():
            dest_dir = self.build_dir / "resources"
            if dest_dir.exists():
                shutil.rmtree(dest_dir)
            shutil.copytree(resources_dir, dest_dir)
            print(f"复制资源文件到: {dest_dir}")
    
    def copy_qml_files(self):
        """复制QML文件"""
        qml_dir = self.project_root / "src" / "frontend" / "qml"
        if qml_dir.exists():
            dest_dir = self.build_dir / "qml"
            if dest_dir.exists():
                shutil.rmtree(dest_dir)
            shutil.copytree(qml_dir, dest_dir)
            print(f"复制QML文件到: {dest_dir}")
    
    def copy_python_files(self):
        """复制Python文件"""
        backend_dir = self.project_root / "src" / "backend"
        if backend_dir.exists():
            dest_dir = self.build_dir / "backend"
            if dest_dir.exists():
                shutil.rmtree(dest_dir)
            shutil.copytree(backend_dir, dest_dir)
            print(f"复制Python文件到: {dest_dir}")
        
        # 复制共享文件
        shared_dir = self.project_root / "src" / "shared"
        if shared_dir.exists():
            dest_dir = self.build_dir / "shared"
            if dest_dir.exists():
                shutil.rmtree(dest_dir)
            shutil.copytree(shared_dir, dest_dir)
            print(f"复制共享文件到: {dest_dir}")
    
    def create_entry_point(self, platform="desktop"):
        """创建入口点"""
        if platform == "desktop":
            entry_content = '''#!/usr/bin/env python3
"""
智慧笔记应用入口点
"""

import sys
import os

# 添加构建目录到Python路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from backend.main import main

if __name__ == "__main__":
    sys.exit(main())
'''
        elif platform == "android":
            entry_content = '''#!/usr/bin/env python3
"""
智慧笔记Android入口点
"""

# Android特定初始化代码
'''
        else:
            entry_content = '''#!/usr/bin/env python3
"""
智慧笔记应用入口点
"""

import sys
import os

# 添加构建目录到Python路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from backend.main import main

if __name__ == "__main__":
    sys.exit(main())
'''

        entry_path = self.build_dir / "smartnotes.py"
        with open(entry_path, "w", encoding="utf-8") as f:
            f.write(entry_content)
        
        # 设置可执行权限(Unix-like系统)
        if os.name != 'nt':
            os.chmod(entry_path, 0o755)
        
        print(f"创建入口点: {entry_path}")
    
    def create_requirements_file(self):
        """创建requirements.txt文件"""
        requirements_content = '''PySide6>=6.5.0
'''
        
        requirements_path = self.build_dir / "requirements.txt"
        with open(requirements_path, "w", encoding="utf-8") as f:
            f.write(requirements_content)
        
        print(f"创建依赖文件: {requirements_path}")
    
    def create_readme(self):
        """创建README文件"""
        readme_content = """# 智慧笔记

一个跨平台的个人知识管理应用。

## 功能特性

- 📝 创建和管理知识卡片
- 🏷️ 智能标签和分类
- 🔗 知识图谱可视化
- 🔍 强大的搜索功能
- 📊 学习统计和洞察
- 🌐 跨平台支持

## 运行要求

- Python 3.8 或更高版本
- PySide6
- SQLite3

## 快速开始

1. 安装依赖:
bash
pip install -r requirements.txt

2. 运行应用:
bash
python smartnotes.py

## 构建

使用构建脚本:
bash
python smartnotes.py

## 许可

MIT License
"""
        
        readme_path = self.build_dir / "README.md"
        with open(readme_path, "w", encoding="utf-8") as f:
            f.write(readme_content)
        
        print(f"创建README文件: {readme_path}")
    
    def install_dependencies(self):
        """安装Python依赖"""
        print("安装Python依赖...")
        
        requirements_path = self.build_dir / "requirements.txt"
        
        try:
            subprocess.check_call([
                sys.executable, "-m", "pip", "install", 
                "-r", str(requirements_path)
            ])
            print("依赖安装完成")
        except subprocess.CalledProcessError as e:
            print(f"依赖安装失败: {e}")
            return False
        
        return True
    
    def build_desktop(self):
        """构建桌面应用"""
        print("开始构建桌面应用...")
        
        # 清理构建目录
        self.clean()
        
        # 准备构建目录
        self.prepare_build_dir()
        
        # 复制文件
        self.copy_resources()
        self.copy_qml_files()
        self.copy_python_files()
        
        # 创建配置文件
        self.create_entry_point("desktop")
        self.create_requirements_file()
        self.create_readme()
        
        print("桌面应用构建完成")
        
        # 提示如何运行
        print("\n运行应用:")
        print(f"  cd {self.build_dir}")
        print("  python smartnotes.py")
        
        return True
    
    def build_android(self):
        """构建Android应用"""
        print("开始构建Android应用...")
        
        # 清理构建目录
        self.clean()
        
        # 准备构建目录
        self.prepare_build_dir()
        
        # 复制文件
        self.copy_resources()
        self.copy_qml_files()
        self.copy_python_files()
        
        # 创建配置文件
        self.create_entry_point("android")
        self.create_requirements_file()
        self.create_readme()
        
        # 复制Android特定文件
        android_dir = self.project_root / "platform" / "android"
        if android_dir.exists():
            dest_dir = self.build_dir / "android"
            if dest_dir.exists():
                shutil.rmtree(dest_dir)
            shutil.copytree(android_dir, dest_dir)
            print(f"复制Android文件到: {dest_dir}")
        
        print("Android应用构建完成")
        print("注意: Android构建需要额外的Qt for Android配置")
        
        return True
    
    def create_installer(self, platform="windows"):
        """创建安装包"""
        print(f"开始创建{platform}安装包...")
        
        installer_dir = self.project_root / "installer"
        installer_dir.mkdir(exist_ok=True)
        
        if platform == "windows":
            # 创建Windows安装脚本
            self._create_windows_installer()
        elif platform == "macos":
            # 创建macOS安装包
            self._create_macos_installer()
        elif platform == "linux":
            # 创建Linux安装包
            self._create_linux_installer()
        
        print(f"{platform}安装包创建完成")
        return True
    
    def _create_windows_installer(self):
        """创建Windows安装器"""
        # 这里可以使用PyInstaller或NSIS
        # 简化版本:创建批处理脚本
        installer_content = '''@echo off
echo 正在安装智慧笔记...
echo.

REM 检查Python
python --version >nul 2>&1
if errorlevel 1 (
    echo 错误: 未找到Python
    echo 请从 https://python.org 下载并安装Python
    pause
    exit /b 1
)

REM 检查PySide6
python -c "import PySide6" >nul 2>&1
if errorlevel 1 (
    echo 安装PySide6...
    pip install PySide6
)

REM 创建桌面快捷方式
echo 创建桌面快捷方式...
echo 安装完成!
pause
'''
        
        installer_path = self.project_root / "installer" / "install.bat"
        with open(installer_path, "w", encoding="gbk") as f:
            f.write(installer_content)
        
        print(f"创建Windows安装脚本: {installer_path}")
    
    def _create_macos_installer(self):
        """创建macOS安装包"""
        # 创建macOS应用包结构
        app_name = "SmartNotes.app"
        app_path = self.project_root / "installer" / app_name
        
        # 创建应用包结构
        contents_dir = app_path / "Contents"
        macos_dir = contents_dir / "MacOS"
        resources_dir = contents_dir / "Resources"
        
        macos_dir.mkdir(parents=True, exist_ok=True)
        resources_dir.mkdir(parents=True, exist_ok=True)
        
        # 创建Info.plist
        plist_content = '''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDisplayName</key>
    <string>智慧笔记</string>
    <key>CFBundleExecutable</key>
    <string>smartnotes</string>
    <key>CFBundleIdentifier</key>
    <string>com.smartnotes.app</string>
    <key>CFBundleName</key>
    <string>SmartNotes</string>
    <key>CFBundleVersion</key>
    <string>1.0.0</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0</string>
    <key>NSPrincipalClass</key>
    <string>NSApplication</string>
    <key>LSMinimumSystemVersion</key>
    <string>10.12</string>
</dict>
</plist>
'''
        
        plist_path = contents_dir / "Info.plist"
        with open(plist_path, "w", encoding="utf-8") as f:
            f.write(plist_content)
        
        print(f"创建macOS应用包: {app_path}")
    
    def _create_linux_installer(self):
        """创建Linux安装包"""
        # 创建deb包控制文件
        control_content = '''Package: smartnotes
Version: 1.0.0
Section: utils
Priority: optional
Architecture: all
Depends: python3, python3-pyside6.qml
Maintainer: SmartNotes Team <team@smartnotes.app>
Description: Personal Knowledge Management Application
 A cross-platform personal knowledge management application
 with smart tagging, knowledge graph, and learning insights.
'''
        
        control_dir = self.project_root / "installer" / "DEBIAN"
        control_dir.mkdir(parents=True, exist_ok=True)
        
        control_path = control_dir / "control"
        with open(control_path, "w", encoding="utf-8") as f:
            f.write(control_content)
        
        print(f"创建Linux安装包控制文件: {control_path}")


def main():
    """主函数"""
    parser = argparse.ArgumentParser(description="智慧笔记构建工具")
    parser.add_argument(
        "action",
        choices=["build", "clean", "install", "package"],
        help="构建动作"
    )
    parser.add_argument(
        "--platform",
        choices=["desktop", "android", "ios"],
        default="desktop",
        help="目标平台"
    )
    parser.add_argument(
        "--installer",
        choices=["windows", "macos", "linux"],
        help="创建安装包"
    )
    
    args = parser.parse_args()
    
    builder = Builder()
    
    if args.action == "clean":
        builder.clean()
        print("清理完成")
    
    elif args.action == "build":
        if args.platform == "desktop":
            builder.build_desktop()
        elif args.platform == "android":
            builder.build_android()
        elif args.platform == "ios":
            print("iOS构建暂不支持")
    
    elif args.action == "install":
        builder.build_desktop()
        if builder.install_dependencies():
            print("安装完成")
        else:
            print("安装失败")
    
    elif args.action == "package":
        if args.installer:
            builder.build_desktop()
            builder.create_installer(args.installer)
        else:
            print("错误: 需要指定--installer参数")
    
    else:
        print("错误: 未知动作")


if __name__ == "__main__":
    main()
bash 复制代码
bash
pip install -r requirements.txt

2. 运行应用:
bash
python smartnotes.py

## 构建

使用构建脚本:
bash
python smartnotes.py

第五部分:总结

5.1 项目回顾

通过这个综合实战项目,我们构建了一个完整的跨平台个人知识管理应用。让我们回顾一下学到的关键知识:

5.2 关键知识点

  1. 架构设计:清晰的分层架构,前后端分离

  2. 数据建模:使用dataclass和枚举定义数据模型

  3. 数据库设计:SQLite数据库设计和管理

  4. 业务逻辑:复杂的业务逻辑实现和优化

  5. QML界面:响应式、美观的用户界面

  6. 知识图谱:数据可视化技术

  7. 构建部署:自动化构建和打包流程

5.3 进一步改进方向

这个应用还有很多可以改进和扩展的地方:

  1. 云同步:添加云存储和同步功能

  2. 协作功能:支持多人协作编辑

  3. AI增强:集成AI进行智能分析和建议

  4. 移动端优化:专门为移动端优化的界面

  5. 插件系统:支持第三方插件扩展

  6. 离线功能:完善的离线工作支持

  7. 数据分析:更深入的学习分析功能

5.4 学习建议

  1. 从简单开始:先实现核心功能,再逐步添加高级功能

  2. 测试驱动:为每个功能编写测试代码

  3. 代码审查:定期审查代码,保持代码质量

  4. 文档先行:编写清晰的文档和注释

  5. 用户反馈:收集用户反馈,持续改进

  6. 性能监控:监控应用性能,及时优化

结束语

恭喜你完成了这个完整的跨平台应用开发实战!通过这个项目,你不仅学习了Qt/QML和Python的技术知识,更重要的是掌握了完整应用开发的全流程

记住,真正的开发者不是在象牙塔里写代码,而是在解决真实世界的问题。这个智慧笔记应用虽然只是一个示例,但它包含了现代应用开发的几乎所有关键要素。

相关推荐
ian4u1 小时前
车载 Android C++ 完整技能路线:从基础到进阶
android·开发语言·c++
lly2024061 小时前
JSP 过滤器
开发语言
aq55356001 小时前
数字资源分发的技术革命与未来趋势
java·开发语言·python·php
AI玫瑰助手1 小时前
Python基础:元组的定义与不可变特性(对比列表)
开发语言·python·信息可视化
张驰咨询公司1 小时前
六西格玛数据分析实战:用Python实现DPMO与西格玛水平计算
开发语言·python·数据分析·六西格玛培训·六西格玛培训公司
HHHHH1010HHHHH2 小时前
Tailwind CSS如何快速定义固定宽高比_使用aspect-square实现CSS正方形
jvm·数据库·python
雕刻刀2 小时前
linux中复制conda环境
linux·python·conda
invicinble2 小时前
对于java基础
java·开发语言
m0_515098422 小时前
c++怎么获取文件的Inode节点信息_stat结构体深度解析【详解】
jvm·数据库·python