diff --git a/backend/src/storage/sql/schema.rs b/backend/src/storage/sql/schema.rs index 4a2c376..8d63200 100644 --- a/backend/src/storage/sql/schema.rs +++ b/backend/src/storage/sql/schema.rs @@ -396,12 +396,6 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes FieldConstraint::new().unique().not_null(), ValidationLevel::Strict, )?) - .add_field(Field::new( - "profile_icon", - FieldType::VarChar(255), - FieldConstraint::new(), - ValidationLevel::Strict, - )?) .add_field(Field::new( "password_hash", FieldType::VarChar(255), @@ -444,7 +438,9 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes .add_field(Field::new( "last_login_at", FieldType::Timestamp, - FieldConstraint::new().default(SafeValue::Text( + FieldConstraint::new() + .not_null() + .default(SafeValue::Text( "CURRENT_TIMESTAMP".to_string(), ValidationLevel::Strict, )), @@ -469,18 +465,6 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes FieldConstraint::new().not_null(), ValidationLevel::Strict, )?) - .add_field(Field::new( - "meta_keywords", - FieldType::VarChar(255), - FieldConstraint::new().not_null(), - ValidationLevel::Strict, - )?) - .add_field(Field::new( - "meta_description", - FieldType::VarChar(255), - FieldConstraint::new().not_null(), - ValidationLevel::Strict, - )?) .add_field(Field::new( "content", FieldType::Text, @@ -493,12 +477,6 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes FieldConstraint::new(), ValidationLevel::Strict, )?) - .add_field(Field::new( - "custom_fields", - FieldType::Text, - FieldConstraint::new(), - ValidationLevel::Strict, - )?) .add_field(Field::new( "status", FieldType::VarChar(20), @@ -548,18 +526,6 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes FieldConstraint::new(), ValidationLevel::Strict, )?) - .add_field(Field::new( - "meta_keywords", - FieldType::VarChar(255), - FieldConstraint::new().not_null(), - ValidationLevel::Strict, - )?) - .add_field(Field::new( - "meta_description", - FieldType::VarChar(255), - FieldConstraint::new().not_null(), - ValidationLevel::Strict, - )?) .add_field(Field::new( "content", FieldType::Text, @@ -622,108 +588,6 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes schema.add_table(posts_table)?; - // 标签表 - let mut tags_tables = Table::new(&format!("{}tags", db_prefix))?; - tags_tables - .add_field(Field::new( - "name", - FieldType::VarChar(50), - FieldConstraint::new().primary(), - ValidationLevel::Strict, - )?) - .add_field(Field::new( - "icon", - FieldType::VarChar(255), - FieldConstraint::new(), - ValidationLevel::Strict, - )?); - - schema.add_table(tags_tables)?; - - // 文章标签 - let mut post_tags_tables = Table::new(&format!("{}post_tags", db_prefix))?; - post_tags_tables - .add_field(Field::new( - "post_id", - FieldType::Integer(false), - FieldConstraint::new() - .not_null() - .foreign_key(format!("{}posts", db_prefix), "id".to_string()) - .on_delete(ForeignKeyAction::Cascade) - .on_update(ForeignKeyAction::Cascade), - ValidationLevel::Strict, - )?) - .add_field(Field::new( - "tag_id", - FieldType::VarChar(50), - FieldConstraint::new() - .not_null() - .foreign_key(format!("{}tags", db_prefix), "name".to_string()) - .on_delete(ForeignKeyAction::Cascade) - .on_update(ForeignKeyAction::Cascade), - ValidationLevel::Strict, - )?); - - post_tags_tables.add_index(Index::new( - "pk_post_tags", - vec!["post_id".to_string(), "tag_id".to_string()], - true, - )?); - - schema.add_table(post_tags_tables)?; - - // 分类表 - - let mut categories_table = Table::new(&format!("{}categories", db_prefix))?; - categories_table - .add_field(Field::new( - "name", - FieldType::VarChar(50), - FieldConstraint::new().primary(), - ValidationLevel::Strict, - )?) - .add_field(Field::new( - "parent_id", - FieldType::VarChar(50), - FieldConstraint::new() - .foreign_key(format!("{}categories", db_prefix), "name".to_string()), - ValidationLevel::Strict, - )?); - - schema.add_table(categories_table)?; - - // 文章分类关联表 - let mut post_categories_table = Table::new(&format!("{}post_categories", db_prefix))?; - post_categories_table - .add_field(Field::new( - "post_id", - FieldType::Integer(false), - FieldConstraint::new() - .not_null() - .foreign_key(format!("{}posts", db_prefix), "id".to_string()) - .on_delete(ForeignKeyAction::Cascade) - .on_update(ForeignKeyAction::Cascade), - ValidationLevel::Strict, - )?) - .add_field(Field::new( - "category_id", - FieldType::VarChar(50), - FieldConstraint::new() - .not_null() - .foreign_key(format!("{}categories", db_prefix), "name".to_string()) - .on_delete(ForeignKeyAction::Cascade) - .on_update(ForeignKeyAction::Cascade), - ValidationLevel::Strict, - )?); - - post_categories_table.add_index(Index::new( - "pk_post_categories", - vec!["post_id".to_string(), "category_id".to_string()], - true, - )?); - - schema.add_table(post_categories_table)?; - // 资源库表 let mut resources_table = Table::new(&format!("{}resources", db_prefix))?; resources_table @@ -762,7 +626,7 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes ValidationLevel::Strict, )?) .add_field(Field::new( - "file_type", + "mime_type", FieldType::VarChar(50), FieldConstraint::new().not_null(), ValidationLevel::Strict, @@ -809,5 +673,187 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes schema.add_table(settings_table)?; + // 元数据表 + let mut metadata_table = Table::new(&format!("{}metadata", db_prefix))?; + metadata_table + .add_field(Field::new( + "id", + FieldType::Integer(true), + FieldConstraint::new().primary(), + ValidationLevel::Strict, + )?).add_field(Field::new( + "target_type", + FieldType::VarChar(20), + FieldConstraint::new() + .not_null() + .check(WhereClause::Condition(Condition::new( + "target_type".to_string(), + Operator::In, + Some(SafeValue::Text( + "('post', 'page')".to_string(), + ValidationLevel::Standard, + )), + )?)), + ValidationLevel::Strict, + )?).add_field(Field::new( + "target_id", + FieldType::Integer(false), + FieldConstraint::new() + .not_null() + .check(WhereClause::Condition(Condition::new( + "(target_type = 'post' AND EXISTS (SELECT 1 FROM posts WHERE id = target_id)) OR \ + (target_type = 'page' AND EXISTS (SELECT 1 FROM pages WHERE id = target_id))".to_string(), + Operator::Raw, + None, + )?)), + ValidationLevel::Strict, + )?).add_field(Field::new( + "meta_key", + FieldType::VarChar(50), + FieldConstraint::new().not_null(), + ValidationLevel::Strict, + )?).add_field(Field::new( + "meta_value", + FieldType::Text, + FieldConstraint::new(), + ValidationLevel::Strict, + )?); + + metadata_table.add_index(Index::new( + "idx_metadata_target", + vec!["target_type".to_string(), "target_id".to_string()], + false, + )?); + + schema.add_table(metadata_table)?; + + // 自定义字段表 + let mut custom_fields_table = Table::new(&format!("{}custom_fields", db_prefix))?; + custom_fields_table + .add_field(Field::new( + "id", + FieldType::Integer(true), + FieldConstraint::new().primary(), + ValidationLevel::Strict, + )?).add_field(Field::new( + "target_type", + FieldType::VarChar(20), + FieldConstraint::new() + .not_null() + .check(WhereClause::Condition(Condition::new( + "target_type".to_string(), + Operator::In, + Some(SafeValue::Text( + "('post', 'page')".to_string(), + ValidationLevel::Standard, + )), + )?)), + ValidationLevel::Strict, + )?).add_field(Field::new( + "target_id", + FieldType::Integer(false), + FieldConstraint::new().not_null(), + ValidationLevel::Strict, + )?).add_field(Field::new( + "field_key", + FieldType::VarChar(50), + FieldConstraint::new().not_null(), + ValidationLevel::Strict, + )?).add_field(Field::new( + "field_value", + FieldType::Text, + FieldConstraint::new(), + ValidationLevel::Strict, + )?).add_field(Field::new( + "field_type", + FieldType::VarChar(20), + FieldConstraint::new().not_null(), + ValidationLevel::Strict, + )?); + + custom_fields_table.add_index(Index::new( + "idx_custom_fields_target", + vec!["target_type".to_string(), "target_id".to_string()], + false, + )?); + + schema.add_table(custom_fields_table)?; + + // 在 generate_schema 函数中,删除原有的 tags_tables 和 categories_table + // 替换为新的 taxonomies 表 + let mut taxonomies_table = Table::new(&format!("{}taxonomies", db_prefix))?; + taxonomies_table + .add_field(Field::new( + "name", + FieldType::VarChar(50), + FieldConstraint::new().primary(), + ValidationLevel::Strict, + )?).add_field(Field::new( + "slug", + FieldType::VarChar(50), + FieldConstraint::new().not_null().unique(), + ValidationLevel::Strict, + )?).add_field(Field::new( + "type", + FieldType::VarChar(20), + FieldConstraint::new() + .not_null() + .check(WhereClause::Condition(Condition::new( + "type".to_string(), + Operator::In, + Some(SafeValue::Text( + "('tag', 'category')".to_string(), + ValidationLevel::Standard, + )), + )?)), + ValidationLevel::Strict, + )?).add_field(Field::new( + "parent_id", + FieldType::VarChar(50), + FieldConstraint::new() + .foreign_key(format!("{}taxonomies", db_prefix), "name".to_string()) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade) + .check(WhereClause::Condition(Condition::new( + "(type = 'category' OR parent_id IS NULL)".to_string(), + Operator::Raw, + None, + )?)), + ValidationLevel::Strict, + )?); + + schema.add_table(taxonomies_table)?; + + // 替换为新的 post_taxonomies 表 + let mut post_taxonomies_table = Table::new(&format!("{}post_taxonomies", db_prefix))?; + post_taxonomies_table + .add_field(Field::new( + "post_id", + FieldType::Integer(false), + FieldConstraint::new() + .not_null() + .foreign_key(format!("{}posts", db_prefix), "id".to_string()) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ValidationLevel::Strict, + )?).add_field(Field::new( + "taxonomy_id", + FieldType::VarChar(50), + FieldConstraint::new() + .not_null() + .foreign_key(format!("{}taxonomies", db_prefix), "name".to_string()) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ValidationLevel::Strict, + )?); + + post_taxonomies_table.add_index(Index::new( + "pk_post_taxonomies", + vec!["post_id".to_string(), "taxonomy_id".to_string()], + true, + )?); + + schema.add_table(post_taxonomies_table)?; + schema.build(db_type) } diff --git a/frontend/hooks/Background.tsx b/frontend/hooks/Background.tsx index 6e80842..22363ef 100644 --- a/frontend/hooks/Background.tsx +++ b/frontend/hooks/Background.tsx @@ -10,12 +10,16 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) => const { mode } = useThemeMode(); useEffect(() => { + const canvas = canvasRef.current; if (!canvas) { onError?.(); return; } + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + try { const ctx = canvas.getContext('2d', { alpha: true, @@ -28,14 +32,14 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) => return; } - // 添加非空断言 - const context = ctx!; + const context = ctx; - // 添加必要的变量定义 const getRandomHSLColor = () => { const hue = Math.random() * 360; - const saturation = 70 + Math.random() * 30; - const lightness = mode === 'dark' ? 40 + Math.random() * 20 : 60 + Math.random() * 20; + const saturation = 90 + Math.random() * 10; + const lightness = mode === 'dark' + ? 50 + Math.random() * 15 // 暗色模式:50-65% + : 60 + Math.random() * 15; // 亮色模式:60-75% return `hsl(${hue}, ${saturation}%, ${lightness}%)`; }; @@ -46,7 +50,6 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) => let dx = 0.2; let dy = -0.2; - // 添加 drawBall 函数 function drawBall() { context.beginPath(); context.arc(x, y, ballRadius, 0, Math.PI * 2); @@ -55,11 +58,6 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) => context.closePath(); } - // 设置 canvas 尺寸 - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - - // 性能优化:降低动画帧率 const fps = 30; const interval = 1000 / fps; let then = Date.now(); @@ -69,10 +67,8 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) => const delta = now - then; if (delta > interval) { - // 更新时间戳 then = now - (delta % interval); - // 绘制逻辑... context.clearRect(0, 0, canvas.width, canvas.height); drawBall(); @@ -87,14 +83,12 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) => y += dy; } - // 使用 requestAnimationFrame 代替 setInterval animationFrameId = requestAnimationFrame(draw); }; let animationFrameId: number; draw(); - // 清理函数 return () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); @@ -111,9 +105,9 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) =>
> 📌 **最佳实践** @@ -327,36 +377,63 @@ function greet(user: User): string { 2. 高级排版:图文混排、折叠面板、卡片布局等 3. 特殊语法:数学公式、脚注、表情符号等 -> 💡 **提示**:部分高级排版功能可能需要特定的 Markdown 编辑器或渲染器支持,请确认是否支持这些功能。 +> 💡 **提示**:部分高级排版功能可能需要特定的 Markdown 编辑器或渲染支持,请确认是否支持这些功能。 `, authorName: "Markdown 专家", publishedAt: new Date("2024-03-15"), coverImage: "https://images.unsplash.com/photo-1499951360447-b19be8fe80f5?w=1200&h=600", - metaKeywords: "Markdown,基础语法,高级排版,布局设计", - metaDescription: "从基础语法到高级排版,全面了解 Markdown 的各种用法和技巧。", status: "published", isEditor: true, createdAt: new Date("2024-03-15"), updatedAt: new Date("2024-03-15"), - categories: [{ name: "教程" }], - tags: [{ name: "Markdown" }, { name: "排版" }, { name: "写作" }], + taxonomies: { + categories: [{ + name: "教程", + slug: "tutorial", + type: "category" + }], + tags: [ + { name: "Markdown", slug: "markdown", type: "tag" }, + { name: "排版", slug: "typography", type: "tag" }, + { name: "写作", slug: "writing", type: "tag" } + ] + }, + metadata: [ + { + id: 1, + targetType: "post", + targetId: 1, + metaKey: "description", + metaValue: "从基础语法到高级排版,全面了解 Markdown 的各种用法和技巧。" + }, + { + id: 2, + targetType: "post", + targetId: 1, + metaKey: "keywords", + metaValue: "Markdown,基础语法,高级排版,布局设计" + } + ] }; // 添 meta 函数 export const meta: MetaFunction = () => { + const description = mockPost.metadata?.find(m => m.metaKey === "description")?.metaValue || ""; + const keywords = mockPost.metadata?.find(m => m.metaKey === "keywords")?.metaValue || ""; + return [ { title: mockPost.title }, - { name: "description", content: mockPost.metaDescription }, - { name: "keywords", content: mockPost.metaKeywords }, - // 添加 Open Graph 标 + { name: "description", content: description }, + { name: "keywords", content: keywords }, + // 添 Open Graph 标 { property: "og:title", content: mockPost.title }, - { property: "og:description", content: mockPost.metaDescription }, + { property: "og:description", content: description }, { property: "og:image", content: mockPost.coverImage }, { property: "og:type", content: "article" }, // 添加 Twitter 卡片标签 { name: "twitter:card", content: "summary_large_image" }, { name: "twitter:title", content: mockPost.title }, - { name: "twitter:description", content: mockPost.metaDescription }, + { name: "twitter:description", content: description }, { name: "twitter:image", content: mockPost.coverImage }, ]; }; @@ -510,7 +587,7 @@ export default new Template({}, ({ http, args }) => { const components = useMemo(() => { return { - h1: ({ children, node, ...props }: ComponentPropsWithoutRef<'h1'> & { node?: any }) => { + h1: ({ children, ...props }: ComponentPropsWithoutRef<'h1'>) => { const headingId = headingIds.current.shift(); return (@@ -518,7 +595,7 @@ export default new Template({}, ({ http, args }) => {
); }, - h2: ({ children, node, ...props }: ComponentPropsWithoutRef<'h2'> & { node?: any }) => { + h2: ({ children, ...props }: ComponentPropsWithoutRef<'h2'>) => { const headingId = headingIds.current.shift(); return (@@ -526,7 +603,7 @@ export default new Template({}, ({ http, args }) => {
); }, - h3: ({ children, node, ...props }: ComponentPropsWithoutRef<'h3'> & { node?: any }) => { + h3: ({ children, ...props }: ComponentPropsWithoutRef<'h3'>) => { const headingId = headingIds.current.shift(); return (@@ -534,45 +611,100 @@ export default new Template({}, ({ http, args }) => {
); }, - p: ({ children, node, ...props }: ComponentPropsWithoutRef<'p'> & { node?: any }) => ( -- {children} -
+ p: ({ node, ...props }: ComponentPropsWithoutRef<'p'> & { node?: any }) => ( + ), - ul: ({ children, node, ...props }: ComponentPropsWithoutRef<'ul'> & { node?: any }) => ( + ul: ({ children, ...props }: ComponentPropsWithoutRef<'ul'>) => (
{children}), - code: ({ inline, className, children, node, ...props }: ComponentPropsWithoutRef<'code'> & { - inline?: boolean, - node?: any - }) => { - // 使用多个条件来确保服务端和客户端渲染一致 - const isInPre = Boolean( - className?.includes('language-') - ); + pre: ({ children, ...props }: ComponentPropsWithoutRef<'pre'>) => { + const childArray = React.Children.toArray(children); - // 如果是行内代码(不在 pre 标签内),使用行内样式 - if (!isInPre) { + // 检查是否包含代码块 + const codeElement = childArray.find( + child => React.isValidElement(child) && child.props.className?.includes('language-') + ); + + // 如果是代码块,让 code 组件处理 + if (codeElement) { + return <>{children}>; + } + + // 获取内容 + let content = ''; + if (typeof children === 'string') { + content = children; + } else if (Array.isArray(children)) { + content = children.map(child => { + if (typeof child === 'string') return child; + if (React.isValidElement(child)) { + // 使用 renderToString 而不是 renderToStaticMarkup + return ReactDOMServer.renderToString(child as React.ReactElement) + // 移除 React 添加的 data 属性 + .replace(/\s+data-reactroot=""/g, '') + // 移除已经存在的 HTML 实体编码 + .replace(/"/g, '"') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/'/g, "'"); + } + return ''; + }).join(''); + } else if (React.isValidElement(children)) { + content = ReactDOMServer.renderToString(children as React.ReactElement) + .replace(/\s+data-reactroot=""/g, '') + .replace(/"/g, '"') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/'/g, "'"); + } + + // 普通预格式化文本 + return ( +
+ {content + } ++ ); + }, + code: ({ inline, className, children, ...props }: ComponentPropsWithoutRef<'code'> & { + inline?: boolean, + className?: string + }) => { + const match = /language-(\w+)/.exec(className || ''); + const code = String(children).replace(/\n$/, ''); + + if (!className || inline) { return (
{children}
@@ -580,27 +712,26 @@ export default new Template({}, ({ http, args }) => {
);
}
- // 以下是代码块的处理逻辑
- const match = /language-(\w+)/.exec(className || "");
- const lang = match ? match[1].toLowerCase() : "";
-
+ const language = match ? match[1] : '';
+
return (
- {lang || "text"}
-
+
+ {language || "text"}
+
+
-
+
{
}
}}
>
- {String(children).replace(/\n$/, "")}
+ {code}
@@ -641,7 +772,7 @@ export default new Template({}, ({ http, args }) => {
-
+
{children}
@@ -650,77 +781,66 @@ export default new Template({}, ({ http, args }) => {
),
- th: ({ children, ...props }: ComponentPropsWithoutRef<'th'>) => (
-
- {children}
-
- ),
+ th: ({ children, style, ...props }: ComponentPropsWithoutRef<'th'> & { style?: React.CSSProperties }) => {
+ // 获取对齐方式
+ const getAlignment = () => {
+ if (style?.textAlign === 'center') return 'text-center';
+ if (style?.textAlign === 'right') return 'text-right';
+ return 'text-left';
+ };
+
+ return (
+
+ {children}
+
+ );
+ },
- td: ({ children, ...props }: ComponentPropsWithoutRef<'td'>) => (
-
- {children}
-
- ),
+ td: ({ children, style, ...props }: ComponentPropsWithoutRef<'td'> & { style?: React.CSSProperties }) => {
+ // 获取父级 th 的对齐方式
+ const getAlignment = () => {
+ if (style?.textAlign === 'center') return 'text-center';
+ if (style?.textAlign === 'right') return 'text-right';
+ return 'text-left';
+ };
+
+ return (
+
+ {children}
+
+ );
+ },
// 修改 details 组件
- details: ({ children, ...props }: ComponentPropsWithoutRef<'details'>) => (
+ details: ({ node, ...props }: ComponentPropsWithoutRef<'details'> & { node?: any }) => (
- {children}
-
+ />
),
// 修改 summary 组件
- summary: ({ children, ...props }: ComponentPropsWithoutRef<'summary'>) => (
+ summary: ({ node, ...props }: ComponentPropsWithoutRef<'summary'> & { node?: any }) => (
- {children}
-
+ />
),
- pre: ({ children, ...props }: ComponentPropsWithoutRef<'pre'>) => {
- // 添加调试日志
- console.log('Pre Component Props:', props);
- console.log('Pre Component Children:', children);
-
- // 检查children的具体结构
- if (Array.isArray(children)) {
- children.forEach((child, index) => {
- console.log(`Child ${index}:`, child);
- console.log(`Child ${index} props:`, (child as any)?.props);
- });
- }
-
- const content = (children as any)?.[0]?.props?.children || '';
- console.log('Extracted content:', content);
-
- return (
-
- {/* 直接输出原始内容,不经过 markdown 解析 */}
- {typeof content === 'string' ? content : children}
-
- );
- },
};
}, []);
@@ -850,7 +970,7 @@ export default new Template({}, ({ http, args }) => {
{isMounted && showToc && (
setShowToc(false)}
>
{
translate-x-0 animate-in slide-in-from-right"
onClick={e => e.stopPropagation()}
>
-
-
- 目录
-
-
-
-
- {article.tags?.map((tag: Tag) => (
+ {article.taxonomies?.tags.map((tag) => (