数据库:合并标签和分类,分离自定义字段和元

前端:优化Markdown解析和排版
This commit is contained in:
lsy 2024-12-09 21:03:05 +08:00
parent 2ffa4883ae
commit e66f7b902d
6 changed files with 669 additions and 484 deletions

View File

@ -396,12 +396,6 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
FieldConstraint::new().unique().not_null(), FieldConstraint::new().unique().not_null(),
ValidationLevel::Strict, ValidationLevel::Strict,
)?) )?)
.add_field(Field::new(
"profile_icon",
FieldType::VarChar(255),
FieldConstraint::new(),
ValidationLevel::Strict,
)?)
.add_field(Field::new( .add_field(Field::new(
"password_hash", "password_hash",
FieldType::VarChar(255), FieldType::VarChar(255),
@ -444,7 +438,9 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
.add_field(Field::new( .add_field(Field::new(
"last_login_at", "last_login_at",
FieldType::Timestamp, FieldType::Timestamp,
FieldConstraint::new().default(SafeValue::Text( FieldConstraint::new()
.not_null()
.default(SafeValue::Text(
"CURRENT_TIMESTAMP".to_string(), "CURRENT_TIMESTAMP".to_string(),
ValidationLevel::Strict, ValidationLevel::Strict,
)), )),
@ -469,18 +465,6 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
FieldConstraint::new().not_null(), FieldConstraint::new().not_null(),
ValidationLevel::Strict, 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( .add_field(Field::new(
"content", "content",
FieldType::Text, FieldType::Text,
@ -493,12 +477,6 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
FieldConstraint::new(), FieldConstraint::new(),
ValidationLevel::Strict, ValidationLevel::Strict,
)?) )?)
.add_field(Field::new(
"custom_fields",
FieldType::Text,
FieldConstraint::new(),
ValidationLevel::Strict,
)?)
.add_field(Field::new( .add_field(Field::new(
"status", "status",
FieldType::VarChar(20), FieldType::VarChar(20),
@ -548,18 +526,6 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
FieldConstraint::new(), FieldConstraint::new(),
ValidationLevel::Strict, 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( .add_field(Field::new(
"content", "content",
FieldType::Text, FieldType::Text,
@ -622,108 +588,6 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
schema.add_table(posts_table)?; 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))?; let mut resources_table = Table::new(&format!("{}resources", db_prefix))?;
resources_table resources_table
@ -762,7 +626,7 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
ValidationLevel::Strict, ValidationLevel::Strict,
)?) )?)
.add_field(Field::new( .add_field(Field::new(
"file_type", "mime_type",
FieldType::VarChar(50), FieldType::VarChar(50),
FieldConstraint::new().not_null(), FieldConstraint::new().not_null(),
ValidationLevel::Strict, ValidationLevel::Strict,
@ -809,5 +673,187 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
schema.add_table(settings_table)?; 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) schema.build(db_type)
} }

View File

@ -10,12 +10,16 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) =>
const { mode } = useThemeMode(); const { mode } = useThemeMode();
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
if (!canvas) { if (!canvas) {
onError?.(); onError?.();
return; return;
} }
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
try { try {
const ctx = canvas.getContext('2d', { const ctx = canvas.getContext('2d', {
alpha: true, alpha: true,
@ -28,14 +32,14 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) =>
return; return;
} }
// 添加非空断言 const context = ctx;
const context = ctx!;
// 添加必要的变量定义
const getRandomHSLColor = () => { const getRandomHSLColor = () => {
const hue = Math.random() * 360; const hue = Math.random() * 360;
const saturation = 70 + Math.random() * 30; const saturation = 90 + Math.random() * 10;
const lightness = mode === 'dark' ? 40 + Math.random() * 20 : 60 + Math.random() * 20; const lightness = mode === 'dark'
? 50 + Math.random() * 15 // 暗色模式50-65%
: 60 + Math.random() * 15; // 亮色模式60-75%
return `hsl(${hue}, ${saturation}%, ${lightness}%)`; return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}; };
@ -46,7 +50,6 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) =>
let dx = 0.2; let dx = 0.2;
let dy = -0.2; let dy = -0.2;
// 添加 drawBall 函数
function drawBall() { function drawBall() {
context.beginPath(); context.beginPath();
context.arc(x, y, ballRadius, 0, Math.PI * 2); context.arc(x, y, ballRadius, 0, Math.PI * 2);
@ -55,11 +58,6 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) =>
context.closePath(); context.closePath();
} }
// 设置 canvas 尺寸
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// 性能优化:降低动画帧率
const fps = 30; const fps = 30;
const interval = 1000 / fps; const interval = 1000 / fps;
let then = Date.now(); let then = Date.now();
@ -69,10 +67,8 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) =>
const delta = now - then; const delta = now - then;
if (delta > interval) { if (delta > interval) {
// 更新时间戳
then = now - (delta % interval); then = now - (delta % interval);
// 绘制逻辑...
context.clearRect(0, 0, canvas.width, canvas.height); context.clearRect(0, 0, canvas.width, canvas.height);
drawBall(); drawBall();
@ -87,14 +83,12 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) =>
y += dy; y += dy;
} }
// 使用 requestAnimationFrame 代替 setInterval
animationFrameId = requestAnimationFrame(draw); animationFrameId = requestAnimationFrame(draw);
}; };
let animationFrameId: number; let animationFrameId: number;
draw(); draw();
// 清理函数
return () => { return () => {
if (animationFrameId) { if (animationFrameId) {
cancelAnimationFrame(animationFrameId); cancelAnimationFrame(animationFrameId);
@ -111,9 +105,9 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) =>
<div className="fixed inset-0 -z-10 overflow-hidden"> <div className="fixed inset-0 -z-10 overflow-hidden">
<canvas <canvas
ref={canvasRef} ref={canvasRef}
className="w-full h-full opacity-50" className="w-full h-full opacity-70"
style={{ style={{
filter: 'blur(150px)', filter: 'blur(100px)',
position: 'absolute', position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,

View File

@ -1,83 +1,95 @@
// 定义数据库表的字段接口 // 定义数据库表的字段接口
export interface User { export interface User {
username: string; // 用户名 username: string;
avatarUrl?: string; // 头像URL avatarUrl?: string;
email: string; // 邮箱 email: string;
profileIcon?: string; // 个人图标 passwordHash: string;
passwordHash: string; // 密码哈希 role: string;
role: string; // 角色 createdAt: Date;
createdAt: Date; // 创建时间 updatedAt: Date;
updatedAt: Date; // 更新时间 lastLoginAt: Date;
lastLoginAt?: Date; // 上次登录时间
} }
export interface Page { export interface Page {
id: number; // 自增整数 id: number;
title: string; // 标题 title: string;
metaKeywords: string; // 元关键词 content: string;
metaDescription: string; // 元描述 template?: string;
content: string; // 内容 status: string;
template?: string; // 模板
customFields?: string; // 自定义字段
status: string; // 状态
} }
export interface Post { export interface Post {
id: number; // 自增整数 id: number;
authorName: string; // 作者名称 authorName: string;
coverImage?: string; // 封面图片 coverImage?: string;
title?: string; // 标题 title?: string;
metaKeywords: string; // 元关键词 content: string;
metaDescription: string; // 元描述 status: string;
content: string; // 内容 isEditor: boolean;
status: string; // 状态 draftContent?: string;
isEditor: boolean; // 是否为编辑器 createdAt: Date;
draftContent?: string; // 草稿内容 updatedAt: Date;
createdAt: Date; // 创建时间 publishedAt?: Date;
updatedAt: Date; // 更新时间
publishedAt?: Date; // 发布时间
}
export interface Tag {
name: string; // 标签名
icon?: string; // 图标
}
export interface PostTag {
postId: number; // 文章ID
tagId: string; // 标签ID
}
export interface Category {
name: string; // 分类名
parentId?: string; // 父分类ID
}
export interface PostCategory {
postId: number; // 文章ID
categoryId: string; // 分类ID
} }
export interface Resource { export interface Resource {
id: number; // 自增整数 id: number;
authorId: string; // 作者ID authorId: string;
name: string; // 名称 name: string;
sizeBytes: number; // 大小(字节) sizeBytes: number;
storagePath: string; // 存储路径 storagePath: string;
fileType: string; // 文件类型 mimeType: string;
category?: string; // 分类 category?: string;
description?: string; // 描述 description?: string;
createdAt: Date; // 创建时间 createdAt: Date;
} }
export interface Setting { export interface Setting {
name: string; // 设置名 name: string;
data?: string; // 数据 data?: string;
} }
// 添加一个新的接口用于前端展示 export interface Metadata {
id: number;
targetType: 'post' | 'page';
targetId: number;
metaKey: string;
metaValue?: string;
}
export interface CustomField {
id: number;
targetType: 'post' | 'page';
targetId: number;
fieldKey: string;
fieldValue?: string;
fieldType: string;
}
export interface Taxonomy {
name: string;
slug: string;
type: 'tag' | 'category';
parentId?: string;
}
export interface PostTaxonomy {
postId: number;
taxonomyId: string;
}
// 用于前端展示的扩展接口
export interface PostDisplay extends Post { export interface PostDisplay extends Post {
categories?: Category[]; taxonomies?: {
tags?: Tag[]; categories: Taxonomy[];
tags: Taxonomy[];
};
metadata?: Metadata[];
customFields?: CustomField[];
}
export interface PageDisplay extends Page {
metadata?: Metadata[];
customFields?: CustomField[];
} }

View File

@ -22,6 +22,7 @@ import remarkGfm from 'remark-gfm';
import { toast } from "hooks/Notification"; import { toast } from "hooks/Notification";
import rehypeRaw from 'rehype-raw'; import rehypeRaw from 'rehype-raw';
import remarkEmoji from 'remark-emoji'; import remarkEmoji from 'remark-emoji';
import ReactDOMServer from 'react-dom/server';
// 示例文章数据 // 示例文章数据
const mockPost: PostDisplay = { const mockPost: PostDisplay = {
@ -34,75 +35,93 @@ const mockPost: PostDisplay = {
## 1. ## 1.
### 1.1 ### 1.1
<pre> \`\`\`markdown
**** ****
** \`\`\`
******
~~线~~
</pre>
**** ****
### 1.2
\`\`\`markdown
** **
\`\`\`
**
### 1.3
\`\`\`markdown
****** ******
\`\`\`
******
### 1.4 线
\`\`\`markdown
~~线~~
\`\`\`
~~线~~ ~~线~~
### 1.2 ### 1.5
\`\`\`markdown
-
- 1
- 2
-
-
\`\`\`
<pre>
####
- -
- 1 - 1
- 2 - 2
- -
- -
#### ### 1.6
\`\`\`markdown
1.
1. 1
2. 2
2.
3.
\`\`\`
1. 1.
1. 1 1. 1
2. 2 2. 2
2. 2.
3. 3.
#### ### 1.7
\`\`\`markdown
- [x] - [x]
- [ ] - [ ]
- [x] - [x]
</pre> \`\`\`
####
-
- 1
- 2
-
-
####
1.
1. 1
2. 2
2.
3.
####
- [x] - [x]
- [ ] - [ ]
- [x] - [x]
### 1.3 ### 1.8
<pre> \`\`\`markdown
\`const greeting = "Hello World";\` \`const greeting = "Hello World";\`的行内代码
\`\`\`
</pre> \`const greeting = "Hello World";\`的行内代码
\`const greeting = "Hello World";\` ### 1.9
<pre> \`\`\`\`markdown
\`\`\`typescript \`\`\`typescript
interface User { interface User {
id: number; id: number;
@ -114,9 +133,8 @@ function greet(user: User): string {
return \`Hello, \${user.name}!\`; return \`Hello, \${user.name}!\`;
} }
\`\`\` \`\`\`
</pre> \`\`\`\`
\`\`\`typescript \`\`\`typescript
interface User { interface User {
id: number; id: number;
@ -129,15 +147,15 @@ function greet(user: User): string {
} }
\`\`\` \`\`\`
### 1.4 ### 1.10
<pre> \`\`\`markdown
| | | | | | | |
|:-----|:------:|-------:| |:-----|:------:|-------:|
| | | | | | | |
| | | | | | | |
| | 2 | 5 | | | 2 | 5 |
</pre> \`\`\`
| | | | | | | |
|:-----|:------:|-------:| |:-----|:------:|-------:|
@ -145,6 +163,38 @@ function greet(user: User): string {
| | | | | | | |
| | 2 | 5 | | | 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.
### 2.1 ### 2.1
@ -232,12 +282,12 @@ function greet(user: User): string {
<pre> <pre>
<div class="p-6 bg-blue-50 border-l-4 border-blue-500 rounded-lg my-8"> <div class="p-6 bg-blue-50 border-l-4 border-blue-500 rounded-lg my-8">
<h4 class="text-lg font-bold text-blue-700 mb-2">💡 </h4> <h4 class="text-lg font-bold text-blue-700 mb-2">💡 </h4>
<p class="text-blue-600"></p> <p class="text-blue-600"></p>
</div> </div>
</pre> </pre>
<div class="p-6 bg-blue-50 border-l-4 border-blue-500 rounded-lg my-8"> <div class="p-6 bg-blue-50 border-l-4 border-blue-500 rounded-lg my-8">
<h4 class="text-lg font-bold text-blue-700 mb-2">💡 </h4> <h4 class="text-lg font-bold text-blue-700 mb-2"></h4>
<p class="text-blue-600"></p> <p class="text-blue-600"></p>
</div> </div>
@ -273,7 +323,7 @@ function greet(user: User): string {
</div> </div>
</div> </div>
### 2.6 ### 2.6
<pre> <pre>
> 📌 **** > 📌 ****
@ -327,36 +377,63 @@ function greet(user: User): string {
2. 2.
3. 3.
> 💡 **** Markdown > 💡 **** Markdown
`, `,
authorName: "Markdown 专家", authorName: "Markdown 专家",
publishedAt: new Date("2024-03-15"), publishedAt: new Date("2024-03-15"),
coverImage: "https://images.unsplash.com/photo-1499951360447-b19be8fe80f5?w=1200&h=600", coverImage: "https://images.unsplash.com/photo-1499951360447-b19be8fe80f5?w=1200&h=600",
metaKeywords: "Markdown,基础语法,高级排版,布局设计",
metaDescription: "从基础语法到高级排版,全面了解 Markdown 的各种用法和技巧。",
status: "published", status: "published",
isEditor: true, isEditor: true,
createdAt: new Date("2024-03-15"), createdAt: new Date("2024-03-15"),
updatedAt: new Date("2024-03-15"), updatedAt: new Date("2024-03-15"),
categories: [{ name: "教程" }], taxonomies: {
tags: [{ name: "Markdown" }, { name: "排版" }, { name: "写作" }], 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 函数 // 添 meta 函数
export const meta: MetaFunction = () => { 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 [ return [
{ title: mockPost.title }, { title: mockPost.title },
{ name: "description", content: mockPost.metaDescription }, { name: "description", content: description },
{ name: "keywords", content: mockPost.metaKeywords }, { name: "keywords", content: keywords },
// 添加 Open Graph 标 // 添 Open Graph 标
{ property: "og:title", content: mockPost.title }, { 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:image", content: mockPost.coverImage },
{ property: "og:type", content: "article" }, { property: "og:type", content: "article" },
// 添加 Twitter 卡片标签 // 添加 Twitter 卡片标签
{ name: "twitter:card", content: "summary_large_image" }, { name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:title", content: mockPost.title }, { name: "twitter:title", content: mockPost.title },
{ name: "twitter:description", content: mockPost.metaDescription }, { name: "twitter:description", content: description },
{ name: "twitter:image", content: mockPost.coverImage }, { name: "twitter:image", content: mockPost.coverImage },
]; ];
}; };
@ -510,7 +587,7 @@ export default new Template({}, ({ http, args }) => {
const components = useMemo(() => { const components = useMemo(() => {
return { return {
h1: ({ children, node, ...props }: ComponentPropsWithoutRef<'h1'> & { node?: any }) => { h1: ({ children, ...props }: ComponentPropsWithoutRef<'h1'>) => {
const headingId = headingIds.current.shift(); const headingId = headingIds.current.shift();
return ( return (
<h1 id={headingId} className="text-xl sm:text-2xl md:text-3xl lg:text-4xl font-bold mt-6 sm:mt-8 mb-3 sm:mb-4" {...props}> <h1 id={headingId} className="text-xl sm:text-2xl md:text-3xl lg:text-4xl font-bold mt-6 sm:mt-8 mb-3 sm:mb-4" {...props}>
@ -518,7 +595,7 @@ export default new Template({}, ({ http, args }) => {
</h1> </h1>
); );
}, },
h2: ({ children, node, ...props }: ComponentPropsWithoutRef<'h2'> & { node?: any }) => { h2: ({ children, ...props }: ComponentPropsWithoutRef<'h2'>) => {
const headingId = headingIds.current.shift(); const headingId = headingIds.current.shift();
return ( return (
<h2 id={headingId} className="text-lg sm:text-xl md:text-2xl lg:text-3xl font-semibold mt-5 sm:mt-6 mb-2 sm:mb-3" {...props}> <h2 id={headingId} className="text-lg sm:text-xl md:text-2xl lg:text-3xl font-semibold mt-5 sm:mt-6 mb-2 sm:mb-3" {...props}>
@ -526,7 +603,7 @@ export default new Template({}, ({ http, args }) => {
</h2> </h2>
); );
}, },
h3: ({ children, node, ...props }: ComponentPropsWithoutRef<'h3'> & { node?: any }) => { h3: ({ children, ...props }: ComponentPropsWithoutRef<'h3'>) => {
const headingId = headingIds.current.shift(); const headingId = headingIds.current.shift();
return ( return (
<h3 id={headingId} className="text-base sm:text-lg md:text-xl lg:text-2xl font-medium mt-4 mb-2" {...props}> <h3 id={headingId} className="text-base sm:text-lg md:text-xl lg:text-2xl font-medium mt-4 mb-2" {...props}>
@ -534,45 +611,100 @@ export default new Template({}, ({ http, args }) => {
</h3> </h3>
); );
}, },
p: ({ children, node, ...props }: ComponentPropsWithoutRef<'p'> & { node?: any }) => ( p: ({ node, ...props }: ComponentPropsWithoutRef<'p'> & { node?: any }) => (
<p className="text-sm sm:text-base md:text-lg leading-relaxed mb-3 sm:mb-4 text-[--gray-11]" {...props}> <p
{children} className="text-sm sm:text-base md:text-lg leading-relaxed mb-3 sm:mb-4 text-[--gray-11]"
</p> {...props}
/>
), ),
ul: ({ children, node, ...props }: ComponentPropsWithoutRef<'ul'> & { node?: any }) => ( ul: ({ children, ...props }: ComponentPropsWithoutRef<'ul'>) => (
<ul className="list-disc pl-4 sm:pl-6 mb-3 sm:mb-4 space-y-1.5 sm:space-y-2 text-[--gray-11]" {...props}> <ul className="list-disc pl-4 sm:pl-6 mb-3 sm:mb-4 space-y-1.5 sm:space-y-2 text-[--gray-11]" {...props}>
{children} {children}
</ul> </ul>
), ),
ol: ({ children, node, ...props }: ComponentPropsWithoutRef<'ol'> & { node?: any }) => ( ol: ({ children, ...props }: ComponentPropsWithoutRef<'ol'>) => (
<ol className="list-decimal pl-4 sm:pl-6 mb-3 sm:mb-4 space-y-1.5 sm:space-y-2 text-[--gray-11]" {...props}> <ol className="list-decimal pl-4 sm:pl-6 mb-3 sm:mb-4 space-y-1.5 sm:space-y-2 text-[--gray-11]" {...props}>
{children} {children}
</ol> </ol>
), ),
li: ({ children, node, ...props }: ComponentPropsWithoutRef<'li'> & { node?: any }) => ( li: ({ children, ...props }: ComponentPropsWithoutRef<'li'>) => (
<li className="text-sm sm:text-base md:text-lg leading-relaxed" {...props}> <li className="text-sm sm:text-base md:text-lg leading-relaxed" {...props}>
{children} {children}
</li> </li>
), ),
blockquote: ({ children, node, ...props }: ComponentPropsWithoutRef<'blockquote'> & { node?: any }) => ( blockquote: ({ children, ...props }: ComponentPropsWithoutRef<'blockquote'>) => (
<blockquote className="border-l-4 border-[--gray-6] pl-4 sm:pl-6 py-2 my-3 sm:my-4 text-[--gray-11] italic" {...props}> <blockquote className="border-l-4 border-[--gray-6] pl-4 sm:pl-6 py-2 my-3 sm:my-4 text-[--gray-11] italic" {...props}>
{children} {children}
</blockquote> </blockquote>
), ),
code: ({ inline, className, children, node, ...props }: ComponentPropsWithoutRef<'code'> & { pre: ({ children, ...props }: ComponentPropsWithoutRef<'pre'>) => {
inline?: boolean, const childArray = React.Children.toArray(children);
node?: any
}) => {
// 使用多个条件来确保服务端和客户端渲染一致
const isInPre = Boolean(
className?.includes('language-')
);
// 如果是行内代码(不在 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(/&quot;/g, '"')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&#39;/g, "'");
}
return '';
}).join('');
} else if (React.isValidElement(children)) {
content = ReactDOMServer.renderToString(children as React.ReactElement)
.replace(/\s+data-reactroot=""/g, '')
.replace(/&quot;/g, '"')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&#39;/g, "'");
}
// 普通预格式化文本
return (
<pre
className="my-4 p-4 rounded-lg overflow-x-auto whitespace-pre-wrap
bg-[--gray-3] border border-[--gray-6] text-[--gray-12]
text-sm leading-relaxed font-mono"
{...props}
>
{content
}
</pre>
);
},
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 ( return (
<code <code
className="px-2 py-1 rounded-md bg-[--gray-4] text-[--accent-11] font-medium text-[0.85em] sm:text-[0.9em]" className="px-2 py-1 rounded-md bg-[--gray-4] text-[--accent-11] font-medium text-[0.85em]"
{...props} {...props}
> >
{children} {children}
@ -580,27 +712,26 @@ export default new Template({}, ({ http, args }) => {
); );
} }
// 以下是代码块的处理逻辑 const language = match ? match[1] : '';
const match = /language-(\w+)/.exec(className || "");
const lang = match ? match[1].toLowerCase() : "";
return ( return (
<div className="my-4 sm:my-6"> <div className="my-4 sm:my-6">
<div className="flex justify-between items-center h-9 sm:h-10 px-4 sm:px-6 <div className="flex justify-between items-center h-9 sm:h-10 px-4 sm:px-6
border-t border-x border-[--gray-6] border-t border-x border-[--gray-6]
bg-[--gray-3] dark:bg-[--gray-3] bg-[--gray-3] dark:bg-[--gray-3]
rounded-t-lg rounded-t-lg"
mx-0"
> >
<div className="text-sm text-[--gray-12] dark:text-[--gray-12] font-medium">{lang || "text"}</div> <div className="text-sm text-[--gray-12] dark:text-[--gray-12] font-medium">
<CopyButton code={String(children)} /> {language || "text"}
</div>
<CopyButton code={code} />
</div> </div>
<div className="border border-[--gray-6] rounded-b-lg bg-white dark:bg-[--gray-1] mx-0"> <div className="border border-[--gray-6] rounded-b-lg bg-white dark:bg-[--gray-1]">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<div className="p-4 sm:p-6"> <div className="p-4 sm:p-6">
<SyntaxHighlighter <SyntaxHighlighter
language={lang || "text"} language={language || "text"}
style={{ style={{
...oneLight, ...oneLight,
'punctuation': { 'punctuation': {
@ -627,7 +758,7 @@ export default new Template({}, ({ http, args }) => {
} }
}} }}
> >
{String(children).replace(/\n$/, "")} {code}
</SyntaxHighlighter> </SyntaxHighlighter>
</div> </div>
</div> </div>
@ -641,7 +772,7 @@ export default new Template({}, ({ http, args }) => {
<div className="scroll-container overflow-x-auto"> <div className="scroll-container overflow-x-auto">
<div className="min-w-[640px] sm:min-w-0"> <div className="min-w-[640px] sm:min-w-0">
<div className="border border-[--gray-6] rounded-lg bg-white dark:bg-[--gray-1]"> <div className="border border-[--gray-6] rounded-lg bg-white dark:bg-[--gray-1]">
<table className="w-full border-collapse text-xs sm:text-sm" {...props}> <table className="w-full border-collapse" {...props}>
{children} {children}
</table> </table>
</div> </div>
@ -650,77 +781,66 @@ export default new Template({}, ({ http, args }) => {
</div> </div>
), ),
th: ({ children, ...props }: ComponentPropsWithoutRef<'th'>) => ( th: ({ children, style, ...props }: ComponentPropsWithoutRef<'th'> & { style?: React.CSSProperties }) => {
<th // 获取对齐方式
className="px-4 sm:px-4 md:px-6 py-2 sm:py-3 text-left text-[10px] sm:text-xs font-medium uppercase tracking-wider const getAlignment = () => {
text-[--gray-12] break-words hyphens-auto if (style?.textAlign === 'center') return 'text-center';
bg-[--gray-3] dark:bg-[--gray-3] if (style?.textAlign === 'right') return 'text-right';
first:rounded-tl-lg last:rounded-tr-lg return 'text-left';
border-b border-[--gray-6]" };
{...props}
> return (
{children} <th
</th> className={`px-4 sm:px-4 md:px-6 py-2 sm:py-3 text-left text-[10px] sm:text-xs font-medium uppercase tracking-wider
), text-[--gray-12] break-words hyphens-auto
bg-[--gray-3] dark:bg-[--gray-3]
first:rounded-tl-lg last:rounded-tr-lg
border-b border-[--gray-6]
align-top ${getAlignment()}`}
{...props}
>
{children}
</th>
);
},
td: ({ children, ...props }: ComponentPropsWithoutRef<'td'>) => ( td: ({ children, style, ...props }: ComponentPropsWithoutRef<'td'> & { style?: React.CSSProperties }) => {
<td // 获取父级 th 的对齐方式
className="px-4 sm:px-4 md:px-6 py-2 sm:py-3 md:py-4 text-xs sm:text-sm text-[--gray-11] break-words hyphens-auto const getAlignment = () => {
[&:first-child]:font-medium [&:first-child]:text-[--gray-12]" if (style?.textAlign === 'center') return 'text-center';
{...props} if (style?.textAlign === 'right') return 'text-right';
> return 'text-left';
{children} };
</td>
), return (
<td
className={`px-4 sm:px-4 md:px-6 py-2 sm:py-3 md:py-4 text-xs sm:text-sm text-[--gray-11] break-words hyphens-auto
[&:first-child]:font-medium [&:first-child]:text-[--gray-12]
align-top ${getAlignment()}`}
{...props}
>
{children}
</td>
);
},
// 修改 details 组件 // 修改 details 组件
details: ({ children, ...props }: ComponentPropsWithoutRef<'details'>) => ( details: ({ node, ...props }: ComponentPropsWithoutRef<'details'> & { node?: any }) => (
<details <details
className="my-4 rounded-lg border border-[--gray-6] bg-[--gray-2] overflow-hidden className="my-4 rounded-lg border border-[--gray-6] bg-[--gray-2] overflow-hidden
marker:text-[--gray-11] [&[open]]:bg-[--gray-1]" marker:text-[--gray-11] [&[open]]:bg-[--gray-1]"
{...props} {...props}
> />
{children}
</details>
), ),
// 修改 summary 组件 // 修改 summary 组件
summary: ({ children, ...props }: ComponentPropsWithoutRef<'summary'>) => ( summary: ({ node, ...props }: ComponentPropsWithoutRef<'summary'> & { node?: any }) => (
<summary <summary
className="px-4 py-3 cursor-pointer hover:bg-[--gray-3] transition-colors className="px-4 py-3 cursor-pointer hover:bg-[--gray-3] transition-colors
text-[--gray-12] font-medium select-none text-[--gray-12] font-medium select-none
marker:text-[--gray-11]" marker:text-[--gray-11]"
{...props} {...props}
> />
{children}
</summary>
), ),
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 (
<pre
className="my-4 p-4 bg-[--gray-3] rounded-lg overflow-x-auto text-sm
border border-[--gray-6] text-[--gray-12]"
{...props}
>
{/* 直接输出原始内容,不经过 markdown 解析 */}
{typeof content === 'string' ? content : children}
</pre>
);
},
}; };
}, []); }, []);
@ -850,7 +970,7 @@ export default new Template({}, ({ http, args }) => {
{isMounted && showToc && ( {isMounted && showToc && (
<div <div
className="lg:hidden fixed inset-0 z-50 bg-black/50 transition-opacity duration-300" className="lg:hidden fixed top-[var(--header-height)] inset-x-0 bottom-0 z-50 bg-black/50 transition-opacity duration-300"
onClick={() => setShowToc(false)} onClick={() => setShowToc(false)}
> >
<div <div
@ -859,35 +979,47 @@ export default new Template({}, ({ http, args }) => {
translate-x-0 animate-in slide-in-from-right" translate-x-0 animate-in slide-in-from-right"
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
> >
<div className="flex justify-between items-center p-4 border-b border-[--gray-6]">
<Text size="2" weight="medium" className="text-[--gray-12]">
</Text>
<Button
variant="ghost"
onClick={() => setShowToc(false)}
className="hover:bg-[--gray-4] active:bg-[--gray-5] transition-colors"
>
</Button>
</div>
<ScrollArea <ScrollArea
type="hover" type="hover"
scrollbars="vertical" scrollbars="vertical"
className="scroll-container h-[calc(100vh-64px)] p-4" className="scroll-container h-full p-4"
> >
<div className="space-y-2"> <div className="space-y-2">
{tocItems.map((item, index) => { {tocItems.map((item, index) => {
if (item.level > 3) return null; if (item.level > 3) return null;
const isActive = activeId === item.id;
return ( return (
<a <a
key={`${item.id}-${index}`} key={`${item.id}-${index}`}
href={`#${item.id}`} href={`#${item.id}`}
ref={node => {
// 当目录打开且是当前高亮项时,将其滚动到居中位置
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={` className={`
block py-1.5 px-3 rounded transition-colors block py-1.5 px-3 rounded transition-colors
${ ${
activeId === item.id isActive
? "text-[--accent-11] font-medium bg-[--accent-3]" ? "text-[--accent-11] font-medium bg-[--accent-3]"
: "text-[--gray-11] hover:text-[--gray-12] hover:bg-[--gray-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" ? "text-sm font-medium"
: item.level === 2 : item.level === 2
? "text-[0.8125rem]" ? "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) => { onClick={(e) => {
@ -924,7 +1056,7 @@ export default new Template({}, ({ http, args }) => {
</> </>
); );
// 在组件顶部添加 useMemo 包静态内容 // 在组件顶部添加 useMemo 包静态内容
const PostContent = useMemo(() => { const PostContent = useMemo(() => {
// 在渲染内容前重置 headingIds // 在渲染内容前重置 headingIds
if (headingIdsArrays[mockPost.id]) { if (headingIdsArrays[mockPost.id]) {
@ -947,6 +1079,7 @@ export default new Template({}, ({ http, args }) => {
components={components} components={components}
remarkPlugins={[remarkGfm, remarkEmoji]} remarkPlugins={[remarkGfm, remarkEmoji]}
rehypePlugins={[rehypeRaw]} rehypePlugins={[rehypeRaw]}
skipHtml={false}
> >
{mockPost.content} {mockPost.content}
</ReactMarkdown> </ReactMarkdown>
@ -967,7 +1100,7 @@ export default new Template({}, ({ http, args }) => {
className="relative flex-col lg:flex-row" className="relative flex-col lg:flex-row"
gap={{initial: "4", lg: "8"}} gap={{initial: "4", lg: "8"}}
> >
{/* 文章体 - 调整宽度计算 */} {/* 文章体 - 调整宽度计算 */}
<Box className="w-full lg:w-[calc(100%-12rem)] xl:w-[calc(100%-13rem)]"> <Box className="w-full lg:w-[calc(100%-12rem)] xl:w-[calc(100%-13rem)]">
<Box className="p-4 sm:p-6 md:p-8"> <Box className="p-4 sm:p-6 md:p-8">
{/* 头部 */} {/* 头部 */}
@ -1014,11 +1147,11 @@ export default new Template({}, ({ http, args }) => {
{/* 分类 */} {/* 分类 */}
<Flex gap="2"> <Flex gap="2">
{mockPost.categories?.map((category) => { {mockPost.taxonomies?.categories.map((category) => {
const color = getColorScheme(category.name); const color = getColorScheme(category.name);
return ( return (
<Text <Text
key={category.name} key={category.slug}
size="2" size="2"
className={`px-3 py-0.5 ${color.bg} ${color.text} rounded-md className={`px-3 py-0.5 ${color.bg} ${color.text} rounded-md
border ${color.border} font-medium ${color.hover} border ${color.border} font-medium ${color.hover}
@ -1035,11 +1168,11 @@ export default new Template({}, ({ http, args }) => {
{/* 标签 */} {/* 标签 */}
<Flex gap="2"> <Flex gap="2">
{mockPost.tags?.map((tag) => { {mockPost.taxonomies?.tags.map((tag) => {
const color = getColorScheme(tag.name); const color = getColorScheme(tag.name);
return ( return (
<Text <Text
key={tag.name} key={tag.slug}
size="2" size="2"
className={`px-3 py-1 ${color.bg} ${color.text} rounded-md className={`px-3 py-1 ${color.bg} ${color.text} rounded-md
border ${color.border} ${color.hover} border ${color.border} ${color.hover}
@ -1060,7 +1193,7 @@ export default new Template({}, ({ http, args }) => {
</Flex> </Flex>
</Box> </Box>
{/* 图片 */} {/* 面 */}
{mockPost.coverImage && ( {mockPost.coverImage && (
<Box className="mb-16 rounded-xl overflow-hidden aspect-[2/1] shadow-lg"> <Box className="mb-16 rounded-xl overflow-hidden aspect-[2/1] shadow-lg">
<img <img

View File

@ -5,7 +5,7 @@ import {
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { Post, PostDisplay, Tag } from "interface/fields"; import { PostDisplay } from "interface/fields";
import { useMemo } from "react"; import { useMemo } from "react";
import { ImageLoader } from "hooks/ParticleImage"; import { ImageLoader } from "hooks/ParticleImage";
@ -20,20 +20,19 @@ const mockArticles: PostDisplay[] = [
authorName: "张三", authorName: "张三",
publishedAt: new Date("2024-03-15"), publishedAt: new Date("2024-03-15"),
coverImage: "https://avatars.githubusercontent.com/u/72159?v=4", coverImage: "https://avatars.githubusercontent.com/u/72159?v=4",
metaKeywords: "",
metaDescription: "",
status: "published", status: "published",
isEditor: false, isEditor: false,
createdAt: new Date("2024-03-15"), createdAt: new Date("2024-03-15"),
updatedAt: new Date("2024-03-15"), updatedAt: new Date("2024-03-15"),
// 添加分类和标签 taxonomies: {
categories: [ categories: [
{ name: "前端开发" } { name: "前端开发", slug: "frontend", type: "category" }
], ],
tags: [ tags: [
{ name: "工程化" }, { name: "工程化", slug: "engineering", type: "tag" },
{ name: "效率提升" } { name: "效率提升", slug: "efficiency", type: "tag" }
] ]
}
}, },
{ {
id: 2, id: 2,
@ -42,19 +41,19 @@ const mockArticles: PostDisplay[] = [
authorName: "李四", authorName: "李四",
publishedAt: new Date("2024-03-14"), publishedAt: new Date("2024-03-14"),
coverImage: "", coverImage: "",
metaKeywords: "",
metaDescription: "",
status: "published", status: "published",
isEditor: false, isEditor: false,
createdAt: new Date("2024-03-14"), createdAt: new Date("2024-03-14"),
updatedAt: new Date("2024-03-14"), updatedAt: new Date("2024-03-14"),
categories: [ taxonomies: {
{ name: "前端开发" } categories: [
], { name: "前端开发", slug: "frontend", type: "category" }
tags: [ ],
{ name: "React" }, tags: [
{ name: "JavaScript" } { name: "React", slug: "react", type: "tag" },
] { name: "JavaScript", slug: "javascript", type: "tag" }
]
}
}, },
{ {
id: 3, id: 3,
@ -63,19 +62,19 @@ const mockArticles: PostDisplay[] = [
authorName: "王五", authorName: "王五",
publishedAt: new Date("2024-03-13"), publishedAt: new Date("2024-03-13"),
coverImage: "ssssxx", coverImage: "ssssxx",
metaKeywords: "",
metaDescription: "",
status: "published", status: "published",
isEditor: false, isEditor: false,
createdAt: new Date("2024-03-13"), createdAt: new Date("2024-03-13"),
updatedAt: new Date("2024-03-13"), updatedAt: new Date("2024-03-13"),
categories: [ taxonomies: {
{ name: "性能优化" } categories: [
], { name: "性能优化", slug: "performance-optimization", type: "category" }
tags: [ ],
{ name: "JavaScript" }, tags: [
{ name: "性能" } { name: "JavaScript", slug: "javascript", type: "tag" },
] { name: "性能", slug: "performance", type: "tag" }
]
}
}, },
{ {
id: 4, id: 4,
@ -84,19 +83,19 @@ const mockArticles: PostDisplay[] = [
authorName: "田六", authorName: "田六",
publishedAt: new Date("2024-03-13"), publishedAt: new Date("2024-03-13"),
coverImage: "https://images.unsplash.com/photo-1537432376769-00f5c2f4c8d2?w=500&auto=format", coverImage: "https://images.unsplash.com/photo-1537432376769-00f5c2f4c8d2?w=500&auto=format",
metaKeywords: "",
metaDescription: "",
status: "published", status: "published",
isEditor: false, isEditor: false,
createdAt: new Date("2024-03-13"), createdAt: new Date("2024-03-13"),
updatedAt: new Date("2024-03-13"), updatedAt: new Date("2024-03-13"),
categories: [ taxonomies: {
{ name: "移动开发" } categories: [
], { name: "移动开发", slug: "mobile-development", type: "category" }
tags: [ ],
{ name: "移动端" }, tags: [
{ name: "响应式" } { name: "移动端", slug: "mobile", type: "tag" },
] { name: "响应式", slug: "responsive", type: "tag" }
]
}
}, },
{ {
id: 5, id: 5,
@ -105,29 +104,29 @@ const mockArticles: PostDisplay[] = [
authorName: "赵七", authorName: "赵七",
publishedAt: new Date("2024-03-12"), publishedAt: new Date("2024-03-12"),
coverImage: "https://images.unsplash.com/photo-1537432376769-00f5c2f4c8d2?w=500&auto=format", coverImage: "https://images.unsplash.com/photo-1537432376769-00f5c2f4c8d2?w=500&auto=format",
metaKeywords: "",
metaDescription: "",
status: "published", status: "published",
isEditor: false, isEditor: false,
createdAt: new Date("2024-03-12"), createdAt: new Date("2024-03-12"),
updatedAt: new Date("2024-03-12"), updatedAt: new Date("2024-03-12"),
categories: [ taxonomies: {
{ name: "全栈开发" }, categories: [
{ name: "云原生" }, { name: "全栈开发", slug: "full-stack-development", type: "category" },
{ name: "微服务" }, { name: "云原生", slug: "cloud-native", type: "category" },
{ name: "DevOps" }, { name: "微服务", slug: "microservices", type: "category" },
{ name: "系统架构" } { name: "DevOps", slug: "devops", type: "category" },
], { name: "系统架构", slug: "system-architecture", type: "category" }
tags: [ ],
{ name: "React" }, tags: [
{ name: "Node.js" }, { name: "React", slug: "react", type: "tag" },
{ name: "Docker" }, { name: "Node.js", slug: "node-js", type: "tag" },
{ name: "Kubernetes" }, { name: "Docker", slug: "docker", type: "tag" },
{ name: "MongoDB" }, { name: "Kubernetes", slug: "kubernetes", type: "tag" },
{ name: "微服务" }, { name: "MongoDB", slug: "mongodb", type: "tag" },
{ name: "CI/CD" }, { name: "微服务", slug: "microservices", type: "tag" },
{ name: "云计算" } { name: "CI/CD", slug: "ci-cd", type: "tag" },
] { name: "云计算", slug: "cloud-computing", type: "tag" }
]
}
}, },
{ {
id: 6, id: 6,
@ -136,22 +135,22 @@ const mockArticles: PostDisplay[] = [
authorName: "孙八", authorName: "孙八",
publishedAt: new Date("2024-03-11"), publishedAt: new Date("2024-03-11"),
coverImage: "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format", coverImage: "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format",
metaKeywords: "",
metaDescription: "",
status: "published", status: "published",
isEditor: false, isEditor: false,
createdAt: new Date("2024-03-11"), createdAt: new Date("2024-03-11"),
updatedAt: new Date("2024-03-11"), updatedAt: new Date("2024-03-11"),
categories: [ taxonomies: {
{ name: "TypeScript" }, categories: [
{ name: "编程语言" } { name: "TypeScript", slug: "typescript", type: "category" },
], { name: "编程语言", slug: "programming-languages", type: "category" }
tags: [ ],
{ name: "类型系统" }, tags: [
{ name: "泛型编程" }, { name: "类型系统", slug: "type-system", type: "tag" },
{ name: "装饰器" }, { name: "泛型编程", slug: "generic-programming", type: "tag" },
{ name: "类型推导" } { name: "装饰器", slug: "decorators", type: "tag" },
] { name: "类型推导", slug: "type-inference", type: "tag" }
]
}
}, },
{ {
id: 7, id: 7,
@ -160,22 +159,22 @@ const mockArticles: PostDisplay[] = [
authorName: "周九", authorName: "周九",
publishedAt: new Date("2024-03-10"), publishedAt: new Date("2024-03-10"),
coverImage: "https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=500&auto=format", coverImage: "https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=500&auto=format",
metaKeywords: "",
metaDescription: "",
status: "published", status: "published",
isEditor: false, isEditor: false,
createdAt: new Date("2024-03-10"), createdAt: new Date("2024-03-10"),
updatedAt: new Date("2024-03-10"), updatedAt: new Date("2024-03-10"),
categories: [ taxonomies: {
{ name: "性能优化" }, categories: [
{ name: "前端开发" } { name: "性能优化", slug: "performance-optimization", type: "category" },
], { name: "前端开发", slug: "frontend-development", type: "category" }
tags: [ ],
{ name: "性能监控" }, tags: [
{ name: "懒加载" }, { name: "性能监控", slug: "performance-monitoring", type: "tag" },
{ name: "缓存策略" }, { name: "懒加载", slug: "lazy-loading", type: "tag" },
{ name: "代码分割" } { name: "缓存策略", slug: "caching-strategies", type: "tag" },
] { name: "代码分割", slug: "code-splitting", type: "tag" }
]
}
}, },
{ {
id: 8, id: 8,
@ -184,22 +183,22 @@ const mockArticles: PostDisplay[] = [
authorName: "吴十", authorName: "吴十",
publishedAt: new Date("2024-03-09"), publishedAt: new Date("2024-03-09"),
coverImage: "https://images.unsplash.com/photo-1517180102446-f3ece451e9d8?w=500&auto=format", coverImage: "https://images.unsplash.com/photo-1517180102446-f3ece451e9d8?w=500&auto=format",
metaKeywords: "",
metaDescription: "",
status: "published", status: "published",
isEditor: false, isEditor: false,
createdAt: new Date("2024-03-09"), createdAt: new Date("2024-03-09"),
updatedAt: new Date("2024-03-09"), updatedAt: new Date("2024-03-09"),
categories: [ taxonomies: {
{ name: "架构设计" }, categories: [
{ name: "微前端" } { name: "架构设计", slug: "architecture-design", type: "category" },
], { name: "微前端", slug: "micro-frontends", type: "category" }
tags: [ ],
{ name: "qiankun" }, tags: [
{ name: "single-spa" }, { name: "qiankun", slug: "qiankun", type: "tag" },
{ name: "模块联邦" }, { name: "single-spa", slug: "single-spa", type: "tag" },
{ name: "应用通信" } { name: "模块联邦", slug: "module-federation", type: "tag" },
] { name: "应用通信", slug: "application-communication", type: "tag" }
]
}
} }
]; ];
@ -282,7 +281,7 @@ export default new Template({}, ({ http, args }) => {
className="scroll-container flex-1" className="scroll-container flex-1"
> >
<Flex gap="2" className="flex-nowrap"> <Flex gap="2" className="flex-nowrap">
{article.categories?.map((category) => ( {article.taxonomies?.categories.map((category) => (
<Text <Text
key={category.name} key={category.name}
size="2" size="2"
@ -316,7 +315,7 @@ export default new Template({}, ({ http, args }) => {
</div> </div>
<Flex gap="2" className="flex-wrap"> <Flex gap="2" className="flex-wrap">
{article.tags?.map((tag: Tag) => ( {article.taxonomies?.tags.map((tag) => (
<Text <Text
key={tag.name} key={tag.name}
size="1" size="1"

View File

@ -8,6 +8,7 @@
/* 共用的尺寸 */ /* 共用的尺寸 */
--scrollbar-size: 8px; --scrollbar-size: 8px;
--border-radius: 4px; --border-radius: 4px;
--header-height: 80px;
} }
/* 暗色主题变量 */ /* 暗色主题变量 */