update
This commit is contained in:
@@ -31,11 +31,14 @@
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.18",
|
||||
"es-toolkit": "^1.40.0",
|
||||
"events": "^3.3.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",
|
||||
@@ -50,11 +53,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"
|
||||
},
|
||||
|
||||
@@ -13,6 +13,10 @@ const tabs = [
|
||||
{
|
||||
key: 'world',
|
||||
title: '世界'
|
||||
},
|
||||
{
|
||||
key: 'docs',
|
||||
title: '文档'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -35,6 +39,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 +59,10 @@ export const BaseApp = () => {
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === tab.key
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${activeTab === tab.key
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
{tab.title}
|
||||
</button>
|
||||
|
||||
@@ -3,7 +3,87 @@ import { nanoid, customAlphabet } from 'nanoid';
|
||||
|
||||
export const random = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
||||
|
||||
// 类型定义
|
||||
// 确保类型定义
|
||||
const ensureType = ['markdown', 'json', 'html', 'image', 'video', 'audio', 'code', 'link', 'file'];
|
||||
export type MarkEnsureType = typeof ensureType[number];
|
||||
|
||||
// 根据新的类型定义
|
||||
export type Mark<T = any> = {
|
||||
/**
|
||||
* 标记ID
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
tags?: string[];
|
||||
|
||||
/**
|
||||
* 标记类型
|
||||
*/
|
||||
markType?: string;
|
||||
|
||||
/**
|
||||
* 封面
|
||||
*/
|
||||
cover?: string;
|
||||
|
||||
/**
|
||||
* 链接
|
||||
*/
|
||||
link?: string;
|
||||
|
||||
/**
|
||||
* 摘要
|
||||
*/
|
||||
summary?: string;
|
||||
|
||||
/**
|
||||
* 键
|
||||
*/
|
||||
key?: string;
|
||||
data: T;
|
||||
|
||||
/**
|
||||
* 附件列表
|
||||
*/
|
||||
fileList?: any[];
|
||||
/**
|
||||
* 创建人信息
|
||||
*/
|
||||
uname?: string;
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
version?: number;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
createdAt: Date;
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
updatedAt: Date;
|
||||
/**
|
||||
* 标记时间
|
||||
*/
|
||||
markedAt?: Date;
|
||||
uid?: string;
|
||||
puid?: string;
|
||||
};
|
||||
|
||||
// 保留原有的辅助类型
|
||||
export type MarkDataNode = {
|
||||
id?: string;
|
||||
content?: string;
|
||||
@@ -20,64 +100,25 @@ export type MarkFile = {
|
||||
name: string;
|
||||
url: string;
|
||||
size: number;
|
||||
type: 'self' | 'data' | 'generate'; // generate为生成文件
|
||||
query: string; // 'data.nodes[id].content';
|
||||
type: 'self' | 'data' | 'generate';
|
||||
query: string;
|
||||
hash: string;
|
||||
fileKey: string; // 文件的名称, 唯一
|
||||
fileKey: string;
|
||||
};
|
||||
|
||||
export type MarkData = {
|
||||
md?: string; // markdown
|
||||
mdList?: string[]; // markdown list
|
||||
type?: string; // 类型 markdown | json | html | image | video | audio | code | link | file
|
||||
md?: string;
|
||||
mdList?: string[];
|
||||
type?: MarkEnsureType;
|
||||
data?: any;
|
||||
key?: string; // 文件的名称, 唯一
|
||||
push?: boolean; // 是否推送到elasticsearch
|
||||
pushTime?: Date; // 推送时间
|
||||
summary?: string; // 摘要
|
||||
nodes?: MarkDataNode[]; // 节点
|
||||
key?: string;
|
||||
push?: boolean;
|
||||
pushTime?: Date;
|
||||
summary?: string;
|
||||
nodes?: MarkDataNode[];
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type MarkConfig = {
|
||||
visibility?: 'public' | 'private' | 'restricted';
|
||||
allowComments?: boolean;
|
||||
allowDownload?: boolean;
|
||||
password?: string;
|
||||
expiredAt?: Date;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type MarkAuth = {
|
||||
permissions?: string[];
|
||||
roles?: string[];
|
||||
userId?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type Mark = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
cover: string;
|
||||
thumbnail: string;
|
||||
key: string;
|
||||
markType: string;
|
||||
link: string;
|
||||
tags: string[];
|
||||
summary: string;
|
||||
data: MarkData;
|
||||
uid: string;
|
||||
puid: string;
|
||||
config: MarkConfig;
|
||||
fileList: MarkFile[];
|
||||
uname: string;
|
||||
markedAt: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
version: number;
|
||||
};
|
||||
|
||||
// 生成模拟的 MarkDataNode
|
||||
const generateMarkDataNode = (): MarkDataNode => ({
|
||||
id: random(12),
|
||||
@@ -99,85 +140,187 @@ const generateMarkDataNode = (): MarkDataNode => ({
|
||||
}
|
||||
});
|
||||
|
||||
// 生成模拟的 MarkFile
|
||||
const generateMarkFile = (): MarkFile => ({
|
||||
// 生成模拟的附件
|
||||
const generateFileAttachment = (): any => ({
|
||||
id: faker.string.uuid(),
|
||||
name: faker.system.fileName(),
|
||||
url: faker.internet.url(),
|
||||
size: faker.number.int({ min: 1024, max: 50 * 1024 * 1024 }), // 1KB to 50MB
|
||||
type: faker.helpers.arrayElement(['self', 'data', 'generate']),
|
||||
query: `data.nodes[${random(12)}].content`,
|
||||
size: faker.number.int({ min: 1024, max: 50 * 1024 * 1024 }),
|
||||
type: faker.helpers.arrayElement(['image', 'document', 'video', 'audio']),
|
||||
mimeType: faker.system.mimeType(),
|
||||
hash: faker.git.commitSha(),
|
||||
fileKey: faker.system.fileName()
|
||||
uploadedAt: faker.date.recent()
|
||||
});
|
||||
|
||||
// 生成模拟的 MarkData
|
||||
const generateMarkData = (): MarkData => ({
|
||||
md: faker.lorem.paragraphs(3, '\n\n'),
|
||||
mdList: Array.from({ length: faker.number.int({ min: 3, max: 8 }) }, () => faker.lorem.sentence()),
|
||||
type: faker.helpers.arrayElement(['markdown', 'json', 'html', 'image', 'video', 'audio', 'code', 'link', 'file']),
|
||||
data: {
|
||||
author: faker.person.fullName(),
|
||||
category: faker.helpers.arrayElement(['技术', '生活', '工作', '学习', '思考']),
|
||||
priority: faker.helpers.arrayElement(['low', 'medium', 'high'])
|
||||
},
|
||||
key: faker.system.fileName(),
|
||||
push: faker.datatype.boolean(),
|
||||
pushTime: faker.date.recent(),
|
||||
summary: faker.lorem.paragraph(),
|
||||
nodes: Array.from({ length: faker.number.int({ min: 2, max: 6 }) }, generateMarkDataNode)
|
||||
});
|
||||
|
||||
// 生成模拟的 MarkConfig
|
||||
const generateMarkConfig = (): MarkConfig => ({
|
||||
visibility: faker.helpers.arrayElement(['public', 'private', 'restricted']),
|
||||
allowComments: faker.datatype.boolean(),
|
||||
allowDownload: faker.datatype.boolean(),
|
||||
password: faker.datatype.boolean() ? faker.internet.password() : undefined,
|
||||
expiredAt: faker.datatype.boolean() ? faker.date.future() : undefined,
|
||||
theme: faker.helpers.arrayElement(['light', 'dark', 'auto']),
|
||||
language: faker.helpers.arrayElement(['zh-CN', 'en-US', 'ja-JP'])
|
||||
});
|
||||
// 生成模拟的 MarkData(通用数据类型)
|
||||
const generateMarkData = <T = any>(): T => {
|
||||
const dataVariants = [
|
||||
// Markdown 数据
|
||||
{
|
||||
content: faker.lorem.paragraphs(3, '\n\n'),
|
||||
format: 'markdown',
|
||||
wordCount: faker.number.int({ min: 100, max: 1000 })
|
||||
},
|
||||
// JSON 数据
|
||||
{
|
||||
jsonData: {
|
||||
name: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
settings: {
|
||||
theme: faker.helpers.arrayElement(['light', 'dark']),
|
||||
language: faker.helpers.arrayElement(['zh-CN', 'en-US', 'ja-JP'])
|
||||
}
|
||||
},
|
||||
schema: 'user-profile'
|
||||
},
|
||||
// 图片数据
|
||||
{
|
||||
src: faker.image.url(),
|
||||
alt: faker.lorem.sentence(),
|
||||
width: faker.number.int({ min: 400, max: 1920 }),
|
||||
height: faker.number.int({ min: 300, max: 1080 }),
|
||||
format: faker.helpers.arrayElement(['jpg', 'png', 'webp'])
|
||||
},
|
||||
// 代码数据
|
||||
{
|
||||
code: `function ${faker.hacker.noun()}() {\n return "${faker.hacker.phrase()}";\n}`,
|
||||
language: faker.helpers.arrayElement(['javascript', 'typescript', 'python', 'java']),
|
||||
lineCount: faker.number.int({ min: 10, max: 100 })
|
||||
},
|
||||
// 链接数据
|
||||
{
|
||||
url: faker.internet.url(),
|
||||
title: faker.lorem.sentence(),
|
||||
description: faker.lorem.paragraph(),
|
||||
favicon: faker.image.url({ width: 32, height: 32 })
|
||||
}
|
||||
];
|
||||
|
||||
return faker.helpers.arrayElement(dataVariants) as T;
|
||||
};
|
||||
|
||||
// 生成单个 Mark 记录
|
||||
const generateMark = (): Mark => {
|
||||
const markType = faker.helpers.arrayElement(['markdown', 'json', 'html', 'image', 'video', 'audio', 'code', 'link', 'file']);
|
||||
const title = faker.lorem.sentence({ min: 3, max: 8 });
|
||||
const generateMark = <T = any>(): Mark<T> => {
|
||||
const createdAt = faker.date.past();
|
||||
const updatedAt = faker.date.between({ from: createdAt, to: new Date() });
|
||||
|
||||
return {
|
||||
id: faker.string.uuid(),
|
||||
title,
|
||||
description: faker.lorem.paragraph(),
|
||||
cover: faker.image.url({ width: 800, height: 600 }),
|
||||
thumbnail: faker.image.url({ width: 200, height: 150 }),
|
||||
key: faker.system.filePath(),
|
||||
markType,
|
||||
link: faker.internet.url(),
|
||||
tags: Array.from({ length: faker.number.int({ min: 1, max: 5 }) }, () =>
|
||||
faker.helpers.arrayElement(['技术', '前端', '后端', '设计', 'AI', '工具', '教程', '笔记'])
|
||||
),
|
||||
summary: faker.lorem.sentence(),
|
||||
data: generateMarkData(),
|
||||
uid: faker.string.uuid(),
|
||||
puid: faker.string.uuid(),
|
||||
config: generateMarkConfig(),
|
||||
fileList: Array.from({ length: faker.number.int({ min: 0, max: 4 }) }, generateMarkFile),
|
||||
uname: faker.person.fullName(),
|
||||
markedAt: faker.date.past(),
|
||||
createdAt: faker.date.past(),
|
||||
updatedAt: faker.date.recent(),
|
||||
version: faker.number.int({ min: 1, max: 10 })
|
||||
title: faker.datatype.boolean() ? faker.lorem.sentence({ min: 3, max: 8 }) : undefined,
|
||||
description: faker.datatype.boolean() ? faker.lorem.paragraph() : undefined,
|
||||
tags: faker.datatype.boolean()
|
||||
? Array.from({ length: faker.number.int({ min: 1, max: 5 }) }, () =>
|
||||
faker.helpers.arrayElement(['技术', '前端', '后端', '设计', 'AI', '工具', '教程', '笔记', '生活', '工作'])
|
||||
)
|
||||
: undefined,
|
||||
markType: faker.datatype.boolean()
|
||||
? faker.helpers.arrayElement(ensureType)
|
||||
: undefined,
|
||||
cover: faker.datatype.boolean()
|
||||
? faker.image.url({ width: 800, height: 600 })
|
||||
: undefined,
|
||||
link: faker.datatype.boolean()
|
||||
? faker.internet.url()
|
||||
: undefined,
|
||||
summary: faker.datatype.boolean()
|
||||
? faker.lorem.sentence()
|
||||
: undefined,
|
||||
key: faker.datatype.boolean()
|
||||
? faker.system.filePath()
|
||||
: undefined,
|
||||
data: generateMarkData<T>(),
|
||||
fileList: faker.datatype.boolean()
|
||||
? Array.from({ length: faker.number.int({ min: 1, max: 4 }) }, generateFileAttachment)
|
||||
: undefined,
|
||||
uname: faker.datatype.boolean()
|
||||
? faker.person.fullName()
|
||||
: undefined,
|
||||
version: faker.datatype.boolean()
|
||||
? faker.number.int({ min: 1, max: 10 })
|
||||
: undefined,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
markedAt: faker.datatype.boolean()
|
||||
? faker.date.between({ from: createdAt, to: updatedAt })
|
||||
: undefined,
|
||||
uid: faker.datatype.boolean()
|
||||
? faker.string.uuid()
|
||||
: undefined,
|
||||
puid: faker.datatype.boolean()
|
||||
? faker.string.uuid()
|
||||
: undefined
|
||||
};
|
||||
};
|
||||
|
||||
// 生成指定类型的 Mark 记录
|
||||
const generateMarkWithType = (markType: MarkEnsureType): Mark => {
|
||||
const mark = generateMark();
|
||||
mark.markType = markType;
|
||||
|
||||
// 根据类型生成相应的数据
|
||||
switch (markType) {
|
||||
case 'markdown':
|
||||
mark.data = {
|
||||
content: faker.lorem.paragraphs(faker.number.int({ min: 2, max: 5 }), '\n\n'),
|
||||
format: 'markdown',
|
||||
wordCount: faker.number.int({ min: 100, max: 1000 })
|
||||
};
|
||||
break;
|
||||
case 'json':
|
||||
mark.data = {
|
||||
jsonData: {
|
||||
id: faker.string.uuid(),
|
||||
name: faker.person.fullName(),
|
||||
settings: {
|
||||
theme: faker.helpers.arrayElement(['light', 'dark']),
|
||||
notifications: faker.datatype.boolean()
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
case 'image':
|
||||
mark.data = {
|
||||
src: faker.image.url(),
|
||||
alt: faker.lorem.sentence(),
|
||||
width: faker.number.int({ min: 400, max: 1920 }),
|
||||
height: faker.number.int({ min: 300, max: 1080 })
|
||||
};
|
||||
break;
|
||||
case 'video':
|
||||
mark.data = {
|
||||
src: faker.internet.url() + '/video.mp4',
|
||||
duration: faker.number.int({ min: 30, max: 3600 }),
|
||||
resolution: faker.helpers.arrayElement(['720p', '1080p', '4K'])
|
||||
};
|
||||
break;
|
||||
case 'code':
|
||||
mark.data = {
|
||||
code: `function ${faker.hacker.noun()}() {\n return "${faker.hacker.phrase()}";\n}`,
|
||||
language: faker.helpers.arrayElement(['javascript', 'typescript', 'python', 'java']),
|
||||
lineCount: faker.number.int({ min: 10, max: 100 })
|
||||
};
|
||||
break;
|
||||
default:
|
||||
mark.data = generateMarkData();
|
||||
}
|
||||
|
||||
return mark;
|
||||
};
|
||||
|
||||
// 生成 20 条模拟数据
|
||||
export const mockMarks: Mark[] = Array.from({ length: 20 }, generateMark);
|
||||
export const mockMarks: Mark[] = Array.from({ length: 20 }, () => generateMark());
|
||||
|
||||
// 生成每种类型的示例数据
|
||||
export const mockMarksByType: Record<MarkEnsureType, Mark> = ensureType.reduce((acc, type) => {
|
||||
acc[type] = generateMarkWithType(type);
|
||||
return acc;
|
||||
}, {} as Record<MarkEnsureType, Mark>);
|
||||
|
||||
// 导出生成器函数
|
||||
export {
|
||||
generateMark,
|
||||
generateMarkWithType,
|
||||
generateMarkData,
|
||||
generateMarkFile,
|
||||
generateMarkDataNode,
|
||||
generateMarkConfig
|
||||
generateFileAttachment
|
||||
};
|
||||
@@ -26,7 +26,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 +37,31 @@ export const Base = () => {
|
||||
dataIndex: 'markType',
|
||||
width: 100,
|
||||
sortable: true,
|
||||
render: (value: string) => (
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: getTypeColor(value),
|
||||
color: '#fff',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
)
|
||||
render: (value: string) => {
|
||||
if (!value) return ''
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: getTypeColor(value),
|
||||
color: '#fff',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'tags',
|
||||
title: '标签',
|
||||
dataIndex: 'tags',
|
||||
width: 200,
|
||||
render: (tags: string[]) => (
|
||||
render: (tags: string[] = []) => (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
|
||||
{tags.slice(0, 3).map((tag, index) => (
|
||||
{tags?.slice?.(0, 3).map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
style={{
|
||||
@@ -96,13 +99,13 @@ export const Base = () => {
|
||||
render: (value: Date) => new Date(value).toLocaleString('zh-CN')
|
||||
},
|
||||
{
|
||||
key: 'config.visibility',
|
||||
key: 'data.visibility',
|
||||
title: '可见性',
|
||||
dataIndex: 'config.visibility',
|
||||
dataIndex: 'data.visibility',
|
||||
width: 100,
|
||||
render: (value: string) => (
|
||||
<span style={{
|
||||
color: value === 'public' ? '#52c41a' : value === 'private' ? '#ff4d4f' : '#faad14'
|
||||
<span style={{
|
||||
color: value === 'public' ? '#52c41a' : value === 'private' ? '#ff4d4f' : '#faad14'
|
||||
}}>
|
||||
{value === 'public' ? '公开' : value === 'private' ? '私有' : '受限'}
|
||||
</span>
|
||||
@@ -180,7 +183,7 @@ export const Base = () => {
|
||||
// 处理批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRowKeys.length === 0) return;
|
||||
|
||||
|
||||
if (window.confirm(`确定要删除选中的 ${selectedRowKeys.length} 项吗?`)) {
|
||||
setData(prevData => prevData.filter(item => !selectedRowKeys.includes(item.id)));
|
||||
setSelectedRowKeys([]);
|
||||
@@ -198,10 +201,10 @@ export const Base = () => {
|
||||
const getNestedValue = (obj: any, path: string) => {
|
||||
return path.split('.').reduce((o, p) => o?.[p], obj);
|
||||
};
|
||||
|
||||
|
||||
const aVal = getNestedValue(a, field);
|
||||
const bVal = getNestedValue(b, field);
|
||||
|
||||
|
||||
if (aVal < bVal) return order === 'asc' ? -1 : 1;
|
||||
if (aVal > bVal) return order === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
@@ -217,7 +220,7 @@ export const Base = () => {
|
||||
total: data.length,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number, range: [number, number]) =>
|
||||
showTotal: (total: number, range: [number, number]) =>
|
||||
`第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
|
||||
onChange: (page: number, size: number) => {
|
||||
setCurrentPage(page);
|
||||
@@ -235,17 +238,17 @@ export const Base = () => {
|
||||
</div>
|
||||
|
||||
{selectedRowKeys.length > 0 && (
|
||||
<div style={{
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
backgroundColor: '#e6f7ff',
|
||||
<div style={{
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
backgroundColor: '#e6f7ff',
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<span>已选择 {selectedRowKeys.length} 项</span>
|
||||
<button
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={handleBatchDelete}
|
||||
>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useState } from 'react';
|
||||
import { VadVoice } from './videos/modules/VadVoice.tsx';
|
||||
import { ChatInterface } from './prompts/index.tsx';
|
||||
import { BaseApp } from './base/index.tsx';
|
||||
import { exampleUsage } from './modules/mark-service.ts';
|
||||
|
||||
const LeftPanel = () => {
|
||||
return (
|
||||
@@ -79,6 +80,16 @@ export const MuseApp = () => {
|
||||
>
|
||||
Right Panel
|
||||
</button>
|
||||
<button className="px-3 py-1 rounded text-sm bg-green-500 text-white hover:bg-green-600" onClick={() => {
|
||||
exampleUsage()
|
||||
}}>
|
||||
初始化DB
|
||||
</button>
|
||||
<button className="px-3 py-1 rounded text-sm bg-red-500 text-white hover:bg-red-600" onClick={() => {
|
||||
// 删除DB的逻辑
|
||||
}}>
|
||||
删除DB
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
383
web/src/apps/muse/modules/db.ts
Normal file
383
web/src/apps/muse/modules/db.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
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('数据库未初始化');
|
||||
}
|
||||
|
||||
const transaction = this.db.transaction([STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
|
||||
// 检查索引是否存在
|
||||
const indexNames = ['uid', 'markType', 'tags', 'createdAt', 'title'];
|
||||
const existingIndexes = Array.from(store.indexNames);
|
||||
|
||||
console.log('现有索引:', existingIndexes);
|
||||
|
||||
const missingIndexes = indexNames.filter(name => !existingIndexes.includes(name));
|
||||
|
||||
if (missingIndexes.length > 0) {
|
||||
console.warn('缺少索引:', missingIndexes);
|
||||
console.log('请删除数据库并重新初始化以创建缺少的索引');
|
||||
} else {
|
||||
console.log('所有索引都已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 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];
|
||||
Reference in New Issue
Block a user