数据库:合并标签和分类,分离自定义字段和元
前端:优化Markdown解析和排版
This commit is contained in:
parent
2ffa4883ae
commit
e66f7b902d
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
export interface PostDisplay extends Post {
|
id: number;
|
||||||
categories?: Category[];
|
targetType: 'post' | 'page';
|
||||||
tags?: Tag[];
|
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 {
|
||||||
|
taxonomies?: {
|
||||||
|
categories: Taxonomy[];
|
||||||
|
tags: Taxonomy[];
|
||||||
|
};
|
||||||
|
metadata?: Metadata[];
|
||||||
|
customFields?: CustomField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageDisplay extends Page {
|
||||||
|
metadata?: Metadata[];
|
||||||
|
customFields?: CustomField[];
|
||||||
}
|
}
|
@ -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 codeElement = childArray.find(
|
||||||
const isInPre = Boolean(
|
child => React.isValidElement(child) && child.props.className?.includes('language-')
|
||||||
className?.includes('language-')
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 如果是行内代码(不在 pre 标签内),使用行内样式
|
// 如果是代码块,让 code 组件处理
|
||||||
if (!isInPre) {
|
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 (
|
||||||
|
<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 }) => {
|
||||||
|
// 获取对齐方式
|
||||||
|
const getAlignment = () => {
|
||||||
|
if (style?.textAlign === 'center') return 'text-center';
|
||||||
|
if (style?.textAlign === 'right') return 'text-right';
|
||||||
|
return 'text-left';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
<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
|
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
|
text-[--gray-12] break-words hyphens-auto
|
||||||
bg-[--gray-3] dark:bg-[--gray-3]
|
bg-[--gray-3] dark:bg-[--gray-3]
|
||||||
first:rounded-tl-lg last:rounded-tr-lg
|
first:rounded-tl-lg last:rounded-tr-lg
|
||||||
border-b border-[--gray-6]"
|
border-b border-[--gray-6]
|
||||||
|
align-top ${getAlignment()}`}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</th>
|
</th>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
|
|
||||||
td: ({ children, ...props }: ComponentPropsWithoutRef<'td'>) => (
|
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 (
|
||||||
<td
|
<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
|
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]"
|
[&:first-child]:font-medium [&:first-child]:text-[--gray-12]
|
||||||
|
align-top ${getAlignment()}`}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</td>
|
</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
|
||||||
|
@ -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"),
|
||||||
|
taxonomies: {
|
||||||
categories: [
|
categories: [
|
||||||
{ name: "前端开发" }
|
{ name: "前端开发", slug: "frontend", type: "category" }
|
||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "React" },
|
{ name: "React", slug: "react", type: "tag" },
|
||||||
{ name: "JavaScript" }
|
{ 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"),
|
||||||
|
taxonomies: {
|
||||||
categories: [
|
categories: [
|
||||||
{ name: "性能优化" }
|
{ name: "性能优化", slug: "performance-optimization", type: "category" }
|
||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "JavaScript" },
|
{ name: "JavaScript", slug: "javascript", type: "tag" },
|
||||||
{ name: "性能" }
|
{ 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"),
|
||||||
|
taxonomies: {
|
||||||
categories: [
|
categories: [
|
||||||
{ name: "移动开发" }
|
{ name: "移动开发", slug: "mobile-development", type: "category" }
|
||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "移动端" },
|
{ name: "移动端", slug: "mobile", type: "tag" },
|
||||||
{ name: "响应式" }
|
{ 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"),
|
||||||
|
taxonomies: {
|
||||||
categories: [
|
categories: [
|
||||||
{ name: "全栈开发" },
|
{ name: "全栈开发", slug: "full-stack-development", type: "category" },
|
||||||
{ name: "云原生" },
|
{ name: "云原生", slug: "cloud-native", type: "category" },
|
||||||
{ name: "微服务" },
|
{ name: "微服务", slug: "microservices", type: "category" },
|
||||||
{ name: "DevOps" },
|
{ name: "DevOps", slug: "devops", type: "category" },
|
||||||
{ name: "系统架构" }
|
{ name: "系统架构", slug: "system-architecture", type: "category" }
|
||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "React" },
|
{ name: "React", slug: "react", type: "tag" },
|
||||||
{ name: "Node.js" },
|
{ name: "Node.js", slug: "node-js", type: "tag" },
|
||||||
{ name: "Docker" },
|
{ name: "Docker", slug: "docker", type: "tag" },
|
||||||
{ name: "Kubernetes" },
|
{ name: "Kubernetes", slug: "kubernetes", type: "tag" },
|
||||||
{ name: "MongoDB" },
|
{ name: "MongoDB", slug: "mongodb", type: "tag" },
|
||||||
{ name: "微服务" },
|
{ name: "微服务", slug: "microservices", type: "tag" },
|
||||||
{ name: "CI/CD" },
|
{ name: "CI/CD", slug: "ci-cd", type: "tag" },
|
||||||
{ name: "云计算" }
|
{ 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"),
|
||||||
|
taxonomies: {
|
||||||
categories: [
|
categories: [
|
||||||
{ name: "TypeScript" },
|
{ name: "TypeScript", slug: "typescript", type: "category" },
|
||||||
{ name: "编程语言" }
|
{ name: "编程语言", slug: "programming-languages", type: "category" }
|
||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "类型系统" },
|
{ name: "类型系统", slug: "type-system", type: "tag" },
|
||||||
{ name: "泛型编程" },
|
{ name: "泛型编程", slug: "generic-programming", type: "tag" },
|
||||||
{ name: "装饰器" },
|
{ name: "装饰器", slug: "decorators", type: "tag" },
|
||||||
{ name: "类型推导" }
|
{ 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"),
|
||||||
|
taxonomies: {
|
||||||
categories: [
|
categories: [
|
||||||
{ name: "性能优化" },
|
{ name: "性能优化", slug: "performance-optimization", type: "category" },
|
||||||
{ name: "前端开发" }
|
{ name: "前端开发", slug: "frontend-development", type: "category" }
|
||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "性能监控" },
|
{ name: "性能监控", slug: "performance-monitoring", type: "tag" },
|
||||||
{ name: "懒加载" },
|
{ name: "懒加载", slug: "lazy-loading", type: "tag" },
|
||||||
{ name: "缓存策略" },
|
{ name: "缓存策略", slug: "caching-strategies", type: "tag" },
|
||||||
{ name: "代码分割" }
|
{ name: "代码分割", slug: "code-splitting", type: "tag" }
|
||||||
]
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
id: 8,
|
||||||
@ -184,23 +183,23 @@ 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"),
|
||||||
|
taxonomies: {
|
||||||
categories: [
|
categories: [
|
||||||
{ name: "架构设计" },
|
{ name: "架构设计", slug: "architecture-design", type: "category" },
|
||||||
{ name: "微前端" }
|
{ name: "微前端", slug: "micro-frontends", type: "category" }
|
||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "qiankun" },
|
{ name: "qiankun", slug: "qiankun", type: "tag" },
|
||||||
{ name: "single-spa" },
|
{ name: "single-spa", slug: "single-spa", type: "tag" },
|
||||||
{ name: "模块联邦" },
|
{ name: "模块联邦", slug: "module-federation", type: "tag" },
|
||||||
{ name: "应用通信" }
|
{ name: "应用通信", slug: "application-communication", type: "tag" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export default new Template({}, ({ http, args }) => {
|
export default new Template({}, ({ http, args }) => {
|
||||||
@ -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"
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
/* 共用的尺寸 */
|
/* 共用的尺寸 */
|
||||||
--scrollbar-size: 8px;
|
--scrollbar-size: 8px;
|
||||||
--border-radius: 4px;
|
--border-radius: 4px;
|
||||||
|
--header-height: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 暗色主题变量 */
|
/* 暗色主题变量 */
|
||||||
|
Loading…
Reference in New Issue
Block a user