Compare commits

..

2 Commits

Author SHA1 Message Date
b5430eb8d0 update 2025-10-20 20:53:14 +08:00
e2e1e9e9e9 update 2025-10-20 13:30:47 +08:00
11 changed files with 1992 additions and 157 deletions

855
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"
},

View 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>
);
};

View File

@@ -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>

View File

@@ -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)
});
// 生成模拟的 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 })
}
];
// 生成模拟的 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'])
});
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
};

View File

@@ -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,9 +108,9 @@ 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={{
@@ -190,7 +202,7 @@ export const Base = () => {
// 排序处理
const handleSort = (field: string, order: 'asc' | 'desc' | null) => {
if (!order) {
setData(mockMarks); // 重置为原始顺序
setData(dataSource); // 重置为原始顺序
return;
}

View File

@@ -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>

View 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;
};

View 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);
}
};

View 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];

View File

@@ -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>