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",
 | 
					    "clsx": "^2.1.1",
 | 
				
			||||||
    "dayjs": "^1.11.18",
 | 
					    "dayjs": "^1.11.18",
 | 
				
			||||||
    "es-toolkit": "^1.40.0",
 | 
					    "es-toolkit": "^1.40.0",
 | 
				
			||||||
 | 
					    "events": "^3.3.0",
 | 
				
			||||||
 | 
					    "graphology": "^0.26.0",
 | 
				
			||||||
    "highlight.js": "^11.11.1",
 | 
					    "highlight.js": "^11.11.1",
 | 
				
			||||||
    "lodash-es": "^4.17.21",
 | 
					    "lodash-es": "^4.17.21",
 | 
				
			||||||
    "lucide-react": "^0.545.0",
 | 
					    "lucide-react": "^0.545.0",
 | 
				
			||||||
    "nanoid": "^5.1.6",
 | 
					    "nanoid": "^5.1.6",
 | 
				
			||||||
    "pocketbase": "^0.26.2",
 | 
					    "pocketbase": "^0.26.2",
 | 
				
			||||||
 | 
					    "pouchdb-adapter-memory": "^9.0.0",
 | 
				
			||||||
 | 
					    "pouchdb-browser": "^9.0.0",
 | 
				
			||||||
    "react": "^19.2.0",
 | 
					    "react": "^19.2.0",
 | 
				
			||||||
    "react-dom": "^19.2.0",
 | 
					    "react-dom": "^19.2.0",
 | 
				
			||||||
    "react-resizable-panels": "^3.0.6",
 | 
					    "react-resizable-panels": "^3.0.6",
 | 
				
			||||||
    "react-toastify": "^11.0.5",
 | 
					    "react-toastify": "^11.0.5",
 | 
				
			||||||
 | 
					    "sigma": "^3.0.2",
 | 
				
			||||||
    "tailwind-merge": "^3.3.1",
 | 
					    "tailwind-merge": "^3.3.1",
 | 
				
			||||||
    "three": "^0.180.0",
 | 
					    "three": "^0.180.0",
 | 
				
			||||||
    "wavesurfer.js": "^7.11.0",
 | 
					    "wavesurfer.js": "^7.11.0",
 | 
				
			||||||
@@ -50,11 +55,14 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@kevisual/types": "^0.0.10",
 | 
					    "@kevisual/types": "^0.0.10",
 | 
				
			||||||
 | 
					    "@types/pouchdb": "^6.4.2",
 | 
				
			||||||
 | 
					    "@types/pouchdb-browser": "^6.1.5",
 | 
				
			||||||
    "@types/react": "^19.2.2",
 | 
					    "@types/react": "^19.2.2",
 | 
				
			||||||
    "@types/react-dom": "^19.2.2",
 | 
					    "@types/react-dom": "^19.2.2",
 | 
				
			||||||
    "@types/three": "^0.180.0",
 | 
					    "@types/three": "^0.180.0",
 | 
				
			||||||
    "@vitejs/plugin-basic-ssl": "^2.1.0",
 | 
					    "@vitejs/plugin-basic-ssl": "^2.1.0",
 | 
				
			||||||
    "dotenv": "^17.2.3",
 | 
					    "dotenv": "^17.2.3",
 | 
				
			||||||
 | 
					    "pouchdb": "^9.0.0",
 | 
				
			||||||
    "tailwindcss": "^4.1.14",
 | 
					    "tailwindcss": "^4.1.14",
 | 
				
			||||||
    "tw-animate-css": "^1.4.0"
 | 
					    "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 { Base } from "./table/index";
 | 
				
			||||||
 | 
					import { markService } from "../modules/mark-service";
 | 
				
			||||||
 | 
					import { SigmaGraph } from "./graph/sigma/index";
 | 
				
			||||||
const tabs = [
 | 
					const tabs = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    key: 'table',
 | 
					    key: 'table',
 | 
				
			||||||
@@ -13,20 +14,31 @@ const tabs = [
 | 
				
			|||||||
  {
 | 
					  {
 | 
				
			||||||
    key: 'world',
 | 
					    key: 'world',
 | 
				
			||||||
    title: '世界'
 | 
					    title: '世界'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    key: 'docs',
 | 
				
			||||||
 | 
					    title: '文档'
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const BaseApp = () => {
 | 
					export const BaseApp = () => {
 | 
				
			||||||
  const [activeTab, setActiveTab] = useState('table');
 | 
					  const [activeTab, setActiveTab] = useState('table');
 | 
				
			||||||
 | 
					  const [dataSource, setDataSource] = useState<any[]>([]);
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    getMarks();
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					  const getMarks = async () => {
 | 
				
			||||||
 | 
					    const marks = await markService.getAllMarks();
 | 
				
			||||||
 | 
					    setDataSource(marks);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  const renderContent = () => {
 | 
					  const renderContent = () => {
 | 
				
			||||||
    switch (activeTab) {
 | 
					    switch (activeTab) {
 | 
				
			||||||
      case 'table':
 | 
					      case 'table':
 | 
				
			||||||
        return <Base />;
 | 
					        return <Base dataSource={dataSource} />;
 | 
				
			||||||
      case 'graph':
 | 
					      case 'graph':
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
          <div className="flex items-center justify-center h-96 text-gray-500">
 | 
					          <div className="w-full h-96">
 | 
				
			||||||
            关系图模块暂未实现
 | 
					            <SigmaGraph dataSource={dataSource} />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      case 'world':
 | 
					      case 'world':
 | 
				
			||||||
@@ -35,6 +47,12 @@ export const BaseApp = () => {
 | 
				
			|||||||
            世界模块暂未实现
 | 
					            世界模块暂未实现
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					      case 'docs':
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <div className="flex items-center justify-center h-96 text-gray-500">
 | 
				
			||||||
 | 
					            文档模块暂未实现
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -49,11 +67,10 @@ export const BaseApp = () => {
 | 
				
			|||||||
            <button
 | 
					            <button
 | 
				
			||||||
              key={tab.key}
 | 
					              key={tab.key}
 | 
				
			||||||
              onClick={() => setActiveTab(tab.key)}
 | 
					              onClick={() => setActiveTab(tab.key)}
 | 
				
			||||||
              className={`py-2 px-1 border-b-2 font-medium text-sm ${
 | 
					              className={`py-2 px-1 border-b-2 font-medium text-sm ${activeTab === tab.key
 | 
				
			||||||
                activeTab === tab.key
 | 
					                ? 'border-blue-500 text-blue-600'
 | 
				
			||||||
                  ? 'border-blue-500 text-blue-600'
 | 
					                : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
 | 
				
			||||||
                  : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
 | 
					                }`}
 | 
				
			||||||
              }`}
 | 
					 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              {tab.title}
 | 
					              {tab.title}
 | 
				
			||||||
            </button>
 | 
					            </button>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,87 @@ import { nanoid, customAlphabet } from 'nanoid';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const random = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
 | 
					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 = {
 | 
					export type MarkDataNode = {
 | 
				
			||||||
  id?: string;
 | 
					  id?: string;
 | 
				
			||||||
  content?: string;
 | 
					  content?: string;
 | 
				
			||||||
@@ -20,64 +100,25 @@ export type MarkFile = {
 | 
				
			|||||||
  name: string;
 | 
					  name: string;
 | 
				
			||||||
  url: string;
 | 
					  url: string;
 | 
				
			||||||
  size: number;
 | 
					  size: number;
 | 
				
			||||||
  type: 'self' | 'data' | 'generate'; // generate为生成文件
 | 
					  type: 'self' | 'data' | 'generate';
 | 
				
			||||||
  query: string; // 'data.nodes[id].content';
 | 
					  query: string;
 | 
				
			||||||
  hash: string;
 | 
					  hash: string;
 | 
				
			||||||
  fileKey: string; // 文件的名称, 唯一
 | 
					  fileKey: string;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type MarkData = {
 | 
					export type MarkData = {
 | 
				
			||||||
  md?: string; // markdown
 | 
					  md?: string;
 | 
				
			||||||
  mdList?: string[]; // markdown list
 | 
					  mdList?: string[];
 | 
				
			||||||
  type?: string; // 类型 markdown | json | html | image | video | audio | code | link | file
 | 
					  type?: MarkEnsureType;
 | 
				
			||||||
  data?: any;
 | 
					  data?: any;
 | 
				
			||||||
  key?: string; // 文件的名称, 唯一
 | 
					  key?: string;
 | 
				
			||||||
  push?: boolean; // 是否推送到elasticsearch
 | 
					  push?: boolean;
 | 
				
			||||||
  pushTime?: Date; // 推送时间
 | 
					  pushTime?: Date;
 | 
				
			||||||
  summary?: string; // 摘要
 | 
					  summary?: string;
 | 
				
			||||||
  nodes?: MarkDataNode[]; // 节点
 | 
					  nodes?: MarkDataNode[];
 | 
				
			||||||
  [key: string]: any;
 | 
					  [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
 | 
					// 生成模拟的 MarkDataNode
 | 
				
			||||||
const generateMarkDataNode = (): MarkDataNode => ({
 | 
					const generateMarkDataNode = (): MarkDataNode => ({
 | 
				
			||||||
  id: random(12),
 | 
					  id: random(12),
 | 
				
			||||||
@@ -99,85 +140,187 @@ const generateMarkDataNode = (): MarkDataNode => ({
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 生成模拟的 MarkFile
 | 
					// 生成模拟的附件
 | 
				
			||||||
const generateMarkFile = (): MarkFile => ({
 | 
					const generateFileAttachment = (): any => ({
 | 
				
			||||||
  id: faker.string.uuid(),
 | 
					  id: faker.string.uuid(),
 | 
				
			||||||
  name: faker.system.fileName(),
 | 
					  name: faker.system.fileName(),
 | 
				
			||||||
  url: faker.internet.url(),
 | 
					  url: faker.internet.url(),
 | 
				
			||||||
  size: faker.number.int({ min: 1024, max: 50 * 1024 * 1024 }), // 1KB to 50MB
 | 
					  size: faker.number.int({ min: 1024, max: 50 * 1024 * 1024 }),
 | 
				
			||||||
  type: faker.helpers.arrayElement(['self', 'data', 'generate']),
 | 
					  type: faker.helpers.arrayElement(['image', 'document', 'video', 'audio']),
 | 
				
			||||||
  query: `data.nodes[${random(12)}].content`,
 | 
					  mimeType: faker.system.mimeType(),
 | 
				
			||||||
  hash: faker.git.commitSha(),
 | 
					  hash: faker.git.commitSha(),
 | 
				
			||||||
  fileKey: faker.system.fileName()
 | 
					  uploadedAt: faker.date.recent()
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 生成模拟的 MarkData
 | 
					// 生成模拟的 MarkData(通用数据类型)
 | 
				
			||||||
const generateMarkData = (): MarkData => ({
 | 
					const generateMarkData = <T = any>(): T => {
 | 
				
			||||||
  md: faker.lorem.paragraphs(3, '\n\n'),
 | 
					  const dataVariants = [
 | 
				
			||||||
  mdList: Array.from({ length: faker.number.int({ min: 3, max: 8 }) }, () => faker.lorem.sentence()),
 | 
					    // Markdown 数据
 | 
				
			||||||
  type: faker.helpers.arrayElement(['markdown', 'json', 'html', 'image', 'video', 'audio', 'code', 'link', 'file']),
 | 
					    {
 | 
				
			||||||
  data: {
 | 
					      content: faker.lorem.paragraphs(3, '\n\n'),
 | 
				
			||||||
    author: faker.person.fullName(),
 | 
					      format: 'markdown',
 | 
				
			||||||
    category: faker.helpers.arrayElement(['技术', '生活', '工作', '学习', '思考']),
 | 
					      wordCount: faker.number.int({ min: 100, max: 1000 })
 | 
				
			||||||
    priority: faker.helpers.arrayElement(['low', 'medium', 'high'])
 | 
					    },
 | 
				
			||||||
  },
 | 
					    // JSON 数据
 | 
				
			||||||
  key: faker.system.fileName(),
 | 
					    {
 | 
				
			||||||
  push: faker.datatype.boolean(),
 | 
					      jsonData: {
 | 
				
			||||||
  pushTime: faker.date.recent(),
 | 
					        name: faker.person.fullName(),
 | 
				
			||||||
  summary: faker.lorem.paragraph(),
 | 
					        email: faker.internet.email(),
 | 
				
			||||||
  nodes: Array.from({ length: faker.number.int({ min: 2, max: 6 }) }, generateMarkDataNode)
 | 
					        settings: {
 | 
				
			||||||
});
 | 
					          theme: faker.helpers.arrayElement(['light', 'dark']),
 | 
				
			||||||
 | 
					          language: faker.helpers.arrayElement(['zh-CN', 'en-US', 'ja-JP'])
 | 
				
			||||||
// 生成模拟的 MarkConfig
 | 
					        }
 | 
				
			||||||
const generateMarkConfig = (): MarkConfig => ({
 | 
					      },
 | 
				
			||||||
  visibility: faker.helpers.arrayElement(['public', 'private', 'restricted']),
 | 
					      schema: 'user-profile'
 | 
				
			||||||
  allowComments: faker.datatype.boolean(),
 | 
					    },
 | 
				
			||||||
  allowDownload: faker.datatype.boolean(),
 | 
					    // 图片数据
 | 
				
			||||||
  password: faker.datatype.boolean() ? faker.internet.password() : undefined,
 | 
					    {
 | 
				
			||||||
  expiredAt: faker.datatype.boolean() ? faker.date.future() : undefined,
 | 
					      src: faker.image.url(),
 | 
				
			||||||
  theme: faker.helpers.arrayElement(['light', 'dark', 'auto']),
 | 
					      alt: faker.lorem.sentence(),
 | 
				
			||||||
  language: faker.helpers.arrayElement(['zh-CN', 'en-US', 'ja-JP'])
 | 
					      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 记录
 | 
					// 生成单个 Mark 记录
 | 
				
			||||||
const generateMark = (): Mark => {
 | 
					const generateMark = <T = any>(): Mark<T> => {
 | 
				
			||||||
  const markType = faker.helpers.arrayElement(['markdown', 'json', 'html', 'image', 'video', 'audio', 'code', 'link', 'file']);
 | 
					  const createdAt = faker.date.past();
 | 
				
			||||||
  const title = faker.lorem.sentence({ min: 3, max: 8 });
 | 
					  const updatedAt = faker.date.between({ from: createdAt, to: new Date() });
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    id: faker.string.uuid(),
 | 
					    id: faker.string.uuid(),
 | 
				
			||||||
    title,
 | 
					    title: faker.datatype.boolean() ? faker.lorem.sentence({ min: 3, max: 8 }) : undefined,
 | 
				
			||||||
    description: faker.lorem.paragraph(),
 | 
					    description: faker.datatype.boolean() ? faker.lorem.paragraph() : undefined,
 | 
				
			||||||
    cover: faker.image.url({ width: 800, height: 600 }),
 | 
					    tags: faker.datatype.boolean() 
 | 
				
			||||||
    thumbnail: faker.image.url({ width: 200, height: 150 }),
 | 
					      ? Array.from({ length: faker.number.int({ min: 1, max: 5 }) }, () => 
 | 
				
			||||||
    key: faker.system.filePath(),
 | 
					          faker.helpers.arrayElement(['技术', '前端', '后端', '设计', 'AI', '工具', '教程', '笔记', '生活', '工作'])
 | 
				
			||||||
    markType,
 | 
					        ) 
 | 
				
			||||||
    link: faker.internet.url(),
 | 
					      : undefined,
 | 
				
			||||||
    tags: Array.from({ length: faker.number.int({ min: 1, max: 5 }) }, () => 
 | 
					    markType: faker.datatype.boolean() 
 | 
				
			||||||
      faker.helpers.arrayElement(['技术', '前端', '后端', '设计', 'AI', '工具', '教程', '笔记'])
 | 
					      ? faker.helpers.arrayElement(ensureType) 
 | 
				
			||||||
    ),
 | 
					      : undefined,
 | 
				
			||||||
    summary: faker.lorem.sentence(),
 | 
					    cover: faker.datatype.boolean() 
 | 
				
			||||||
    data: generateMarkData(),
 | 
					      ? faker.image.url({ width: 800, height: 600 }) 
 | 
				
			||||||
    uid: faker.string.uuid(),
 | 
					      : undefined,
 | 
				
			||||||
    puid: faker.string.uuid(),
 | 
					    link: faker.datatype.boolean() 
 | 
				
			||||||
    config: generateMarkConfig(),
 | 
					      ? faker.internet.url() 
 | 
				
			||||||
    fileList: Array.from({ length: faker.number.int({ min: 0, max: 4 }) }, generateMarkFile),
 | 
					      : undefined,
 | 
				
			||||||
    uname: faker.person.fullName(),
 | 
					    summary: faker.datatype.boolean() 
 | 
				
			||||||
    markedAt: faker.date.past(),
 | 
					      ? faker.lorem.sentence() 
 | 
				
			||||||
    createdAt: faker.date.past(),
 | 
					      : undefined,
 | 
				
			||||||
    updatedAt: faker.date.recent(),
 | 
					    key: faker.datatype.boolean() 
 | 
				
			||||||
    version: faker.number.int({ min: 1, max: 10 })
 | 
					      ? 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 条模拟数据
 | 
					// 生成 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 {
 | 
					export {
 | 
				
			||||||
  generateMark,
 | 
					  generateMark,
 | 
				
			||||||
 | 
					  generateMarkWithType,
 | 
				
			||||||
  generateMarkData,
 | 
					  generateMarkData,
 | 
				
			||||||
  generateMarkFile,
 | 
					 | 
				
			||||||
  generateMarkDataNode,
 | 
					  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 { Table } from './Table';
 | 
				
			||||||
import { DetailModal } from './DetailModal';
 | 
					import { DetailModal } from './DetailModal';
 | 
				
			||||||
import { mockMarks, Mark } from '../mock/collection';
 | 
					import { Mark } from '../mock/collection';
 | 
				
			||||||
import { TableColumn, ActionButton } from './types';
 | 
					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 [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
 | 
				
			||||||
  const [currentPage, setCurrentPage] = useState(1);
 | 
					  const [currentPage, setCurrentPage] = useState(1);
 | 
				
			||||||
  const [pageSize, setPageSize] = useState(10);
 | 
					  const [pageSize, setPageSize] = useState(10);
 | 
				
			||||||
  const [data, setData] = useState<Mark[]>(mockMarks);
 | 
					  const [data, setData] = useState<Mark[]>([]);
 | 
				
			||||||
  const [detailModalVisible, setDetailModalVisible] = useState(false);
 | 
					  const [detailModalVisible, setDetailModalVisible] = useState(false);
 | 
				
			||||||
  const [currentRecord, setCurrentRecord] = useState<Mark | null>(null);
 | 
					  const [currentRecord, setCurrentRecord] = useState<Mark | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (dataSource) {
 | 
				
			||||||
 | 
					      setData(dataSource);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [dataSource]);
 | 
				
			||||||
  // 表格列配置
 | 
					  // 表格列配置
 | 
				
			||||||
  const columns: TableColumn<Mark>[] = [
 | 
					  const columns: TableColumn<Mark>[] = [
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -26,7 +35,7 @@ export const Base = () => {
 | 
				
			|||||||
            {value}
 | 
					            {value}
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div style={{ fontSize: '12px', color: '#666' }}>
 | 
					          <div style={{ fontSize: '12px', color: '#666' }}>
 | 
				
			||||||
            {record.description.slice(0, 60)}...
 | 
					            {record.description?.slice?.(0, 60)}...
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
@@ -37,28 +46,31 @@ export const Base = () => {
 | 
				
			|||||||
      dataIndex: 'markType',
 | 
					      dataIndex: 'markType',
 | 
				
			||||||
      width: 100,
 | 
					      width: 100,
 | 
				
			||||||
      sortable: true,
 | 
					      sortable: true,
 | 
				
			||||||
      render: (value: string) => (
 | 
					      render: (value: string) => {
 | 
				
			||||||
        <span 
 | 
					        if (!value) return ''
 | 
				
			||||||
          style={{
 | 
					        return (
 | 
				
			||||||
            padding: '4px 8px',
 | 
					          <span
 | 
				
			||||||
            borderRadius: '4px',
 | 
					            style={{
 | 
				
			||||||
            backgroundColor: getTypeColor(value),
 | 
					              padding: '4px 8px',
 | 
				
			||||||
            color: '#fff',
 | 
					              borderRadius: '4px',
 | 
				
			||||||
            fontSize: '12px'
 | 
					              backgroundColor: getTypeColor(value),
 | 
				
			||||||
          }}
 | 
					              color: '#fff',
 | 
				
			||||||
        >
 | 
					              fontSize: '12px'
 | 
				
			||||||
          {value}
 | 
					            }}
 | 
				
			||||||
        </span>
 | 
					          >
 | 
				
			||||||
      )
 | 
					            {value}
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      key: 'tags',
 | 
					      key: 'tags',
 | 
				
			||||||
      title: '标签',
 | 
					      title: '标签',
 | 
				
			||||||
      dataIndex: 'tags',
 | 
					      dataIndex: 'tags',
 | 
				
			||||||
      width: 200,
 | 
					      width: 200,
 | 
				
			||||||
      render: (tags: string[]) => (
 | 
					      render: (tags: string[] = []) => (
 | 
				
			||||||
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
 | 
					        <div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
 | 
				
			||||||
          {tags.slice(0, 3).map((tag, index) => (
 | 
					          {tags?.slice?.(0, 3).map((tag, index) => (
 | 
				
			||||||
            <span
 | 
					            <span
 | 
				
			||||||
              key={index}
 | 
					              key={index}
 | 
				
			||||||
              style={{
 | 
					              style={{
 | 
				
			||||||
@@ -96,13 +108,13 @@ export const Base = () => {
 | 
				
			|||||||
      render: (value: Date) => new Date(value).toLocaleString('zh-CN')
 | 
					      render: (value: Date) => new Date(value).toLocaleString('zh-CN')
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      key: 'config.visibility',
 | 
					      key: 'data.visibility',
 | 
				
			||||||
      title: '可见性',
 | 
					      title: '可见性',
 | 
				
			||||||
      dataIndex: 'config.visibility',
 | 
					      dataIndex: 'data.visibility',
 | 
				
			||||||
      width: 100,
 | 
					      width: 100,
 | 
				
			||||||
      render: (value: string) => (
 | 
					      render: (value: string) => (
 | 
				
			||||||
        <span style={{ 
 | 
					        <span style={{
 | 
				
			||||||
          color: value === 'public' ? '#52c41a' : value === 'private' ? '#ff4d4f' : '#faad14' 
 | 
					          color: value === 'public' ? '#52c41a' : value === 'private' ? '#ff4d4f' : '#faad14'
 | 
				
			||||||
        }}>
 | 
					        }}>
 | 
				
			||||||
          {value === 'public' ? '公开' : value === 'private' ? '私有' : '受限'}
 | 
					          {value === 'public' ? '公开' : value === 'private' ? '私有' : '受限'}
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
@@ -180,7 +192,7 @@ export const Base = () => {
 | 
				
			|||||||
  // 处理批量删除
 | 
					  // 处理批量删除
 | 
				
			||||||
  const handleBatchDelete = () => {
 | 
					  const handleBatchDelete = () => {
 | 
				
			||||||
    if (selectedRowKeys.length === 0) return;
 | 
					    if (selectedRowKeys.length === 0) return;
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    if (window.confirm(`确定要删除选中的 ${selectedRowKeys.length} 项吗?`)) {
 | 
					    if (window.confirm(`确定要删除选中的 ${selectedRowKeys.length} 项吗?`)) {
 | 
				
			||||||
      setData(prevData => prevData.filter(item => !selectedRowKeys.includes(item.id)));
 | 
					      setData(prevData => prevData.filter(item => !selectedRowKeys.includes(item.id)));
 | 
				
			||||||
      setSelectedRowKeys([]);
 | 
					      setSelectedRowKeys([]);
 | 
				
			||||||
@@ -190,7 +202,7 @@ export const Base = () => {
 | 
				
			|||||||
  // 排序处理
 | 
					  // 排序处理
 | 
				
			||||||
  const handleSort = (field: string, order: 'asc' | 'desc' | null) => {
 | 
					  const handleSort = (field: string, order: 'asc' | 'desc' | null) => {
 | 
				
			||||||
    if (!order) {
 | 
					    if (!order) {
 | 
				
			||||||
      setData(mockMarks); // 重置为原始顺序
 | 
					      setData(dataSource); // 重置为原始顺序
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -198,10 +210,10 @@ export const Base = () => {
 | 
				
			|||||||
      const getNestedValue = (obj: any, path: string) => {
 | 
					      const getNestedValue = (obj: any, path: string) => {
 | 
				
			||||||
        return path.split('.').reduce((o, p) => o?.[p], obj);
 | 
					        return path.split('.').reduce((o, p) => o?.[p], obj);
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      const aVal = getNestedValue(a, field);
 | 
					      const aVal = getNestedValue(a, field);
 | 
				
			||||||
      const bVal = getNestedValue(b, field);
 | 
					      const bVal = getNestedValue(b, field);
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      if (aVal < bVal) return order === 'asc' ? -1 : 1;
 | 
					      if (aVal < bVal) return order === 'asc' ? -1 : 1;
 | 
				
			||||||
      if (aVal > bVal) return order === 'asc' ? 1 : -1;
 | 
					      if (aVal > bVal) return order === 'asc' ? 1 : -1;
 | 
				
			||||||
      return 0;
 | 
					      return 0;
 | 
				
			||||||
@@ -217,7 +229,7 @@ export const Base = () => {
 | 
				
			|||||||
    total: data.length,
 | 
					    total: data.length,
 | 
				
			||||||
    showSizeChanger: true,
 | 
					    showSizeChanger: true,
 | 
				
			||||||
    showQuickJumper: true,
 | 
					    showQuickJumper: true,
 | 
				
			||||||
    showTotal: (total: number, range: [number, number]) => 
 | 
					    showTotal: (total: number, range: [number, number]) =>
 | 
				
			||||||
      `第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
 | 
					      `第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
 | 
				
			||||||
    onChange: (page: number, size: number) => {
 | 
					    onChange: (page: number, size: number) => {
 | 
				
			||||||
      setCurrentPage(page);
 | 
					      setCurrentPage(page);
 | 
				
			||||||
@@ -235,17 +247,17 @@ export const Base = () => {
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {selectedRowKeys.length > 0 && (
 | 
					      {selectedRowKeys.length > 0 && (
 | 
				
			||||||
        <div style={{ 
 | 
					        <div style={{
 | 
				
			||||||
          marginBottom: '16px', 
 | 
					          marginBottom: '16px',
 | 
				
			||||||
          padding: '12px', 
 | 
					          padding: '12px',
 | 
				
			||||||
          backgroundColor: '#e6f7ff', 
 | 
					          backgroundColor: '#e6f7ff',
 | 
				
			||||||
          borderRadius: '4px',
 | 
					          borderRadius: '4px',
 | 
				
			||||||
          display: 'flex',
 | 
					          display: 'flex',
 | 
				
			||||||
          justifyContent: 'space-between',
 | 
					          justifyContent: 'space-between',
 | 
				
			||||||
          alignItems: 'center'
 | 
					          alignItems: 'center'
 | 
				
			||||||
        }}>
 | 
					        }}>
 | 
				
			||||||
          <span>已选择 {selectedRowKeys.length} 项</span>
 | 
					          <span>已选择 {selectedRowKeys.length} 项</span>
 | 
				
			||||||
          <button 
 | 
					          <button
 | 
				
			||||||
            className="btn btn-danger"
 | 
					            className="btn btn-danger"
 | 
				
			||||||
            onClick={handleBatchDelete}
 | 
					            onClick={handleBatchDelete}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ import { useState } from 'react';
 | 
				
			|||||||
import { VadVoice } from './videos/modules/VadVoice.tsx';
 | 
					import { VadVoice } from './videos/modules/VadVoice.tsx';
 | 
				
			||||||
import { ChatInterface } from './prompts/index.tsx';
 | 
					import { ChatInterface } from './prompts/index.tsx';
 | 
				
			||||||
import { BaseApp } from './base/index.tsx';
 | 
					import { BaseApp } from './base/index.tsx';
 | 
				
			||||||
 | 
					import { exampleUsage } from './modules/mark-service.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const LeftPanel = () => {
 | 
					const LeftPanel = () => {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
@@ -79,6 +80,16 @@ export const MuseApp = () => {
 | 
				
			|||||||
          >
 | 
					          >
 | 
				
			||||||
            Right Panel
 | 
					            Right Panel
 | 
				
			||||||
          </button>
 | 
					          </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>
 | 
				
			||||||
      </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" />
 | 
					            <Bot className="w-6 h-6 text-white" />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <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>
 | 
					            <p className="text-sm text-gray-500">在线 · 随时为您服务</p>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user