From e66f7b902d4f0349f062c0b841792d105ad8088b Mon Sep 17 00:00:00 2001 From: lsy Date: Mon, 9 Dec 2024 21:03:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=EF=BC=9A=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E6=A0=87=E7=AD=BE=E5=92=8C=E5=88=86=E7=B1=BB=EF=BC=8C?= =?UTF-8?q?=E5=88=86=E7=A6=BB=E8=87=AA=E5=AE=9A=E4=B9=89=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E5=92=8C=E5=85=83=20=E5=89=8D=E7=AB=AF=EF=BC=9A=E4=BC=98?= =?UTF-8?q?=E5=8C=96Markdown=E8=A7=A3=E6=9E=90=E5=92=8C=E6=8E=92=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/storage/sql/schema.rs | 326 ++++++++------- frontend/hooks/Background.tsx | 28 +- frontend/interface/fields.ts | 140 ++++--- frontend/themes/echoes/post.tsx | 469 ++++++++++++++-------- frontend/themes/echoes/posts.tsx | 189 +++++---- frontend/themes/echoes/styles/layouts.css | 1 + 6 files changed, 669 insertions(+), 484 deletions(-) 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) =>
+\`\`\`markdown **这是粗体文本** -*这是斜体文本* -***这是粗斜体文本*** -~~这是删除线文本~~ - - -普通文本不需要任何特殊标记。 +\`\`\` **这是粗体文本** + +### 1.2 斜体文本 + +\`\`\`markdown *这是斜体文本* +\`\`\` + +*这是斜体文本* + +### 1.3 粗斜体文本 + +\`\`\`markdown ***这是粗斜体文本*** +\`\`\` + +***这是粗斜体文本*** + +### 1.4 删除线文本 + +\`\`\`markdown +~~这是删除线文本~~ +\`\`\` + ~~这是删除线文本~~ -### 1.2 列表 +### 1.5 无序列表 + +\`\`\`markdown +- 第一项 + - 子项 1 + - 子项 2 +- 第二项 +- 第三项 +\`\`\` -
-#### 无序列表:
 - 第一项
   - 子项 1
   - 子项 2
 - 第二项
 - 第三项
 
-#### 有序列表:
+### 1.6 有序列表
+
+\`\`\`markdown
+1. 第一步
+   1. 子步骤 1
+   2. 子步骤 2
+2. 第二步
+3. 第三步
+\`\`\`
+
 1. 第一步
    1. 子步骤 1
    2. 子步骤 2
 2. 第二步
 3. 第三步
 
-#### 任务列表:
+### 1.7 任务列表
+
+\`\`\`markdown
 - [x] 已完成任务
 - [ ] 未完成任务
 - [x] 又一个已完成任务
-
+\`\`\` -#### 无序列表: -- 第一项 - - 子项 1 - - 子项 2 -- 第二项 -- 第三项 - -#### 有序列表: -1. 第一步 - 1. 子步骤 1 - 2. 子步骤 2 -2. 第二步 -3. 第三步 - -#### 任务列表: - [x] 已完成任务 - [ ] 未完成任务 - [x] 又一个已完成任务 -### 1.3 代码展示 +### 1.8 行内代码 -
-行内代码:\`const greeting = "Hello World";\`
+\`\`\`markdown
+这是一段包含\`const greeting = "Hello World";\`的行内代码
+\`\`\`
 
-
+这是一段包含\`const greeting = "Hello World";\`的行内代码 -行内代码:\`const greeting = "Hello World";\` +### 1.9 代码块 -
-代码块:
+\`\`\`\`markdown
 \`\`\`typescript
 interface User {
   id: number;
@@ -114,9 +133,8 @@ function greet(user: User): string {
   return \`Hello, \${user.name}!\`;
 }
 \`\`\`
-
+\`\`\`\` -代码块: \`\`\`typescript interface User { id: number; @@ -129,15 +147,15 @@ function greet(user: User): string { } \`\`\` -### 1.4 表格 +### 1.10 表格 -
+\`\`\`markdown
 | 功能 | 基础版 | 高级版 |
 |:-----|:------:|-------:|
 | 文本编辑 | ✓ | ✓ |
 | 实时预览 | ✗ | ✓ |
 | 导出格式 | 2种 | 5种 |
-
+\`\`\` | 功能 | 基础版 | 高级版 | |:-----|:------:|-------:| @@ -145,6 +163,38 @@ function greet(user: User): string { | 实时预览 | ✗ | ✓ | | 导出格式 | 2种 | 5种 | +### 1.11 引用 + +\`\`\`markdown +> 📌 **最佳实践** +> +> 好的文章需要有清晰的结构和流畅的表达。 +\`\`\` + +> 📌 **最佳实践** +> +> 好的文章需要有清晰的结构和流畅的表达。 + +### 1.12 脚注 + +\`\`\`markdown +这里有一个脚注[^1]。 + +[^1]: 这是脚注的内容。 +\`\`\` + +这里有一个脚注[^1]。 + +[^1]: 这是脚注的内容。 + +### 1.13 表情符号 + +\`\`\`markdown +:smile: :heart: :star: :rocket: +\`\`\` + +:smile: :heart: :star: :rocket: + ## 2. 高级排版 ### 2.1 图文混排布局 @@ -232,12 +282,12 @@ function greet(user: User): string {
 

💡 小贴士

-

在写作时,可以先列出文章大纲,再逐步充实内容。这样可以保证文章结构清晰,内容完整。

+

在写作时,可以先列出文章大纲,再逐步充实内容。这可以保证文章结构清晰,内容完整。

-

💡 小贴士

+

小贴士

在写作时,可以先列出文章大纲,再逐步充实内容。这样可以保证文章结构清晰,内容完整。

@@ -273,7 +323,7 @@ function greet(user: User): string {
-### 2.6 引用样式 +### 2.6 引用式
 > 📌 **最佳实践**
@@ -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'>) => (

), - ol: ({ children, node, ...props }: ComponentPropsWithoutRef<'ol'> & { node?: any }) => ( + ol: ({ children, ...props }: ComponentPropsWithoutRef<'ol'>) => (
    {children}
), - li: ({ children, node, ...props }: ComponentPropsWithoutRef<'li'> & { node?: any }) => ( + li: ({ children, ...props }: ComponentPropsWithoutRef<'li'>) => (
  • {children}
  • ), - blockquote: ({ children, node, ...props }: ComponentPropsWithoutRef<'blockquote'> & { node?: any }) => ( + blockquote: ({ children, ...props }: ComponentPropsWithoutRef<'blockquote'>) => (
    {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()} > -
    - - 目录 - - -
    -
    {tocItems.map((item, index) => { if (item.level > 3) return null; + const isActive = activeId === item.id; + return ( { + // 当目录打开且是当前高亮项时,将其滚动到居中位置 + if (node && isActive && showToc) { + requestAnimationFrame(() => { + // 直接查找最近的滚动容器 + const scrollContainer = node.closest('.rt-ScrollAreaViewport'); + if (scrollContainer) { + const containerHeight = scrollContainer.clientHeight; + const elementTop = node.offsetTop; + const elementHeight = node.clientHeight; + + // 确保计算的滚动位置是正数 + const scrollTop = Math.max(0, elementTop - (containerHeight / 2) + (elementHeight / 2)); + + // 使用 scrollContainer 而不是 container + scrollContainer.scrollTo({ + top: scrollTop, + behavior: 'smooth' + }); + } + }); + } + }} className={` block py-1.5 px-3 rounded transition-colors ${ - activeId === item.id + isActive ? "text-[--accent-11] font-medium bg-[--accent-3]" : "text-[--gray-11] hover:text-[--gray-12] hover:bg-[--gray-3]" } @@ -897,7 +1029,7 @@ export default new Template({}, ({ http, args }) => { ? "text-sm font-medium" : item.level === 2 ? "text-[0.8125rem]" - : `text-xs ${activeId === item.id ? "text-[--accent-11]" : "text-[--gray-10]"}` + : `text-xs ${isActive ? "text-[--accent-11]" : "text-[--gray-10]"}` } `} onClick={(e) => { @@ -924,7 +1056,7 @@ export default new Template({}, ({ http, args }) => { ); - // 在组件顶部添加 useMemo 包裹静态内容 + // 在组件顶部添加 useMemo 包静态内容 const PostContent = useMemo(() => { // 在渲染内容前重置 headingIds if (headingIdsArrays[mockPost.id]) { @@ -947,6 +1079,7 @@ export default new Template({}, ({ http, args }) => { components={components} remarkPlugins={[remarkGfm, remarkEmoji]} rehypePlugins={[rehypeRaw]} + skipHtml={false} > {mockPost.content} @@ -967,7 +1100,7 @@ export default new Template({}, ({ http, args }) => { className="relative flex-col lg:flex-row" gap={{initial: "4", lg: "8"}} > - {/* 文章主体 - 调整宽度计算 */} + {/* 文章体 - 调整宽度计算 */} {/* 头部 */} @@ -1014,11 +1147,11 @@ export default new Template({}, ({ http, args }) => { {/* 分类 */} - {mockPost.categories?.map((category) => { + {mockPost.taxonomies?.categories.map((category) => { const color = getColorScheme(category.name); return ( { {/* 标签 */} - {mockPost.tags?.map((tag) => { + {mockPost.taxonomies?.tags.map((tag) => { const color = getColorScheme(tag.name); return ( { - {/* 面图片 */} + {/* 封面 */} {mockPost.coverImage && ( { className="scroll-container flex-1" > - {article.categories?.map((category) => ( + {article.taxonomies?.categories.map((category) => ( {
    - {article.tags?.map((tag: Tag) => ( + {article.taxonomies?.tags.map((tag) => (