Compare commits
	
		
			2 Commits
		
	
	
		
			15af405d02
			...
			b5430eb8d0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b5430eb8d0 | |||
| e2e1e9e9e9 | 
							
								
								
									
										855
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										855
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -31,15 +31,20 @@
 | 
			
		||||
    "clsx": "^2.1.1",
 | 
			
		||||
    "dayjs": "^1.11.18",
 | 
			
		||||
    "es-toolkit": "^1.40.0",
 | 
			
		||||
    "events": "^3.3.0",
 | 
			
		||||
    "graphology": "^0.26.0",
 | 
			
		||||
    "highlight.js": "^11.11.1",
 | 
			
		||||
    "lodash-es": "^4.17.21",
 | 
			
		||||
    "lucide-react": "^0.545.0",
 | 
			
		||||
    "nanoid": "^5.1.6",
 | 
			
		||||
    "pocketbase": "^0.26.2",
 | 
			
		||||
    "pouchdb-adapter-memory": "^9.0.0",
 | 
			
		||||
    "pouchdb-browser": "^9.0.0",
 | 
			
		||||
    "react": "^19.2.0",
 | 
			
		||||
    "react-dom": "^19.2.0",
 | 
			
		||||
    "react-resizable-panels": "^3.0.6",
 | 
			
		||||
    "react-toastify": "^11.0.5",
 | 
			
		||||
    "sigma": "^3.0.2",
 | 
			
		||||
    "tailwind-merge": "^3.3.1",
 | 
			
		||||
    "three": "^0.180.0",
 | 
			
		||||
    "wavesurfer.js": "^7.11.0",
 | 
			
		||||
@@ -50,11 +55,14 @@
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@kevisual/types": "^0.0.10",
 | 
			
		||||
    "@types/pouchdb": "^6.4.2",
 | 
			
		||||
    "@types/pouchdb-browser": "^6.1.5",
 | 
			
		||||
    "@types/react": "^19.2.2",
 | 
			
		||||
    "@types/react-dom": "^19.2.2",
 | 
			
		||||
    "@types/three": "^0.180.0",
 | 
			
		||||
    "@vitejs/plugin-basic-ssl": "^2.1.0",
 | 
			
		||||
    "dotenv": "^17.2.3",
 | 
			
		||||
    "pouchdb": "^9.0.0",
 | 
			
		||||
    "tailwindcss": "^4.1.14",
 | 
			
		||||
    "tw-animate-css": "^1.4.0"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										127
									
								
								web/src/apps/muse/base/graph/sigma/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								web/src/apps/muse/base/graph/sigma/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,127 @@
 | 
			
		||||
import React, { useEffect, useRef, useState } from 'react';
 | 
			
		||||
import Graph from 'graphology';
 | 
			
		||||
import Sigma from 'sigma';
 | 
			
		||||
import { Mark } from '../../../modules/mark';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  dataSource?: Mark[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SigmaGraph = (props: Props) => {
 | 
			
		||||
  const { dataSource = [] } = props;
 | 
			
		||||
  const containerRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const sigmaRef = useRef<Sigma | null>(null);
 | 
			
		||||
  const graphRef = useRef<Graph | null>(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!containerRef.current) return;
 | 
			
		||||
 | 
			
		||||
    // 创建图实例
 | 
			
		||||
    const graph = new Graph();
 | 
			
		||||
    graphRef.current = graph;
 | 
			
		||||
 | 
			
		||||
    // 创建 Sigma 实例
 | 
			
		||||
    const sigma = new Sigma(graph, containerRef.current, {
 | 
			
		||||
      renderLabels: true,
 | 
			
		||||
      labelRenderedSizeThreshold: 8,
 | 
			
		||||
      labelDensity: 8,
 | 
			
		||||
    });
 | 
			
		||||
    sigmaRef.current = sigma;
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      sigma.kill();
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!graphRef.current || !dataSource.length) return;
 | 
			
		||||
 | 
			
		||||
    const graph = graphRef.current;
 | 
			
		||||
 | 
			
		||||
    // 清空现有图数据
 | 
			
		||||
    graph.clear();
 | 
			
		||||
 | 
			
		||||
    // 添加节点
 | 
			
		||||
    dataSource.forEach((mark, index) => {
 | 
			
		||||
      graph.addNode(`node-${index}`, {
 | 
			
		||||
        x: Math.random() * 800,
 | 
			
		||||
        y: Math.random() * 600,
 | 
			
		||||
        size: Math.random() * 20 + 5,
 | 
			
		||||
        label: mark.title || `Mark ${index}`,
 | 
			
		||||
        color: `#${Math.floor(Math.random() * 16777215).toString(16)}`,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // // 添加一些随机边(基于数据关系或随机生成)
 | 
			
		||||
    // for (let i = 0; i < Math.min(dataSource.length - 1, 20); i++) {
 | 
			
		||||
    //   const sourceIndex = Math.floor(Math.random() * dataSource.length);
 | 
			
		||||
    //   const targetIndex = Math.floor(Math.random() * dataSource.length);
 | 
			
		||||
 | 
			
		||||
    //   if (sourceIndex !== targetIndex) {
 | 
			
		||||
    //     try {
 | 
			
		||||
    //       graph.addEdge(`node-${sourceIndex}`, `node-${targetIndex}`, {
 | 
			
		||||
    //         size: Math.random() * 5 + 1,
 | 
			
		||||
    //         color: '#999',
 | 
			
		||||
    //       });
 | 
			
		||||
    //     } catch (error) {
 | 
			
		||||
    //       // 边已存在,忽略错误
 | 
			
		||||
    //     }
 | 
			
		||||
    //   }
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    // 刷新 Sigma 渲染
 | 
			
		||||
    if (sigmaRef.current) {
 | 
			
		||||
      sigmaRef.current.refresh();
 | 
			
		||||
    }
 | 
			
		||||
  }, [dataSource]);
 | 
			
		||||
 | 
			
		||||
  // 添加交互事件处理
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!sigmaRef.current) return;
 | 
			
		||||
 | 
			
		||||
    const sigma = sigmaRef.current;
 | 
			
		||||
 | 
			
		||||
    // 节点点击事件
 | 
			
		||||
    const handleNodeClick = (event: any) => {
 | 
			
		||||
      console.log('Node clicked:', event.node);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 节点悬停事件
 | 
			
		||||
    const handleNodeHover = (event: any) => {
 | 
			
		||||
      console.log('Node hovered:', event.node);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    sigma.on('clickNode', handleNodeClick);
 | 
			
		||||
    sigma.on('enterNode', handleNodeHover);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      sigma.off('clickNode', handleNodeClick);
 | 
			
		||||
      sigma.off('enterNode', handleNodeHover);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div style={{ width: '100%', height: '100vh', position: 'relative' }}>
 | 
			
		||||
      <div
 | 
			
		||||
        ref={containerRef}
 | 
			
		||||
        style={{
 | 
			
		||||
          width: '100%',
 | 
			
		||||
          height: '100%',
 | 
			
		||||
          background: '#f5f5f5'
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
      <div style={{
 | 
			
		||||
        position: 'absolute',
 | 
			
		||||
        top: 10,
 | 
			
		||||
        left: 10,
 | 
			
		||||
        background: 'rgba(255,255,255,0.8)',
 | 
			
		||||
        padding: '10px',
 | 
			
		||||
        borderRadius: '4px'
 | 
			
		||||
      }}>
 | 
			
		||||
        <p>节点数量: {dataSource.length}</p>
 | 
			
		||||
        <p>使用鼠标拖拽和缩放来探索图形</p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { Base } from "./table/index";
 | 
			
		||||
 | 
			
		||||
import { markService } from "../modules/mark-service";
 | 
			
		||||
import { SigmaGraph } from "./graph/sigma/index";
 | 
			
		||||
const tabs = [
 | 
			
		||||
  {
 | 
			
		||||
    key: 'table',
 | 
			
		||||
@@ -13,20 +14,31 @@ const tabs = [
 | 
			
		||||
  {
 | 
			
		||||
    key: 'world',
 | 
			
		||||
    title: '世界'
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 'docs',
 | 
			
		||||
    title: '文档'
 | 
			
		||||
  }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const BaseApp = () => {
 | 
			
		||||
  const [activeTab, setActiveTab] = useState('table');
 | 
			
		||||
 | 
			
		||||
  const [dataSource, setDataSource] = useState<any[]>([]);
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getMarks();
 | 
			
		||||
  }, []);
 | 
			
		||||
  const getMarks = async () => {
 | 
			
		||||
    const marks = await markService.getAllMarks();
 | 
			
		||||
    setDataSource(marks);
 | 
			
		||||
  }
 | 
			
		||||
  const renderContent = () => {
 | 
			
		||||
    switch (activeTab) {
 | 
			
		||||
      case 'table':
 | 
			
		||||
        return <Base />;
 | 
			
		||||
        return <Base dataSource={dataSource} />;
 | 
			
		||||
      case 'graph':
 | 
			
		||||
        return (
 | 
			
		||||
          <div className="flex items-center justify-center h-96 text-gray-500">
 | 
			
		||||
            关系图模块暂未实现
 | 
			
		||||
          <div className="w-full h-96">
 | 
			
		||||
            <SigmaGraph dataSource={dataSource} />
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      case 'world':
 | 
			
		||||
@@ -35,6 +47,12 @@ export const BaseApp = () => {
 | 
			
		||||
            世界模块暂未实现
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      case 'docs':
 | 
			
		||||
        return (
 | 
			
		||||
          <div className="flex items-center justify-center h-96 text-gray-500">
 | 
			
		||||
            文档模块暂未实现
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      default:
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
@@ -49,11 +67,10 @@ export const BaseApp = () => {
 | 
			
		||||
            <button
 | 
			
		||||
              key={tab.key}
 | 
			
		||||
              onClick={() => setActiveTab(tab.key)}
 | 
			
		||||
              className={`py-2 px-1 border-b-2 font-medium text-sm ${
 | 
			
		||||
                activeTab === tab.key
 | 
			
		||||
                  ? 'border-blue-500 text-blue-600'
 | 
			
		||||
                  : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
 | 
			
		||||
              }`}
 | 
			
		||||
              className={`py-2 px-1 border-b-2 font-medium text-sm ${activeTab === tab.key
 | 
			
		||||
                ? 'border-blue-500 text-blue-600'
 | 
			
		||||
                : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
 | 
			
		||||
                }`}
 | 
			
		||||
            >
 | 
			
		||||
              {tab.title}
 | 
			
		||||
            </button>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,87 @@ import { nanoid, customAlphabet } from 'nanoid';
 | 
			
		||||
 | 
			
		||||
export const random = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
 | 
			
		||||
 | 
			
		||||
// 类型定义
 | 
			
		||||
// 确保类型定义
 | 
			
		||||
const ensureType = ['markdown', 'json', 'html', 'image', 'video', 'audio', 'code', 'link', 'file'];
 | 
			
		||||
export type MarkEnsureType = typeof ensureType[number];
 | 
			
		||||
 | 
			
		||||
// 根据新的类型定义
 | 
			
		||||
export type Mark<T = any> = {
 | 
			
		||||
  /**
 | 
			
		||||
   * 标记ID
 | 
			
		||||
   */
 | 
			
		||||
  id: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 标题
 | 
			
		||||
   */
 | 
			
		||||
  title?: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 描述
 | 
			
		||||
   */
 | 
			
		||||
  description?: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 标签
 | 
			
		||||
   */
 | 
			
		||||
  tags?: string[];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 标记类型
 | 
			
		||||
   */
 | 
			
		||||
  markType?: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 封面
 | 
			
		||||
   */
 | 
			
		||||
  cover?: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 链接
 | 
			
		||||
   */
 | 
			
		||||
  link?: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 摘要
 | 
			
		||||
   */
 | 
			
		||||
  summary?: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 键
 | 
			
		||||
   */
 | 
			
		||||
  key?: string;
 | 
			
		||||
  data: T;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 附件列表
 | 
			
		||||
   */
 | 
			
		||||
  fileList?: any[];
 | 
			
		||||
  /**
 | 
			
		||||
   * 创建人信息
 | 
			
		||||
   */
 | 
			
		||||
  uname?: string;
 | 
			
		||||
  /**
 | 
			
		||||
   * 版本号
 | 
			
		||||
   */
 | 
			
		||||
  version?: number;
 | 
			
		||||
  /**
 | 
			
		||||
   * 创建时间
 | 
			
		||||
   */
 | 
			
		||||
  createdAt: Date;
 | 
			
		||||
  /**
 | 
			
		||||
   * 更新时间
 | 
			
		||||
   */
 | 
			
		||||
  updatedAt: Date;
 | 
			
		||||
  /**
 | 
			
		||||
   * 标记时间
 | 
			
		||||
   */
 | 
			
		||||
  markedAt?: Date;
 | 
			
		||||
  uid?: string;
 | 
			
		||||
  puid?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 保留原有的辅助类型
 | 
			
		||||
export type MarkDataNode = {
 | 
			
		||||
  id?: string;
 | 
			
		||||
  content?: string;
 | 
			
		||||
@@ -20,64 +100,25 @@ export type MarkFile = {
 | 
			
		||||
  name: string;
 | 
			
		||||
  url: string;
 | 
			
		||||
  size: number;
 | 
			
		||||
  type: 'self' | 'data' | 'generate'; // generate为生成文件
 | 
			
		||||
  query: string; // 'data.nodes[id].content';
 | 
			
		||||
  type: 'self' | 'data' | 'generate';
 | 
			
		||||
  query: string;
 | 
			
		||||
  hash: string;
 | 
			
		||||
  fileKey: string; // 文件的名称, 唯一
 | 
			
		||||
  fileKey: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type MarkData = {
 | 
			
		||||
  md?: string; // markdown
 | 
			
		||||
  mdList?: string[]; // markdown list
 | 
			
		||||
  type?: string; // 类型 markdown | json | html | image | video | audio | code | link | file
 | 
			
		||||
  md?: string;
 | 
			
		||||
  mdList?: string[];
 | 
			
		||||
  type?: MarkEnsureType;
 | 
			
		||||
  data?: any;
 | 
			
		||||
  key?: string; // 文件的名称, 唯一
 | 
			
		||||
  push?: boolean; // 是否推送到elasticsearch
 | 
			
		||||
  pushTime?: Date; // 推送时间
 | 
			
		||||
  summary?: string; // 摘要
 | 
			
		||||
  nodes?: MarkDataNode[]; // 节点
 | 
			
		||||
  key?: string;
 | 
			
		||||
  push?: boolean;
 | 
			
		||||
  pushTime?: Date;
 | 
			
		||||
  summary?: string;
 | 
			
		||||
  nodes?: MarkDataNode[];
 | 
			
		||||
  [key: string]: any;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type MarkConfig = {
 | 
			
		||||
  visibility?: 'public' | 'private' | 'restricted';
 | 
			
		||||
  allowComments?: boolean;
 | 
			
		||||
  allowDownload?: boolean;
 | 
			
		||||
  password?: string;
 | 
			
		||||
  expiredAt?: Date;
 | 
			
		||||
  [key: string]: any;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type MarkAuth = {
 | 
			
		||||
  permissions?: string[];
 | 
			
		||||
  roles?: string[];
 | 
			
		||||
  userId?: string;
 | 
			
		||||
  [key: string]: any;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type Mark = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  title: string;
 | 
			
		||||
  description: string;
 | 
			
		||||
  cover: string;
 | 
			
		||||
  thumbnail: string;
 | 
			
		||||
  key: string;
 | 
			
		||||
  markType: string;
 | 
			
		||||
  link: string;
 | 
			
		||||
  tags: string[];
 | 
			
		||||
  summary: string;
 | 
			
		||||
  data: MarkData;
 | 
			
		||||
  uid: string;
 | 
			
		||||
  puid: string;
 | 
			
		||||
  config: MarkConfig;
 | 
			
		||||
  fileList: MarkFile[];
 | 
			
		||||
  uname: string;
 | 
			
		||||
  markedAt: Date;
 | 
			
		||||
  createdAt: Date;
 | 
			
		||||
  updatedAt: Date;
 | 
			
		||||
  version: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 生成模拟的 MarkDataNode
 | 
			
		||||
const generateMarkDataNode = (): MarkDataNode => ({
 | 
			
		||||
  id: random(12),
 | 
			
		||||
@@ -99,85 +140,187 @@ const generateMarkDataNode = (): MarkDataNode => ({
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 生成模拟的 MarkFile
 | 
			
		||||
const generateMarkFile = (): MarkFile => ({
 | 
			
		||||
// 生成模拟的附件
 | 
			
		||||
const generateFileAttachment = (): any => ({
 | 
			
		||||
  id: faker.string.uuid(),
 | 
			
		||||
  name: faker.system.fileName(),
 | 
			
		||||
  url: faker.internet.url(),
 | 
			
		||||
  size: faker.number.int({ min: 1024, max: 50 * 1024 * 1024 }), // 1KB to 50MB
 | 
			
		||||
  type: faker.helpers.arrayElement(['self', 'data', 'generate']),
 | 
			
		||||
  query: `data.nodes[${random(12)}].content`,
 | 
			
		||||
  size: faker.number.int({ min: 1024, max: 50 * 1024 * 1024 }),
 | 
			
		||||
  type: faker.helpers.arrayElement(['image', 'document', 'video', 'audio']),
 | 
			
		||||
  mimeType: faker.system.mimeType(),
 | 
			
		||||
  hash: faker.git.commitSha(),
 | 
			
		||||
  fileKey: faker.system.fileName()
 | 
			
		||||
  uploadedAt: faker.date.recent()
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 生成模拟的 MarkData
 | 
			
		||||
const generateMarkData = (): MarkData => ({
 | 
			
		||||
  md: faker.lorem.paragraphs(3, '\n\n'),
 | 
			
		||||
  mdList: Array.from({ length: faker.number.int({ min: 3, max: 8 }) }, () => faker.lorem.sentence()),
 | 
			
		||||
  type: faker.helpers.arrayElement(['markdown', 'json', 'html', 'image', 'video', 'audio', 'code', 'link', 'file']),
 | 
			
		||||
  data: {
 | 
			
		||||
    author: faker.person.fullName(),
 | 
			
		||||
    category: faker.helpers.arrayElement(['技术', '生活', '工作', '学习', '思考']),
 | 
			
		||||
    priority: faker.helpers.arrayElement(['low', 'medium', 'high'])
 | 
			
		||||
  },
 | 
			
		||||
  key: faker.system.fileName(),
 | 
			
		||||
  push: faker.datatype.boolean(),
 | 
			
		||||
  pushTime: faker.date.recent(),
 | 
			
		||||
  summary: faker.lorem.paragraph(),
 | 
			
		||||
  nodes: Array.from({ length: faker.number.int({ min: 2, max: 6 }) }, generateMarkDataNode)
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 生成模拟的 MarkConfig
 | 
			
		||||
const generateMarkConfig = (): MarkConfig => ({
 | 
			
		||||
  visibility: faker.helpers.arrayElement(['public', 'private', 'restricted']),
 | 
			
		||||
  allowComments: faker.datatype.boolean(),
 | 
			
		||||
  allowDownload: faker.datatype.boolean(),
 | 
			
		||||
  password: faker.datatype.boolean() ? faker.internet.password() : undefined,
 | 
			
		||||
  expiredAt: faker.datatype.boolean() ? faker.date.future() : undefined,
 | 
			
		||||
  theme: faker.helpers.arrayElement(['light', 'dark', 'auto']),
 | 
			
		||||
  language: faker.helpers.arrayElement(['zh-CN', 'en-US', 'ja-JP'])
 | 
			
		||||
});
 | 
			
		||||
// 生成模拟的 MarkData(通用数据类型)
 | 
			
		||||
const generateMarkData = <T = any>(): T => {
 | 
			
		||||
  const dataVariants = [
 | 
			
		||||
    // Markdown 数据
 | 
			
		||||
    {
 | 
			
		||||
      content: faker.lorem.paragraphs(3, '\n\n'),
 | 
			
		||||
      format: 'markdown',
 | 
			
		||||
      wordCount: faker.number.int({ min: 100, max: 1000 })
 | 
			
		||||
    },
 | 
			
		||||
    // JSON 数据
 | 
			
		||||
    {
 | 
			
		||||
      jsonData: {
 | 
			
		||||
        name: faker.person.fullName(),
 | 
			
		||||
        email: faker.internet.email(),
 | 
			
		||||
        settings: {
 | 
			
		||||
          theme: faker.helpers.arrayElement(['light', 'dark']),
 | 
			
		||||
          language: faker.helpers.arrayElement(['zh-CN', 'en-US', 'ja-JP'])
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      schema: 'user-profile'
 | 
			
		||||
    },
 | 
			
		||||
    // 图片数据
 | 
			
		||||
    {
 | 
			
		||||
      src: faker.image.url(),
 | 
			
		||||
      alt: faker.lorem.sentence(),
 | 
			
		||||
      width: faker.number.int({ min: 400, max: 1920 }),
 | 
			
		||||
      height: faker.number.int({ min: 300, max: 1080 }),
 | 
			
		||||
      format: faker.helpers.arrayElement(['jpg', 'png', 'webp'])
 | 
			
		||||
    },
 | 
			
		||||
    // 代码数据
 | 
			
		||||
    {
 | 
			
		||||
      code: `function ${faker.hacker.noun()}() {\n  return "${faker.hacker.phrase()}";\n}`,
 | 
			
		||||
      language: faker.helpers.arrayElement(['javascript', 'typescript', 'python', 'java']),
 | 
			
		||||
      lineCount: faker.number.int({ min: 10, max: 100 })
 | 
			
		||||
    },
 | 
			
		||||
    // 链接数据
 | 
			
		||||
    {
 | 
			
		||||
      url: faker.internet.url(),
 | 
			
		||||
      title: faker.lorem.sentence(),
 | 
			
		||||
      description: faker.lorem.paragraph(),
 | 
			
		||||
      favicon: faker.image.url({ width: 32, height: 32 })
 | 
			
		||||
    }
 | 
			
		||||
  ];
 | 
			
		||||
  
 | 
			
		||||
  return faker.helpers.arrayElement(dataVariants) as T;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 生成单个 Mark 记录
 | 
			
		||||
const generateMark = (): Mark => {
 | 
			
		||||
  const markType = faker.helpers.arrayElement(['markdown', 'json', 'html', 'image', 'video', 'audio', 'code', 'link', 'file']);
 | 
			
		||||
  const title = faker.lorem.sentence({ min: 3, max: 8 });
 | 
			
		||||
const generateMark = <T = any>(): Mark<T> => {
 | 
			
		||||
  const createdAt = faker.date.past();
 | 
			
		||||
  const updatedAt = faker.date.between({ from: createdAt, to: new Date() });
 | 
			
		||||
  
 | 
			
		||||
  return {
 | 
			
		||||
    id: faker.string.uuid(),
 | 
			
		||||
    title,
 | 
			
		||||
    description: faker.lorem.paragraph(),
 | 
			
		||||
    cover: faker.image.url({ width: 800, height: 600 }),
 | 
			
		||||
    thumbnail: faker.image.url({ width: 200, height: 150 }),
 | 
			
		||||
    key: faker.system.filePath(),
 | 
			
		||||
    markType,
 | 
			
		||||
    link: faker.internet.url(),
 | 
			
		||||
    tags: Array.from({ length: faker.number.int({ min: 1, max: 5 }) }, () => 
 | 
			
		||||
      faker.helpers.arrayElement(['技术', '前端', '后端', '设计', 'AI', '工具', '教程', '笔记'])
 | 
			
		||||
    ),
 | 
			
		||||
    summary: faker.lorem.sentence(),
 | 
			
		||||
    data: generateMarkData(),
 | 
			
		||||
    uid: faker.string.uuid(),
 | 
			
		||||
    puid: faker.string.uuid(),
 | 
			
		||||
    config: generateMarkConfig(),
 | 
			
		||||
    fileList: Array.from({ length: faker.number.int({ min: 0, max: 4 }) }, generateMarkFile),
 | 
			
		||||
    uname: faker.person.fullName(),
 | 
			
		||||
    markedAt: faker.date.past(),
 | 
			
		||||
    createdAt: faker.date.past(),
 | 
			
		||||
    updatedAt: faker.date.recent(),
 | 
			
		||||
    version: faker.number.int({ min: 1, max: 10 })
 | 
			
		||||
    title: faker.datatype.boolean() ? faker.lorem.sentence({ min: 3, max: 8 }) : undefined,
 | 
			
		||||
    description: faker.datatype.boolean() ? faker.lorem.paragraph() : undefined,
 | 
			
		||||
    tags: faker.datatype.boolean() 
 | 
			
		||||
      ? Array.from({ length: faker.number.int({ min: 1, max: 5 }) }, () => 
 | 
			
		||||
          faker.helpers.arrayElement(['技术', '前端', '后端', '设计', 'AI', '工具', '教程', '笔记', '生活', '工作'])
 | 
			
		||||
        ) 
 | 
			
		||||
      : undefined,
 | 
			
		||||
    markType: faker.datatype.boolean() 
 | 
			
		||||
      ? faker.helpers.arrayElement(ensureType) 
 | 
			
		||||
      : undefined,
 | 
			
		||||
    cover: faker.datatype.boolean() 
 | 
			
		||||
      ? faker.image.url({ width: 800, height: 600 }) 
 | 
			
		||||
      : undefined,
 | 
			
		||||
    link: faker.datatype.boolean() 
 | 
			
		||||
      ? faker.internet.url() 
 | 
			
		||||
      : undefined,
 | 
			
		||||
    summary: faker.datatype.boolean() 
 | 
			
		||||
      ? faker.lorem.sentence() 
 | 
			
		||||
      : undefined,
 | 
			
		||||
    key: faker.datatype.boolean() 
 | 
			
		||||
      ? faker.system.filePath() 
 | 
			
		||||
      : undefined,
 | 
			
		||||
    data: generateMarkData<T>(),
 | 
			
		||||
    fileList: faker.datatype.boolean() 
 | 
			
		||||
      ? Array.from({ length: faker.number.int({ min: 1, max: 4 }) }, generateFileAttachment)
 | 
			
		||||
      : undefined,
 | 
			
		||||
    uname: faker.datatype.boolean() 
 | 
			
		||||
      ? faker.person.fullName() 
 | 
			
		||||
      : undefined,
 | 
			
		||||
    version: faker.datatype.boolean() 
 | 
			
		||||
      ? faker.number.int({ min: 1, max: 10 }) 
 | 
			
		||||
      : undefined,
 | 
			
		||||
    createdAt,
 | 
			
		||||
    updatedAt,
 | 
			
		||||
    markedAt: faker.datatype.boolean() 
 | 
			
		||||
      ? faker.date.between({ from: createdAt, to: updatedAt }) 
 | 
			
		||||
      : undefined,
 | 
			
		||||
    uid: faker.datatype.boolean() 
 | 
			
		||||
      ? faker.string.uuid() 
 | 
			
		||||
      : undefined,
 | 
			
		||||
    puid: faker.datatype.boolean() 
 | 
			
		||||
      ? faker.string.uuid() 
 | 
			
		||||
      : undefined
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 生成指定类型的 Mark 记录
 | 
			
		||||
const generateMarkWithType = (markType: MarkEnsureType): Mark => {
 | 
			
		||||
  const mark = generateMark();
 | 
			
		||||
  mark.markType = markType;
 | 
			
		||||
  
 | 
			
		||||
  // 根据类型生成相应的数据
 | 
			
		||||
  switch (markType) {
 | 
			
		||||
    case 'markdown':
 | 
			
		||||
      mark.data = {
 | 
			
		||||
        content: faker.lorem.paragraphs(faker.number.int({ min: 2, max: 5 }), '\n\n'),
 | 
			
		||||
        format: 'markdown',
 | 
			
		||||
        wordCount: faker.number.int({ min: 100, max: 1000 })
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    case 'json':
 | 
			
		||||
      mark.data = {
 | 
			
		||||
        jsonData: {
 | 
			
		||||
          id: faker.string.uuid(),
 | 
			
		||||
          name: faker.person.fullName(),
 | 
			
		||||
          settings: {
 | 
			
		||||
            theme: faker.helpers.arrayElement(['light', 'dark']),
 | 
			
		||||
            notifications: faker.datatype.boolean()
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    case 'image':
 | 
			
		||||
      mark.data = {
 | 
			
		||||
        src: faker.image.url(),
 | 
			
		||||
        alt: faker.lorem.sentence(),
 | 
			
		||||
        width: faker.number.int({ min: 400, max: 1920 }),
 | 
			
		||||
        height: faker.number.int({ min: 300, max: 1080 })
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    case 'video':
 | 
			
		||||
      mark.data = {
 | 
			
		||||
        src: faker.internet.url() + '/video.mp4',
 | 
			
		||||
        duration: faker.number.int({ min: 30, max: 3600 }),
 | 
			
		||||
        resolution: faker.helpers.arrayElement(['720p', '1080p', '4K'])
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    case 'code':
 | 
			
		||||
      mark.data = {
 | 
			
		||||
        code: `function ${faker.hacker.noun()}() {\n  return "${faker.hacker.phrase()}";\n}`,
 | 
			
		||||
        language: faker.helpers.arrayElement(['javascript', 'typescript', 'python', 'java']),
 | 
			
		||||
        lineCount: faker.number.int({ min: 10, max: 100 })
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      mark.data = generateMarkData();
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return mark;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 生成 20 条模拟数据
 | 
			
		||||
export const mockMarks: Mark[] = Array.from({ length: 20 }, generateMark);
 | 
			
		||||
export const mockMarks: Mark[] = Array.from({ length: 20 }, () => generateMark());
 | 
			
		||||
 | 
			
		||||
// 生成每种类型的示例数据
 | 
			
		||||
export const mockMarksByType: Record<MarkEnsureType, Mark> = ensureType.reduce((acc, type) => {
 | 
			
		||||
  acc[type] = generateMarkWithType(type);
 | 
			
		||||
  return acc;
 | 
			
		||||
}, {} as Record<MarkEnsureType, Mark>);
 | 
			
		||||
 | 
			
		||||
// 导出生成器函数
 | 
			
		||||
export {
 | 
			
		||||
  generateMark,
 | 
			
		||||
  generateMarkWithType,
 | 
			
		||||
  generateMarkData,
 | 
			
		||||
  generateMarkFile,
 | 
			
		||||
  generateMarkDataNode,
 | 
			
		||||
  generateMarkConfig
 | 
			
		||||
  generateFileAttachment
 | 
			
		||||
};
 | 
			
		||||
@@ -1,17 +1,26 @@
 | 
			
		||||
import React, { useState, useMemo } from 'react';
 | 
			
		||||
import React, { useState, useMemo, useEffect } from 'react';
 | 
			
		||||
import { Table } from './Table';
 | 
			
		||||
import { DetailModal } from './DetailModal';
 | 
			
		||||
import { mockMarks, Mark } from '../mock/collection';
 | 
			
		||||
import { Mark } from '../mock/collection';
 | 
			
		||||
import { TableColumn, ActionButton } from './types';
 | 
			
		||||
 | 
			
		||||
export const Base = () => {
 | 
			
		||||
type Props = {
 | 
			
		||||
  dataSource?: Mark[];
 | 
			
		||||
}
 | 
			
		||||
export const Base = (props: Props) => {
 | 
			
		||||
  const { dataSource = [] } = props;
 | 
			
		||||
  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
 | 
			
		||||
  const [currentPage, setCurrentPage] = useState(1);
 | 
			
		||||
  const [pageSize, setPageSize] = useState(10);
 | 
			
		||||
  const [data, setData] = useState<Mark[]>(mockMarks);
 | 
			
		||||
  const [data, setData] = useState<Mark[]>([]);
 | 
			
		||||
  const [detailModalVisible, setDetailModalVisible] = useState(false);
 | 
			
		||||
  const [currentRecord, setCurrentRecord] = useState<Mark | null>(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (dataSource) {
 | 
			
		||||
      setData(dataSource);
 | 
			
		||||
    }
 | 
			
		||||
  }, [dataSource]);
 | 
			
		||||
  // 表格列配置
 | 
			
		||||
  const columns: TableColumn<Mark>[] = [
 | 
			
		||||
    {
 | 
			
		||||
@@ -26,7 +35,7 @@ export const Base = () => {
 | 
			
		||||
            {value}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div style={{ fontSize: '12px', color: '#666' }}>
 | 
			
		||||
            {record.description.slice(0, 60)}...
 | 
			
		||||
            {record.description?.slice?.(0, 60)}...
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      )
 | 
			
		||||
@@ -37,28 +46,31 @@ export const Base = () => {
 | 
			
		||||
      dataIndex: 'markType',
 | 
			
		||||
      width: 100,
 | 
			
		||||
      sortable: true,
 | 
			
		||||
      render: (value: string) => (
 | 
			
		||||
        <span 
 | 
			
		||||
          style={{
 | 
			
		||||
            padding: '4px 8px',
 | 
			
		||||
            borderRadius: '4px',
 | 
			
		||||
            backgroundColor: getTypeColor(value),
 | 
			
		||||
            color: '#fff',
 | 
			
		||||
            fontSize: '12px'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {value}
 | 
			
		||||
        </span>
 | 
			
		||||
      )
 | 
			
		||||
      render: (value: string) => {
 | 
			
		||||
        if (!value) return ''
 | 
			
		||||
        return (
 | 
			
		||||
          <span
 | 
			
		||||
            style={{
 | 
			
		||||
              padding: '4px 8px',
 | 
			
		||||
              borderRadius: '4px',
 | 
			
		||||
              backgroundColor: getTypeColor(value),
 | 
			
		||||
              color: '#fff',
 | 
			
		||||
              fontSize: '12px'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {value}
 | 
			
		||||
          </span>
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      key: 'tags',
 | 
			
		||||
      title: '标签',
 | 
			
		||||
      dataIndex: 'tags',
 | 
			
		||||
      width: 200,
 | 
			
		||||
      render: (tags: string[]) => (
 | 
			
		||||
      render: (tags: string[] = []) => (
 | 
			
		||||
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
 | 
			
		||||
          {tags.slice(0, 3).map((tag, index) => (
 | 
			
		||||
          {tags?.slice?.(0, 3).map((tag, index) => (
 | 
			
		||||
            <span
 | 
			
		||||
              key={index}
 | 
			
		||||
              style={{
 | 
			
		||||
@@ -96,13 +108,13 @@ export const Base = () => {
 | 
			
		||||
      render: (value: Date) => new Date(value).toLocaleString('zh-CN')
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      key: 'config.visibility',
 | 
			
		||||
      key: 'data.visibility',
 | 
			
		||||
      title: '可见性',
 | 
			
		||||
      dataIndex: 'config.visibility',
 | 
			
		||||
      dataIndex: 'data.visibility',
 | 
			
		||||
      width: 100,
 | 
			
		||||
      render: (value: string) => (
 | 
			
		||||
        <span style={{ 
 | 
			
		||||
          color: value === 'public' ? '#52c41a' : value === 'private' ? '#ff4d4f' : '#faad14' 
 | 
			
		||||
        <span style={{
 | 
			
		||||
          color: value === 'public' ? '#52c41a' : value === 'private' ? '#ff4d4f' : '#faad14'
 | 
			
		||||
        }}>
 | 
			
		||||
          {value === 'public' ? '公开' : value === 'private' ? '私有' : '受限'}
 | 
			
		||||
        </span>
 | 
			
		||||
@@ -180,7 +192,7 @@ export const Base = () => {
 | 
			
		||||
  // 处理批量删除
 | 
			
		||||
  const handleBatchDelete = () => {
 | 
			
		||||
    if (selectedRowKeys.length === 0) return;
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    if (window.confirm(`确定要删除选中的 ${selectedRowKeys.length} 项吗?`)) {
 | 
			
		||||
      setData(prevData => prevData.filter(item => !selectedRowKeys.includes(item.id)));
 | 
			
		||||
      setSelectedRowKeys([]);
 | 
			
		||||
@@ -190,7 +202,7 @@ export const Base = () => {
 | 
			
		||||
  // 排序处理
 | 
			
		||||
  const handleSort = (field: string, order: 'asc' | 'desc' | null) => {
 | 
			
		||||
    if (!order) {
 | 
			
		||||
      setData(mockMarks); // 重置为原始顺序
 | 
			
		||||
      setData(dataSource); // 重置为原始顺序
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -198,10 +210,10 @@ export const Base = () => {
 | 
			
		||||
      const getNestedValue = (obj: any, path: string) => {
 | 
			
		||||
        return path.split('.').reduce((o, p) => o?.[p], obj);
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
      const aVal = getNestedValue(a, field);
 | 
			
		||||
      const bVal = getNestedValue(b, field);
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
      if (aVal < bVal) return order === 'asc' ? -1 : 1;
 | 
			
		||||
      if (aVal > bVal) return order === 'asc' ? 1 : -1;
 | 
			
		||||
      return 0;
 | 
			
		||||
@@ -217,7 +229,7 @@ export const Base = () => {
 | 
			
		||||
    total: data.length,
 | 
			
		||||
    showSizeChanger: true,
 | 
			
		||||
    showQuickJumper: true,
 | 
			
		||||
    showTotal: (total: number, range: [number, number]) => 
 | 
			
		||||
    showTotal: (total: number, range: [number, number]) =>
 | 
			
		||||
      `第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
 | 
			
		||||
    onChange: (page: number, size: number) => {
 | 
			
		||||
      setCurrentPage(page);
 | 
			
		||||
@@ -235,17 +247,17 @@ export const Base = () => {
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {selectedRowKeys.length > 0 && (
 | 
			
		||||
        <div style={{ 
 | 
			
		||||
          marginBottom: '16px', 
 | 
			
		||||
          padding: '12px', 
 | 
			
		||||
          backgroundColor: '#e6f7ff', 
 | 
			
		||||
        <div style={{
 | 
			
		||||
          marginBottom: '16px',
 | 
			
		||||
          padding: '12px',
 | 
			
		||||
          backgroundColor: '#e6f7ff',
 | 
			
		||||
          borderRadius: '4px',
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          justifyContent: 'space-between',
 | 
			
		||||
          alignItems: 'center'
 | 
			
		||||
        }}>
 | 
			
		||||
          <span>已选择 {selectedRowKeys.length} 项</span>
 | 
			
		||||
          <button 
 | 
			
		||||
          <button
 | 
			
		||||
            className="btn btn-danger"
 | 
			
		||||
            onClick={handleBatchDelete}
 | 
			
		||||
          >
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import { useState } from 'react';
 | 
			
		||||
import { VadVoice } from './videos/modules/VadVoice.tsx';
 | 
			
		||||
import { ChatInterface } from './prompts/index.tsx';
 | 
			
		||||
import { BaseApp } from './base/index.tsx';
 | 
			
		||||
import { exampleUsage } from './modules/mark-service.ts';
 | 
			
		||||
 | 
			
		||||
const LeftPanel = () => {
 | 
			
		||||
  return (
 | 
			
		||||
@@ -79,6 +80,16 @@ export const MuseApp = () => {
 | 
			
		||||
          >
 | 
			
		||||
            Right Panel
 | 
			
		||||
          </button>
 | 
			
		||||
          <button className="px-3 py-1 rounded text-sm bg-green-500 text-white hover:bg-green-600" onClick={() => {
 | 
			
		||||
            exampleUsage()
 | 
			
		||||
          }}>
 | 
			
		||||
            初始化DB
 | 
			
		||||
          </button>
 | 
			
		||||
          <button className="px-3 py-1 rounded text-sm bg-red-500 text-white hover:bg-red-600" onClick={() => {
 | 
			
		||||
            // 删除DB的逻辑
 | 
			
		||||
          }}>
 | 
			
		||||
            删除DB
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										398
									
								
								web/src/apps/muse/modules/db.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										398
									
								
								web/src/apps/muse/modules/db.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,398 @@
 | 
			
		||||
import PouchDB from 'pouchdb-browser';
 | 
			
		||||
import { Mark, MarkEnsureType } from './mark';
 | 
			
		||||
 | 
			
		||||
console.log('PouchDB version:', PouchDB.version);
 | 
			
		||||
// 扩展 Mark 类型以包含 PouchDB 特有字段
 | 
			
		||||
export type MarkDocument = Mark & {
 | 
			
		||||
  _id: string;
 | 
			
		||||
  _rev?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 创建或获取数据库实例
 | 
			
		||||
export const createDB = (name: string = 'marks_db', opts?: { adapter?: string }) => {
 | 
			
		||||
  return new PouchDB(name);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 辅助函数:将 PouchDB 文档转换为 Mark 对象
 | 
			
		||||
const docToMark = (doc: any): Mark => {
 | 
			
		||||
  const { _id, _rev, ...mark } = doc as MarkDocument;
 | 
			
		||||
  return mark;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Mark 数据库操作类
 | 
			
		||||
export class MarkDB {
 | 
			
		||||
  private db: PouchDB.Database;
 | 
			
		||||
 | 
			
		||||
  constructor(dbName: string = 'marks_db') {
 | 
			
		||||
    this.db = createDB(dbName);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 创建索引以支持查询
 | 
			
		||||
  async createIndexes() {
 | 
			
		||||
    if (!this.db) {
 | 
			
		||||
      throw new Error('数据库未初始化');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      // PouchDB 创建索引的正确方式
 | 
			
		||||
      const indexes = [
 | 
			
		||||
        { index: { fields: ['uid'] } },
 | 
			
		||||
        { index: { fields: ['markType'] } },
 | 
			
		||||
        { index: { fields: ['tags'] } },
 | 
			
		||||
        { index: { fields: ['createdAt'] } },
 | 
			
		||||
        { index: { fields: ['title'] } },
 | 
			
		||||
        { index: { fields: ['uid', 'markType'] } },
 | 
			
		||||
        { index: { fields: ['createdAt', 'uid'] } }
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
      const results = await Promise.allSettled(
 | 
			
		||||
        indexes.map(indexDef => this.db.createIndex(indexDef))
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // 检查索引创建结果
 | 
			
		||||
      results.forEach((result, index) => {
 | 
			
		||||
        if (result.status === 'fulfilled') {
 | 
			
		||||
          console.log(`索引 ${index + 1} 创建成功:`, result.value);
 | 
			
		||||
        } else {
 | 
			
		||||
          // 如果索引已存在,PouchDB 会返回错误,这是正常的
 | 
			
		||||
          if (result.reason?.status !== 409) {
 | 
			
		||||
            console.warn(`索引 ${index + 1} 创建失败:`, result.reason);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      console.log('索引初始化完成');
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('创建索引失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 创建 Mark
 | 
			
		||||
  async create(mark: Omit<Mark, 'id' | 'createdAt' | 'updatedAt'>): Promise<Mark> {
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
    const newMark: Mark = {
 | 
			
		||||
      ...mark,
 | 
			
		||||
      id: this.generateId(),
 | 
			
		||||
      createdAt: now,
 | 
			
		||||
      updatedAt: now
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const doc: MarkDocument = {
 | 
			
		||||
        ...newMark,
 | 
			
		||||
        _id: newMark.id
 | 
			
		||||
      };
 | 
			
		||||
      const response = await this.db.put(doc);
 | 
			
		||||
      return { ...newMark };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('创建 Mark 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 根据 ID 获取 Mark
 | 
			
		||||
  async getById(id: string): Promise<Mark | null> {
 | 
			
		||||
    try {
 | 
			
		||||
      const doc = await this.db.get(id);
 | 
			
		||||
      return docToMark(doc);
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      if (error.status === 404) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
      console.error('获取 Mark 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 获取所有 Marks
 | 
			
		||||
  async getAll(): Promise<Mark[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await this.db.allDocs({
 | 
			
		||||
        include_docs: true,
 | 
			
		||||
        attachments: false
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return result.rows.map(row => docToMark(row.doc));
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('获取所有 Marks 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 按用户 ID 获取 Marks
 | 
			
		||||
  async getByUserId(uid: string): Promise<Mark[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await this.db.find({
 | 
			
		||||
        selector: {
 | 
			
		||||
          uid: uid
 | 
			
		||||
        },
 | 
			
		||||
        sort: [{ createdAt: 'desc' }]
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return result.docs.map(doc => docToMark(doc));
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('按用户获取 Marks 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 按类型获取 Marks
 | 
			
		||||
  async getByType(markType: MarkEnsureType): Promise<Mark[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await this.db.find({
 | 
			
		||||
        selector: {
 | 
			
		||||
          markType: markType
 | 
			
		||||
        },
 | 
			
		||||
        sort: [{ createdAt: 'desc' }]
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return result.docs.map(doc => docToMark(doc));
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('按类型获取 Marks 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 按标签搜索 Marks
 | 
			
		||||
  async getByTag(tag: string): Promise<Mark[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await this.db.find({
 | 
			
		||||
        selector: {
 | 
			
		||||
          tags: { $elemMatch: { $eq: tag } }
 | 
			
		||||
        },
 | 
			
		||||
        sort: [{ createdAt: 'desc' }]
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return result.docs.map(doc => docToMark(doc));
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('按标签获取 Marks 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 搜索 Marks(按标题或描述)
 | 
			
		||||
  async search(query: string): Promise<Mark[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await this.db.find({
 | 
			
		||||
        selector: {
 | 
			
		||||
          $or: [
 | 
			
		||||
            { title: { $regex: query, $options: 'i' } },
 | 
			
		||||
            { description: { $regex: query, $options: 'i' } },
 | 
			
		||||
            { summary: { $regex: query, $options: 'i' } }
 | 
			
		||||
          ]
 | 
			
		||||
        },
 | 
			
		||||
        sort: [{ createdAt: 'desc' }]
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return result.docs.map(doc => docToMark(doc));
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('搜索 Marks 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 更新 Mark
 | 
			
		||||
  async update(id: string, updates: Partial<Omit<Mark, 'id' | 'createdAt'>>): Promise<Mark> {
 | 
			
		||||
    try {
 | 
			
		||||
      const existingDoc = await this.db.get(id);
 | 
			
		||||
      const existingMark = docToMark(existingDoc);
 | 
			
		||||
      const updatedMark: Mark = {
 | 
			
		||||
        ...existingMark,
 | 
			
		||||
        ...updates,
 | 
			
		||||
        updatedAt: new Date()
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const doc: MarkDocument = {
 | 
			
		||||
        ...updatedMark,
 | 
			
		||||
        _id: id,
 | 
			
		||||
        _rev: existingDoc._rev
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      await this.db.put(doc);
 | 
			
		||||
      return updatedMark;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('更新 Mark 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 删除 Mark
 | 
			
		||||
  async delete(id: string): Promise<boolean> {
 | 
			
		||||
    try {
 | 
			
		||||
      const doc = await this.db.get(id);
 | 
			
		||||
      await this.db.remove(doc);
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('删除 Mark 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 批量删除 Marks
 | 
			
		||||
  async deleteMultiple(ids: string[]): Promise<boolean> {
 | 
			
		||||
    try {
 | 
			
		||||
      const docs = await Promise.all(ids.map(id => this.db.get(id)));
 | 
			
		||||
      const responses = await Promise.all(
 | 
			
		||||
        docs.map(doc => this.db.remove(doc))
 | 
			
		||||
      );
 | 
			
		||||
      return responses.every(response => response.ok);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('批量删除 Marks 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 分页获取 Marks
 | 
			
		||||
  async getPaginated(page: number = 1, limit: number = 10, filters?: {
 | 
			
		||||
    uid?: string;
 | 
			
		||||
    markType?: MarkEnsureType;
 | 
			
		||||
    tags?: string[];
 | 
			
		||||
  }): Promise<{
 | 
			
		||||
    marks: Mark[];
 | 
			
		||||
    total: number;
 | 
			
		||||
    page: number;
 | 
			
		||||
    limit: number;
 | 
			
		||||
    totalPages: number;
 | 
			
		||||
  }> {
 | 
			
		||||
    try {
 | 
			
		||||
      // 构建查询选择器
 | 
			
		||||
      let selector: any = {};
 | 
			
		||||
 | 
			
		||||
      if (filters?.uid) {
 | 
			
		||||
        selector.uid = filters.uid;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (filters?.markType) {
 | 
			
		||||
        selector.markType = filters.markType;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (filters?.tags && filters.tags.length > 0) {
 | 
			
		||||
        selector.tags = { $elemMatch: { $in: filters.tags } };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // 获取总数
 | 
			
		||||
      const countResult = await this.db.find({
 | 
			
		||||
        selector,
 | 
			
		||||
        fields: []
 | 
			
		||||
      });
 | 
			
		||||
      const total = countResult.docs.length;
 | 
			
		||||
 | 
			
		||||
      // 计算分页
 | 
			
		||||
      const skip = (page - 1) * limit;
 | 
			
		||||
      const totalPages = Math.ceil(total / limit);
 | 
			
		||||
 | 
			
		||||
      // 获取数据
 | 
			
		||||
      const result = await this.db.find({
 | 
			
		||||
        selector,
 | 
			
		||||
        sort: [{ createdAt: 'desc' }],
 | 
			
		||||
        skip,
 | 
			
		||||
        limit
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        marks: result.docs.map(doc => docToMark(doc)),
 | 
			
		||||
        total,
 | 
			
		||||
        page,
 | 
			
		||||
        limit,
 | 
			
		||||
        totalPages
 | 
			
		||||
      };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('分页获取 Marks 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 获取统计信息
 | 
			
		||||
  async getStats(): Promise<{
 | 
			
		||||
    total: number;
 | 
			
		||||
    byType: Record<string, number>;
 | 
			
		||||
    byUser: Record<string, number>;
 | 
			
		||||
    recentActivity: number;
 | 
			
		||||
  }> {
 | 
			
		||||
    try {
 | 
			
		||||
      const marks = await this.getAll();
 | 
			
		||||
 | 
			
		||||
      const stats = {
 | 
			
		||||
        total: marks.length,
 | 
			
		||||
        byType: {} as Record<string, number>,
 | 
			
		||||
        byUser: {} as Record<string, number>,
 | 
			
		||||
        recentActivity: 0
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      // 统计按类型分组
 | 
			
		||||
      marks.forEach(mark => {
 | 
			
		||||
        if (mark.markType) {
 | 
			
		||||
          stats.byType[mark.markType] = (stats.byType[mark.markType] || 0) + 1;
 | 
			
		||||
        }
 | 
			
		||||
        if (mark.uid) {
 | 
			
		||||
          stats.byUser[mark.uid] = (stats.byUser[mark.uid] || 0) + 1;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // 统计最近7天的活动
 | 
			
		||||
      const weekAgo = new Date();
 | 
			
		||||
      weekAgo.setDate(weekAgo.getDate() - 7);
 | 
			
		||||
      stats.recentActivity = marks.filter(mark =>
 | 
			
		||||
        new Date(mark.updatedAt) > weekAgo
 | 
			
		||||
      ).length;
 | 
			
		||||
 | 
			
		||||
      return stats;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('获取统计信息失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 同步数据库(用于远程同步)
 | 
			
		||||
  async sync(remoteDB: string | PouchDB.Database): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const remote = typeof remoteDB === 'string' ? new PouchDB(remoteDB) : remoteDB;
 | 
			
		||||
 | 
			
		||||
      await this.db.sync(remote).on('complete', () => {
 | 
			
		||||
        console.log('同步完成');
 | 
			
		||||
      }).on('error', (err) => {
 | 
			
		||||
        console.error('同步错误:', err);
 | 
			
		||||
      });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('同步失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 清理数据库
 | 
			
		||||
  async clear(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      await this.db.destroy();
 | 
			
		||||
      this.db = createDB(this.db.name);
 | 
			
		||||
      await this.createIndexes();
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('清理数据库失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 生成唯一ID
 | 
			
		||||
  private generateId(): string {
 | 
			
		||||
    return `mark_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 关闭数据库连接
 | 
			
		||||
  async close(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      await this.db.close();
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('关闭数据库失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 创建默认实例
 | 
			
		||||
export const markDB = new MarkDB();
 | 
			
		||||
 | 
			
		||||
// 初始化数据库
 | 
			
		||||
export const initMarkDB = async () => {
 | 
			
		||||
  await markDB.createIndexes();
 | 
			
		||||
  return markDB;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										186
									
								
								web/src/apps/muse/modules/mark-service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								web/src/apps/muse/modules/mark-service.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,186 @@
 | 
			
		||||
import { markDB, initMarkDB } from './db';
 | 
			
		||||
import { Mark } from './mark';
 | 
			
		||||
import { mockMarks } from '../base/mock/collection';
 | 
			
		||||
 | 
			
		||||
// Mark 服务类 - 提供业务逻辑层
 | 
			
		||||
export class MarkService {
 | 
			
		||||
  private db = markDB;
 | 
			
		||||
 | 
			
		||||
  // 初始化服务
 | 
			
		||||
  async init() {
 | 
			
		||||
    await initMarkDB();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 创建新的 Mark
 | 
			
		||||
  async createMark(markData: Omit<Mark, 'id' | 'createdAt' | 'updatedAt'>): Promise<Mark> {
 | 
			
		||||
    return await this.db.create(markData);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 根据 ID 获取 Mark
 | 
			
		||||
  async getMark(id: string): Promise<Mark | null> {
 | 
			
		||||
    return await this.db.getById(id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 获取所有 Marks
 | 
			
		||||
  async getAllMarks(): Promise<Mark[]> {
 | 
			
		||||
    return await this.db.getAll();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 按用户获取 Marks
 | 
			
		||||
  async getMarksByUser(userId: string): Promise<Mark[]> {
 | 
			
		||||
    return await this.db.getByUserId(userId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 按类型获取 Marks
 | 
			
		||||
  async getMarksByType(markType: string): Promise<Mark[]> {
 | 
			
		||||
    return await this.db.getByType(markType as any);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 按标签搜索 Marks
 | 
			
		||||
  async getMarksByTag(tag: string): Promise<Mark[]> {
 | 
			
		||||
    return await this.db.getByTag(tag);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 搜索 Marks
 | 
			
		||||
  async searchMarks(query: string): Promise<Mark[]> {
 | 
			
		||||
    return await this.db.search(query);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 更新 Mark
 | 
			
		||||
  async updateMark(id: string, updates: Partial<Omit<Mark, 'id' | 'createdAt'>>): Promise<Mark> {
 | 
			
		||||
    return await this.db.update(id, updates);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 删除 Mark
 | 
			
		||||
  async deleteMark(id: string): Promise<boolean> {
 | 
			
		||||
    return await this.db.delete(id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 批量删除 Marks
 | 
			
		||||
  async deleteMultipleMarks(ids: string[]): Promise<boolean> {
 | 
			
		||||
    return await this.db.deleteMultiple(ids);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 分页获取 Marks
 | 
			
		||||
  async getMarksPaginated(
 | 
			
		||||
    page: number = 1,
 | 
			
		||||
    limit: number = 10,
 | 
			
		||||
    filters?: {
 | 
			
		||||
      uid?: string;
 | 
			
		||||
      markType?: string;
 | 
			
		||||
      tags?: string[];
 | 
			
		||||
    }
 | 
			
		||||
  ) {
 | 
			
		||||
    return await this.db.getPaginated(page, limit, filters);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 获取统计信息
 | 
			
		||||
  async getStats() {
 | 
			
		||||
    return await this.db.getStats();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 初始化示例数据
 | 
			
		||||
  async initSampleData(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      // 检查是否已有数据
 | 
			
		||||
      const existingMarks = await this.getAllMarks();
 | 
			
		||||
      if (existingMarks.length === 0) {
 | 
			
		||||
        // 添加示例数据
 | 
			
		||||
        for (const mockMark of mockMarks) {
 | 
			
		||||
          const { id, createdAt, updatedAt, ...markData } = mockMark;
 | 
			
		||||
          await this.createMark(markData);
 | 
			
		||||
        }
 | 
			
		||||
        console.log(`已初始化 ${mockMarks.length} 条示例数据`);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('初始化示例数据失败:', error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 导出数据
 | 
			
		||||
  async exportData(): Promise<Mark[]> {
 | 
			
		||||
    return await this.getAllMarks();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 导入数据
 | 
			
		||||
  async importData(marks: Mark[]): Promise<number> {
 | 
			
		||||
    let importedCount = 0;
 | 
			
		||||
    try {
 | 
			
		||||
      for (const mark of marks) {
 | 
			
		||||
        const { id, createdAt, updatedAt, ...markData } = mark;
 | 
			
		||||
        await this.createMark(markData);
 | 
			
		||||
        importedCount++;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('导入数据失败:', error);
 | 
			
		||||
    }
 | 
			
		||||
    return importedCount;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 创建默认服务实例
 | 
			
		||||
export const markService = new MarkService();
 | 
			
		||||
 | 
			
		||||
// 使用示例函数
 | 
			
		||||
export const exampleUsage = async () => {
 | 
			
		||||
  // 1. 初始化服务
 | 
			
		||||
  await markService.init();
 | 
			
		||||
 | 
			
		||||
  // 2. 初始化示例数据
 | 
			
		||||
  await markService.initSampleData();
 | 
			
		||||
 | 
			
		||||
  // 3. 创建新的 Mark
 | 
			
		||||
  const newMark = await markService.createMark({
 | 
			
		||||
    title: '我的第一个标记',
 | 
			
		||||
    description: '这是一个测试标记',
 | 
			
		||||
    markType: 'markdown',
 | 
			
		||||
    tags: ['测试', '示例'],
 | 
			
		||||
    data: {
 | 
			
		||||
      md: '# 测试内容\n\n这是一个测试标记的内容。',
 | 
			
		||||
      type: 'markdown'
 | 
			
		||||
    },
 | 
			
		||||
    uid: 'user123',
 | 
			
		||||
    uname: '测试用户'
 | 
			
		||||
  });
 | 
			
		||||
  console.log('创建的标记:', newMark);
 | 
			
		||||
 | 
			
		||||
  // 4. 获取所有标记
 | 
			
		||||
  const allMarks = await markService.getAllMarks();
 | 
			
		||||
  console.log('所有标记数量:', allMarks.length);
 | 
			
		||||
 | 
			
		||||
  // 5. 搜索标记
 | 
			
		||||
  const searchResults = await markService.searchMarks('测试');
 | 
			
		||||
  console.log('搜索结果数量:', searchResults.length);
 | 
			
		||||
 | 
			
		||||
  // 6. 按类型获取标记
 | 
			
		||||
  const markdownMarks = await markService.getMarksByType('markdown');
 | 
			
		||||
  console.log('Markdown 标记数量:', markdownMarks.length);
 | 
			
		||||
 | 
			
		||||
  // 7. 分页获取标记
 | 
			
		||||
  const paginatedResults = await markService.getMarksPaginated(1, 5);
 | 
			
		||||
  console.log('分页结果:', {
 | 
			
		||||
    currentPage: paginatedResults.page,
 | 
			
		||||
    totalPages: paginatedResults.totalPages,
 | 
			
		||||
    total: paginatedResults.total,
 | 
			
		||||
    marksOnPage: paginatedResults.marks.length
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // 8. 获取统计信息
 | 
			
		||||
  const stats = await markService.getStats();
 | 
			
		||||
  console.log('统计信息:', stats);
 | 
			
		||||
 | 
			
		||||
  // 9. 更新标记
 | 
			
		||||
  if (newMark.id) {
 | 
			
		||||
    const updatedMark = await markService.updateMark(newMark.id, {
 | 
			
		||||
      title: '更新后的标题',
 | 
			
		||||
      description: '更新后的描述'
 | 
			
		||||
    });
 | 
			
		||||
    console.log('更新后的标记:', updatedMark);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 10. 删除标记
 | 
			
		||||
  if (newMark.id) {
 | 
			
		||||
    const deleted = await markService.deleteMark(newMark.id);
 | 
			
		||||
    console.log('删除结果:', deleted);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										78
									
								
								web/src/apps/muse/modules/mark.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								web/src/apps/muse/modules/mark.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
export type Mark<T = any> = {
 | 
			
		||||
  /**
 | 
			
		||||
   * 标记ID
 | 
			
		||||
   */
 | 
			
		||||
  id: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 标题
 | 
			
		||||
   */
 | 
			
		||||
  title?: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 描述
 | 
			
		||||
   */
 | 
			
		||||
  description?: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 标签
 | 
			
		||||
   */
 | 
			
		||||
  tags?: string[];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 标记类型
 | 
			
		||||
   */
 | 
			
		||||
  markType?: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 封面
 | 
			
		||||
   */
 | 
			
		||||
  cover?: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 链接
 | 
			
		||||
   */
 | 
			
		||||
  link?: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 摘要
 | 
			
		||||
   */
 | 
			
		||||
  summary?: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 键
 | 
			
		||||
   */
 | 
			
		||||
  key?: string;
 | 
			
		||||
  data: T;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 附件列表
 | 
			
		||||
   */
 | 
			
		||||
  fileList?: any[];
 | 
			
		||||
  /**
 | 
			
		||||
   * 创建人信息
 | 
			
		||||
   */
 | 
			
		||||
  uname?: string;
 | 
			
		||||
  /**
 | 
			
		||||
   * 版本号
 | 
			
		||||
   */
 | 
			
		||||
  version?: number;
 | 
			
		||||
  /**
 | 
			
		||||
   * 创建时间
 | 
			
		||||
   */
 | 
			
		||||
  createdAt: Date;
 | 
			
		||||
  /**
 | 
			
		||||
   * 更新时间
 | 
			
		||||
   */
 | 
			
		||||
  updatedAt: Date;
 | 
			
		||||
  /**
 | 
			
		||||
   * 标记时间
 | 
			
		||||
   */
 | 
			
		||||
  markedAt?: Date;
 | 
			
		||||
  uid?: string;
 | 
			
		||||
  puid?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ensureType = ['markdown', 'json', 'html', 'image', 'video', 'audio', 'code', 'link', 'file']
 | 
			
		||||
 | 
			
		||||
export type MarkEnsureType = typeof ensureType[number];
 | 
			
		||||
@@ -84,7 +84,7 @@ export const ChatInterface: React.FC = () => {
 | 
			
		||||
            <Bot className="w-6 h-6 text-white" />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div>
 | 
			
		||||
            <h1 className="text-xl font-semibold text-gray-900">AI 助手</h1>
 | 
			
		||||
            <h1 className="text-xl font-semibold text-gray-900">智能工作台</h1>
 | 
			
		||||
            <p className="text-sm text-gray-500">在线 · 随时为您服务</p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user