后端:更新CORS配置;前端,修复数据库构建有问题:构建模式和生产模式配置为一个端口,删除代理
This commit is contained in:
parent
195f8de576
commit
f336271ad6
@ -24,3 +24,4 @@ chrono = "0.4"
|
|||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
bcrypt = "0.16"
|
bcrypt = "0.16"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
|
rocket_cors = "0.6.0"
|
||||||
|
@ -5,10 +5,13 @@ mod storage;
|
|||||||
|
|
||||||
use crate::common::config;
|
use crate::common::config;
|
||||||
use common::error::{CustomErrorInto, CustomResult};
|
use common::error::{CustomErrorInto, CustomResult};
|
||||||
|
use rocket::http::Method;
|
||||||
use rocket::Shutdown;
|
use rocket::Shutdown;
|
||||||
|
use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors, CorsOptions};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use storage::sql;
|
use storage::sql;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
db: Arc<Mutex<Option<sql::Database>>>,
|
db: Arc<Mutex<Option<sql::Database>>>,
|
||||||
shutdown: Arc<Mutex<Option<Shutdown>>>,
|
shutdown: Arc<Mutex<Option<Shutdown>>>,
|
||||||
@ -61,6 +64,25 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cors() -> Cors {
|
||||||
|
CorsOptions {
|
||||||
|
allowed_origins: AllowedOrigins::all(),
|
||||||
|
allowed_methods: vec![Method::Get, Method::Post, Method::Options]
|
||||||
|
.into_iter()
|
||||||
|
.map(From::from)
|
||||||
|
.collect(),
|
||||||
|
allowed_headers: AllowedHeaders::all(),
|
||||||
|
allow_credentials: true,
|
||||||
|
expose_headers: Default::default(),
|
||||||
|
max_age: None,
|
||||||
|
send_wildcard: false,
|
||||||
|
fairing_route_base: "/".to_string(),
|
||||||
|
fairing_route_rank: 0,
|
||||||
|
}
|
||||||
|
.to_cors()
|
||||||
|
.expect("CORS配置错误")
|
||||||
|
}
|
||||||
|
|
||||||
#[rocket::main]
|
#[rocket::main]
|
||||||
async fn main() -> CustomResult<()> {
|
async fn main() -> CustomResult<()> {
|
||||||
let config = config::Config::read().unwrap_or_else(|e| {
|
let config = config::Config::read().unwrap_or_else(|e| {
|
||||||
@ -75,7 +97,8 @@ async fn main() -> CustomResult<()> {
|
|||||||
|
|
||||||
let mut rocket_builder = rocket::build()
|
let mut rocket_builder = rocket::build()
|
||||||
.configure(rocket_config)
|
.configure(rocket_config)
|
||||||
.manage(state.clone());
|
.manage(state.clone())
|
||||||
|
.attach(cors());
|
||||||
|
|
||||||
if !config.init.sql {
|
if !config.init.sql {
|
||||||
rocket_builder = rocket_builder.mount("/", rocket::routes![api::setup::setup_sql]);
|
rocket_builder = rocket_builder.mount("/", rocket::routes![api::setup::setup_sql]);
|
||||||
|
@ -254,7 +254,7 @@ pub enum Operator {
|
|||||||
Lte,
|
Lte,
|
||||||
Like,
|
Like,
|
||||||
In,
|
In,
|
||||||
IsNull
|
IsNull,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operator {
|
impl Operator {
|
||||||
@ -268,7 +268,7 @@ impl Operator {
|
|||||||
Operator::Lte => "<=",
|
Operator::Lte => "<=",
|
||||||
Operator::Like => "LIKE",
|
Operator::Like => "LIKE",
|
||||||
Operator::In => "IN",
|
Operator::In => "IN",
|
||||||
Operator::IsNull => "IS NULL"
|
Operator::IsNull => "IS NULL",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -295,7 +295,7 @@ pub enum WhereClause {
|
|||||||
And(Vec<WhereClause>),
|
And(Vec<WhereClause>),
|
||||||
Or(Vec<WhereClause>),
|
Or(Vec<WhereClause>),
|
||||||
Condition(Condition),
|
Condition(Condition),
|
||||||
Not(Condition)
|
Not(Condition),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -541,7 +541,9 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
|
|||||||
.add_field(Field::new(
|
.add_field(Field::new(
|
||||||
"status",
|
"status",
|
||||||
FieldType::VarChar(20),
|
FieldType::VarChar(20),
|
||||||
FieldConstraint::new().not_null().check(content_state_check.clone()),
|
FieldConstraint::new()
|
||||||
|
.not_null()
|
||||||
|
.check(content_state_check.clone()),
|
||||||
ValidationLevel::Strict,
|
ValidationLevel::Strict,
|
||||||
)?);
|
)?);
|
||||||
|
|
||||||
@ -587,7 +589,9 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
|
|||||||
.add_field(Field::new(
|
.add_field(Field::new(
|
||||||
"status",
|
"status",
|
||||||
FieldType::VarChar(20),
|
FieldType::VarChar(20),
|
||||||
FieldConstraint::new().not_null().check(content_state_check.clone()),
|
FieldConstraint::new()
|
||||||
|
.not_null()
|
||||||
|
.check(content_state_check.clone()),
|
||||||
ValidationLevel::Strict,
|
ValidationLevel::Strict,
|
||||||
)?)
|
)?)
|
||||||
.add_field(Field::new(
|
.add_field(Field::new(
|
||||||
@ -728,7 +732,9 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
|
|||||||
.add_field(Field::new(
|
.add_field(Field::new(
|
||||||
"target_type",
|
"target_type",
|
||||||
FieldType::VarChar(20),
|
FieldType::VarChar(20),
|
||||||
FieldConstraint::new().not_null().check(target_type_check.clone()),
|
FieldConstraint::new()
|
||||||
|
.not_null()
|
||||||
|
.check(target_type_check.clone()),
|
||||||
ValidationLevel::Strict,
|
ValidationLevel::Strict,
|
||||||
)?)
|
)?)
|
||||||
.add_field(Field::new(
|
.add_field(Field::new(
|
||||||
@ -782,7 +788,9 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
|
|||||||
.add_field(Field::new(
|
.add_field(Field::new(
|
||||||
"target_type",
|
"target_type",
|
||||||
FieldType::VarChar(20),
|
FieldType::VarChar(20),
|
||||||
FieldConstraint::new().not_null().check(target_type_check.clone()),
|
FieldConstraint::new()
|
||||||
|
.not_null()
|
||||||
|
.check(target_type_check.clone()),
|
||||||
ValidationLevel::Strict,
|
ValidationLevel::Strict,
|
||||||
)?)
|
)?)
|
||||||
.add_field(Field::new(
|
.add_field(Field::new(
|
||||||
@ -843,10 +851,7 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
|
|||||||
WhereClause::Condition(Condition::new(
|
WhereClause::Condition(Condition::new(
|
||||||
"type".to_string(),
|
"type".to_string(),
|
||||||
Operator::Eq,
|
Operator::Eq,
|
||||||
Some(SafeValue::Text(
|
Some(SafeValue::Text("'tag'".to_string(), ValidationLevel::Raw)),
|
||||||
"'tag'".to_string(),
|
|
||||||
ValidationLevel::Raw,
|
|
||||||
)),
|
|
||||||
)?),
|
)?),
|
||||||
WhereClause::Condition(Condition::new(
|
WhereClause::Condition(Condition::new(
|
||||||
"type".to_string(),
|
"type".to_string(),
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
Dialog,
|
Dialog,
|
||||||
IconButton
|
IconButton,
|
||||||
} from "@radix-ui/themes";
|
} from "@radix-ui/themes";
|
||||||
import {
|
import {
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
@ -28,39 +28,41 @@ const mockCategories: (Category & { id: number; count: number })[] = [
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: "前端开发",
|
name: "前端开发",
|
||||||
parentId: undefined,
|
parentId: undefined,
|
||||||
count: 15
|
count: 15,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: "React",
|
name: "React",
|
||||||
parentId: "1",
|
parentId: "1",
|
||||||
count: 8
|
count: 8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
name: "Vue",
|
name: "Vue",
|
||||||
parentId: "1",
|
parentId: "1",
|
||||||
count: 5
|
count: 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
name: "后端开发",
|
name: "后端开发",
|
||||||
parentId: undefined,
|
parentId: undefined,
|
||||||
count: 12
|
count: 12,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
name: "Node.js",
|
name: "Node.js",
|
||||||
parentId: "4",
|
parentId: "4",
|
||||||
count: 6
|
count: 6,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default new Template({}, ({ http, args }) => {
|
export default new Template({}, ({ http, args }) => {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
const [newCategoryName, setNewCategoryName] = useState("");
|
const [newCategoryName, setNewCategoryName] = useState("");
|
||||||
const [selectedParentId, setSelectedParentId] = useState<string | undefined>();
|
const [selectedParentId, setSelectedParentId] = useState<
|
||||||
|
string | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
@ -88,7 +90,9 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<TextField.Root
|
<TextField.Root
|
||||||
placeholder="搜索分类..."
|
placeholder="搜索分类..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setSearchTerm(e.target.value)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<TextField.Slot>
|
<TextField.Slot>
|
||||||
<MagnifyingGlassIcon height="16" width="16" />
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
@ -126,9 +130,10 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Text>
|
<Text>
|
||||||
{category.parentId
|
{category.parentId
|
||||||
? mockCategories.find(c => c.id.toString() === category.parentId)?.name
|
? mockCategories.find(
|
||||||
: '-'
|
(c) => c.id.toString() === category.parentId,
|
||||||
}
|
)?.name
|
||||||
|
: "-"}
|
||||||
</Text>
|
</Text>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
@ -164,7 +169,9 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<TextField.Root
|
<TextField.Root
|
||||||
placeholder="输入分类名称"
|
placeholder="输入分类名称"
|
||||||
value={newCategoryName}
|
value={newCategoryName}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewCategoryName(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setNewCategoryName(e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@ -179,13 +186,12 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
>
|
>
|
||||||
<option value="">无</option>
|
<option value="">无</option>
|
||||||
{mockCategories
|
{mockCategories
|
||||||
.filter(c => !c.parentId)
|
.filter((c) => !c.parentId)
|
||||||
.map(category => (
|
.map((category) => (
|
||||||
<option key={category.id} value={category.id}>
|
<option key={category.id} value={category.id}>
|
||||||
{category.name}
|
{category.name}
|
||||||
</option>
|
</option>
|
||||||
))
|
))}
|
||||||
}
|
|
||||||
</select>
|
</select>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -197,9 +203,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
<Dialog.Close>
|
<Dialog.Close>
|
||||||
<Button className="bg-[--accent-9]">
|
<Button className="bg-[--accent-9]">创建</Button>
|
||||||
创建
|
|
||||||
</Button>
|
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
ScrollArea,
|
ScrollArea,
|
||||||
DataList,
|
DataList,
|
||||||
Avatar,
|
Avatar,
|
||||||
Badge
|
Badge,
|
||||||
} from "@radix-ui/themes";
|
} from "@radix-ui/themes";
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
@ -32,7 +32,7 @@ const mockComments = [
|
|||||||
postTitle: "构建现代化的前端开发工作流",
|
postTitle: "构建现代化的前端开发工作流",
|
||||||
createdAt: new Date("2024-03-15"),
|
createdAt: new Date("2024-03-15"),
|
||||||
status: "pending", // pending, approved, rejected
|
status: "pending", // pending, approved, rejected
|
||||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=1"
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@ -41,7 +41,7 @@ const mockComments = [
|
|||||||
postTitle: "React 18 新特性详解",
|
postTitle: "React 18 新特性详解",
|
||||||
createdAt: new Date("2024-03-14"),
|
createdAt: new Date("2024-03-14"),
|
||||||
status: "approved",
|
status: "approved",
|
||||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2"
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
|
||||||
},
|
},
|
||||||
// 可以添加更多模拟数据
|
// 可以添加更多模拟数据
|
||||||
];
|
];
|
||||||
@ -52,23 +52,23 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
|
|
||||||
const getStatusStyle = (status: string) => {
|
const getStatusStyle = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'approved':
|
case "approved":
|
||||||
return 'bg-[--green-3] text-[--green-11]';
|
return "bg-[--green-3] text-[--green-11]";
|
||||||
case 'rejected':
|
case "rejected":
|
||||||
return 'bg-[--red-3] text-[--red-11]';
|
return "bg-[--red-3] text-[--red-11]";
|
||||||
default:
|
default:
|
||||||
return 'bg-[--yellow-3] text-[--yellow-11]';
|
return "bg-[--yellow-3] text-[--yellow-11]";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusText = (status: string) => {
|
const getStatusText = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'approved':
|
case "approved":
|
||||||
return '已通过';
|
return "已通过";
|
||||||
case 'rejected':
|
case "rejected":
|
||||||
return '已拒绝';
|
return "已拒绝";
|
||||||
default:
|
default:
|
||||||
return '待审核';
|
return "待审核";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -87,15 +87,14 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* 搜索和筛选栏 */}
|
{/* 搜索和筛选栏 */}
|
||||||
<Flex
|
<Flex gap="4" className="mb-6 flex-col sm:flex-row">
|
||||||
gap="4"
|
|
||||||
className="mb-6 flex-col sm:flex-row"
|
|
||||||
>
|
|
||||||
<Box className="w-full sm:w-64">
|
<Box className="w-full sm:w-64">
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
placeholder="搜索评论..."
|
placeholder="搜索评论..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setSearchTerm(e.target.value)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<TextField.Slot>
|
<TextField.Slot>
|
||||||
<MagnifyingGlassIcon height="16" width="16" />
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
@ -106,20 +105,20 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<Button variant="surface">
|
<Button variant="surface">
|
||||||
状态: {selectedStatus === 'all' ? '全部' : selectedStatus}
|
状态: {selectedStatus === "all" ? "全部" : selectedStatus}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
<DropdownMenu.Item onClick={() => setSelectedStatus('all')}>
|
<DropdownMenu.Item onClick={() => setSelectedStatus("all")}>
|
||||||
全部
|
全部
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item onClick={() => setSelectedStatus('pending')}>
|
<DropdownMenu.Item onClick={() => setSelectedStatus("pending")}>
|
||||||
待审核
|
待审核
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item onClick={() => setSelectedStatus('approved')}>
|
<DropdownMenu.Item onClick={() => setSelectedStatus("approved")}>
|
||||||
已通过
|
已通过
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item onClick={() => setSelectedStatus('rejected')}>
|
<DropdownMenu.Item onClick={() => setSelectedStatus("rejected")}>
|
||||||
已拒绝
|
已拒绝
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
@ -158,20 +157,22 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Text className="line-clamp-2">
|
<Text className="line-clamp-2">{comment.content}</Text>
|
||||||
{comment.content}
|
|
||||||
</Text>
|
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Text className="line-clamp-1">
|
<Text className="line-clamp-1">{comment.postTitle}</Text>
|
||||||
{comment.postTitle}
|
|
||||||
</Text>
|
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
{comment.status === 'approved' && <Badge color="green">已通过</Badge>}
|
{comment.status === "approved" && (
|
||||||
{comment.status === 'pending' && <Badge color="orange">待审核</Badge>}
|
<Badge color="green">已通过</Badge>
|
||||||
{comment.status === 'rejected' && <Badge color="red">已拒绝</Badge>}
|
)}
|
||||||
|
{comment.status === "pending" && (
|
||||||
|
<Badge color="orange">待审核</Badge>
|
||||||
|
)}
|
||||||
|
{comment.status === "rejected" && (
|
||||||
|
<Badge color="red">已拒绝</Badge>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
@ -193,11 +194,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
>
|
>
|
||||||
<Cross2Icon className="w-4 h-4" />
|
<Cross2Icon className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="ghost" size="1" color="red">
|
||||||
variant="ghost"
|
|
||||||
size="1"
|
|
||||||
color="red"
|
|
||||||
>
|
|
||||||
<TrashIcon className="w-4 h-4" />
|
<TrashIcon className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -211,7 +208,10 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
{/* 移动端列表视图 */}
|
{/* 移动端列表视图 */}
|
||||||
<div className="block sm:hidden">
|
<div className="block sm:hidden">
|
||||||
{mockComments.map((comment) => (
|
{mockComments.map((comment) => (
|
||||||
<DataList.Root key={comment.id} className="p-4 border-b border-[--gray-6] last:border-b-0">
|
<DataList.Root
|
||||||
|
key={comment.id}
|
||||||
|
className="p-4 border-b border-[--gray-6] last:border-b-0"
|
||||||
|
>
|
||||||
<DataList.Item>
|
<DataList.Item>
|
||||||
<DataList.Label minWidth="88px">评论者</DataList.Label>
|
<DataList.Label minWidth="88px">评论者</DataList.Label>
|
||||||
<DataList.Value>
|
<DataList.Value>
|
||||||
@ -245,9 +245,15 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<DataList.Label minWidth="88px">状态</DataList.Label>
|
<DataList.Label minWidth="88px">状态</DataList.Label>
|
||||||
<DataList.Value>
|
<DataList.Value>
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
{comment.status === 'approved' && <Badge color="green">已通过</Badge>}
|
{comment.status === "approved" && (
|
||||||
{comment.status === 'pending' && <Badge color="orange">待审核</Badge>}
|
<Badge color="green">已通过</Badge>
|
||||||
{comment.status === 'rejected' && <Badge color="red">已拒绝</Badge>}
|
)}
|
||||||
|
{comment.status === "pending" && (
|
||||||
|
<Badge color="orange">待审核</Badge>
|
||||||
|
)}
|
||||||
|
{comment.status === "rejected" && (
|
||||||
|
<Badge color="red">已拒绝</Badge>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</DataList.Value>
|
</DataList.Value>
|
||||||
</DataList.Item>
|
</DataList.Item>
|
||||||
@ -277,11 +283,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
>
|
>
|
||||||
<Cross2Icon className="w-4 h-4" />
|
<Cross2Icon className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="ghost" size="1" color="red">
|
||||||
variant="ghost"
|
|
||||||
size="1"
|
|
||||||
color="red"
|
|
||||||
>
|
|
||||||
<TrashIcon className="w-4 h-4" />
|
<TrashIcon className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
Dialog,
|
Dialog,
|
||||||
DataList
|
DataList,
|
||||||
} from "@radix-ui/themes";
|
} from "@radix-ui/themes";
|
||||||
import {
|
import {
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
@ -22,7 +22,7 @@ import {
|
|||||||
DotsHorizontalIcon,
|
DotsHorizontalIcon,
|
||||||
FileTextIcon,
|
FileTextIcon,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
VideoIcon
|
VideoIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { Resource } from "interface/fields";
|
import type { Resource } from "interface/fields";
|
||||||
@ -38,7 +38,7 @@ const mockFiles: Resource[] = [
|
|||||||
fileType: "application/pdf",
|
fileType: "application/pdf",
|
||||||
category: "documents",
|
category: "documents",
|
||||||
description: "前端开发规范文档",
|
description: "前端开发规范文档",
|
||||||
createdAt: new Date("2024-03-15")
|
createdAt: new Date("2024-03-15"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@ -49,7 +49,7 @@ const mockFiles: Resource[] = [
|
|||||||
fileType: "image/png",
|
fileType: "image/png",
|
||||||
category: "images",
|
category: "images",
|
||||||
description: "项目整体架构示意图",
|
description: "项目整体架构示意图",
|
||||||
createdAt: new Date("2024-03-14")
|
createdAt: new Date("2024-03-14"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
@ -60,8 +60,8 @@ const mockFiles: Resource[] = [
|
|||||||
fileType: "text/markdown",
|
fileType: "text/markdown",
|
||||||
category: "documents",
|
category: "documents",
|
||||||
description: "API接口文档",
|
description: "API接口文档",
|
||||||
createdAt: new Date("2024-03-13")
|
createdAt: new Date("2024-03-13"),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default new Template({}, ({ http, args }) => {
|
export default new Template({}, ({ http, args }) => {
|
||||||
@ -71,18 +71,19 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
|
|
||||||
// 格式化文件大小
|
// 格式化文件大小
|
||||||
const formatFileSize = (bytes: number) => {
|
const formatFileSize = (bytes: number) => {
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return "0 Bytes";
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取文件图标
|
// 获取文件图标
|
||||||
const getFileIcon = (fileType: string) => {
|
const getFileIcon = (fileType: string) => {
|
||||||
if (fileType.startsWith('image/')) return <ImageIcon className="w-4 h-4" />;
|
if (fileType.startsWith("image/")) return <ImageIcon className="w-4 h-4" />;
|
||||||
if (fileType.startsWith('video/')) return <VideoIcon className="w-4 h-4" />;
|
if (fileType.startsWith("video/")) return <VideoIcon className="w-4 h-4" />;
|
||||||
if (fileType.startsWith('text/')) return <FileTextIcon className="w-4 h-4" />;
|
if (fileType.startsWith("text/"))
|
||||||
|
return <FileTextIcon className="w-4 h-4" />;
|
||||||
return <FileIcon className="w-4 h-4" />;
|
return <FileIcon className="w-4 h-4" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -94,9 +95,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<Heading size="6" className="text-[--gray-12] mb-2">
|
<Heading size="6" className="text-[--gray-12] mb-2">
|
||||||
文件管理
|
文件管理
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text className="text-[--gray-11]">
|
<Text className="text-[--gray-11]">共 {mockFiles.length} 个文件</Text>
|
||||||
共 {mockFiles.length} 个文件
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Button
|
<Button
|
||||||
className="bg-[--accent-9]"
|
className="bg-[--accent-9]"
|
||||||
@ -108,15 +107,14 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* 搜索和筛选栏 */}
|
{/* 搜索和筛选栏 */}
|
||||||
<Flex
|
<Flex gap="4" className="mb-6 flex-col sm:flex-row">
|
||||||
gap="4"
|
|
||||||
className="mb-6 flex-col sm:flex-row"
|
|
||||||
>
|
|
||||||
<Box className="w-full sm:w-64">
|
<Box className="w-full sm:w-64">
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
placeholder="搜索文件..."
|
placeholder="搜索文件..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setSearchTerm(e.target.value)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<TextField.Slot>
|
<TextField.Slot>
|
||||||
<MagnifyingGlassIcon height="16" width="16" />
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
@ -127,20 +125,20 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<Button variant="surface">
|
<Button variant="surface">
|
||||||
类型: {selectedType === 'all' ? '全部' : selectedType}
|
类型: {selectedType === "all" ? "全部" : selectedType}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
<DropdownMenu.Item onClick={() => setSelectedType('all')}>
|
<DropdownMenu.Item onClick={() => setSelectedType("all")}>
|
||||||
全部
|
全部
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item onClick={() => setSelectedType('documents')}>
|
<DropdownMenu.Item onClick={() => setSelectedType("documents")}>
|
||||||
文档
|
文档
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item onClick={() => setSelectedType('images')}>
|
<DropdownMenu.Item onClick={() => setSelectedType("images")}>
|
||||||
图片
|
图片
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item onClick={() => setSelectedType('others')}>
|
<DropdownMenu.Item onClick={() => setSelectedType("others")}>
|
||||||
其他
|
其他
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
@ -172,12 +170,10 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<Text className="font-medium">{file.name}</Text>
|
<Text className="font-medium">{file.name}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>{formatFileSize(file.sizeBytes)}</Table.Cell>
|
||||||
{formatFileSize(file.sizeBytes)}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Text className="text-[--gray-11]">
|
<Text className="text-[--gray-11]">
|
||||||
{file.fileType.split('/')[1].toUpperCase()}
|
{file.fileType.split("/")[1].toUpperCase()}
|
||||||
</Text>
|
</Text>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
@ -202,7 +198,10 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
{/* 移动端列表视图 */}
|
{/* 移动端列表视图 */}
|
||||||
<div className="block sm:hidden">
|
<div className="block sm:hidden">
|
||||||
{mockFiles.map((file) => (
|
{mockFiles.map((file) => (
|
||||||
<DataList.Root key={file.id} className="p-4 border-b border-[--gray-6] last:border-b-0">
|
<DataList.Root
|
||||||
|
key={file.id}
|
||||||
|
className="p-4 border-b border-[--gray-6] last:border-b-0"
|
||||||
|
>
|
||||||
<DataList.Item>
|
<DataList.Item>
|
||||||
<DataList.Label minWidth="88px">文件名</DataList.Label>
|
<DataList.Label minWidth="88px">文件名</DataList.Label>
|
||||||
<DataList.Value>
|
<DataList.Value>
|
||||||
@ -223,7 +222,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<DataList.Item>
|
<DataList.Item>
|
||||||
<DataList.Label minWidth="88px">类型</DataList.Label>
|
<DataList.Label minWidth="88px">类型</DataList.Label>
|
||||||
<DataList.Value>
|
<DataList.Value>
|
||||||
{file.fileType.split('/')[1].toUpperCase()}
|
{file.fileType.split("/")[1].toUpperCase()}
|
||||||
</DataList.Value>
|
</DataList.Value>
|
||||||
</DataList.Item>
|
</DataList.Item>
|
||||||
|
|
||||||
@ -254,7 +253,10 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 上传对话框 */}
|
{/* 上传对话框 */}
|
||||||
<Dialog.Root open={isUploadDialogOpen} onOpenChange={setIsUploadDialogOpen}>
|
<Dialog.Root
|
||||||
|
open={isUploadDialogOpen}
|
||||||
|
onOpenChange={setIsUploadDialogOpen}
|
||||||
|
>
|
||||||
<Dialog.Content style={{ maxWidth: 450 }}>
|
<Dialog.Content style={{ maxWidth: 450 }}>
|
||||||
<Dialog.Title>上传文件</Dialog.Title>
|
<Dialog.Title>上传文件</Dialog.Title>
|
||||||
<Dialog.Description size="2" mb="4">
|
<Dialog.Description size="2" mb="4">
|
||||||
@ -272,10 +274,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
console.log(e.target.files);
|
console.log(e.target.files);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<label
|
<label htmlFor="file-upload" className="cursor-pointer">
|
||||||
htmlFor="file-upload"
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<FileIcon className="w-12 h-12 mx-auto mb-4 text-[--gray-9]" />
|
<FileIcon className="w-12 h-12 mx-auto mb-4 text-[--gray-9]" />
|
||||||
<Text className="text-[--gray-11] mb-2">
|
<Text className="text-[--gray-11] mb-2">
|
||||||
拖拽文件到此处或点击上传
|
拖拽文件到此处或点击上传
|
||||||
@ -293,9 +292,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
<Dialog.Close>
|
<Dialog.Close>
|
||||||
<Button className="bg-[--accent-9]">
|
<Button className="bg-[--accent-9]">开始上传</Button>
|
||||||
开始上传
|
|
||||||
</Button>
|
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
|
@ -102,11 +102,11 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
className="w-10 h-10 rounded-full flex items-center justify-center"
|
className="w-10 h-10 rounded-full flex items-center justify-center"
|
||||||
style={{ backgroundColor: `color-mix(in srgb, ${stat.color} 15%, transparent)` }}
|
style={{
|
||||||
|
backgroundColor: `color-mix(in srgb, ${stat.color} 15%, transparent)`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box style={{ color: stat.color }}>
|
<Box style={{ color: stat.color }}>{stat.icon}</Box>
|
||||||
{stat.icon}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
@ -152,12 +152,13 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
className={`px-2 py-1 rounded-full text-xs
|
className={`px-2 py-1 rounded-full text-xs
|
||||||
${post.status === 'published'
|
${
|
||||||
? 'bg-[--green-3] text-[--green-11]'
|
post.status === "published"
|
||||||
: 'bg-[--gray-3] text-[--gray-11]'
|
? "bg-[--green-3] text-[--green-11]"
|
||||||
|
: "bg-[--gray-3] text-[--gray-11]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{post.status === 'published' ? '已发布' : '草稿'}
|
{post.status === "published" ? "已发布" : "草稿"}
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -214,7 +214,7 @@ export default new Layout(({ children }) => {
|
|||||||
size="3"
|
size="3"
|
||||||
className="gap-2 text-base"
|
className="gap-2 text-base"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.location.href = '/';
|
window.location.href = "/";
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HomeIcon className="w-5 h-5" />
|
<HomeIcon className="w-5 h-5" />
|
||||||
@ -228,7 +228,7 @@ export default new Layout(({ children }) => {
|
|||||||
className="gap-2 text-base"
|
className="gap-2 text-base"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// 这里添加退出登录的逻辑
|
// 这里添加退出登录的逻辑
|
||||||
console.log('退出登录');
|
console.log("退出登录");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ExitIcon className="w-5 h-5" />
|
<ExitIcon className="w-5 h-5" />
|
||||||
@ -243,10 +243,7 @@ export default new Layout(({ children }) => {
|
|||||||
id="main-content"
|
id="main-content"
|
||||||
className="flex-1 overflow-y-auto bg-[--gray-2]"
|
className="flex-1 overflow-y-auto bg-[--gray-2]"
|
||||||
>
|
>
|
||||||
<Container
|
<Container size="4" className="py-6 px-4">
|
||||||
size="4"
|
|
||||||
className="py-6 px-4"
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Container>
|
</Container>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -4,9 +4,9 @@ import { Container, Heading, Text, Box, Flex, Button } from "@radix-ui/themes";
|
|||||||
import { PersonIcon, LockClosedIcon } from "@radix-ui/react-icons";
|
import { PersonIcon, LockClosedIcon } from "@radix-ui/react-icons";
|
||||||
import { useEffect, useRef, useState, useMemo } from "react";
|
import { useEffect, useRef, useState, useMemo } from "react";
|
||||||
import { gsap } from "gsap";
|
import { gsap } from "gsap";
|
||||||
import { AnimatedBackground } from 'hooks/Background';
|
import { AnimatedBackground } from "hooks/Background";
|
||||||
import { useThemeMode, ThemeModeToggle } from 'hooks/ThemeMode';
|
import { useThemeMode, ThemeModeToggle } from "hooks/ThemeMode";
|
||||||
import { useNotification } from 'hooks/Notification';
|
import { useNotification } from "hooks/Notification";
|
||||||
|
|
||||||
export default new Template({}, ({ http, args }) => {
|
export default new Template({}, ({ http, args }) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@ -59,16 +59,16 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 这里添加登录逻辑
|
// 这里添加登录逻辑
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500)); // 模拟API请求
|
await new Promise((resolve) => setTimeout(resolve, 1500)); // 模拟API请求
|
||||||
|
|
||||||
// 登录成功的通知
|
// 登录成功的通知
|
||||||
notification.success('登录成功', '欢迎回来!');
|
notification.success("登录成功", "欢迎回来!");
|
||||||
|
|
||||||
// 登录成功后的处理
|
// 登录成功后的处理
|
||||||
console.log("Login successful");
|
console.log("Login successful");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 登录失败的通知
|
// 登录失败的通知
|
||||||
notification.error('登录失败', '用户名或密码错误');
|
notification.error("登录失败", "用户名或密码错误");
|
||||||
console.error("Login failed:", error);
|
console.error("Login failed:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -76,14 +76,18 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleBackgroundError = () => {
|
const handleBackgroundError = () => {
|
||||||
console.log('Background failed to load, switching to fallback');
|
console.log("Background failed to load, switching to fallback");
|
||||||
setHasBackgroundError(true);
|
setHasBackgroundError(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 使用 useMemo 包裹背景组件
|
// 使用 useMemo 包裹背景组件
|
||||||
const backgroundComponent = useMemo(() => (
|
const backgroundComponent = useMemo(
|
||||||
!hasBackgroundError && <AnimatedBackground onError={handleBackgroundError} />
|
() =>
|
||||||
), [hasBackgroundError]);
|
!hasBackgroundError && (
|
||||||
|
<AnimatedBackground onError={handleBackgroundError} />
|
||||||
|
),
|
||||||
|
[hasBackgroundError],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen">
|
<div className="relative min-h-screen">
|
||||||
@ -91,10 +95,12 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
|
|
||||||
<Box
|
<Box
|
||||||
className="fixed top-4 right-4 z-20 w-10 h-10 flex items-center justify-center [&_button]:w-10 [&_button]:h-10 [&_svg]:w-6 [&_svg]:h-6"
|
className="fixed top-4 right-4 z-20 w-10 h-10 flex items-center justify-center [&_button]:w-10 [&_button]:h-10 [&_svg]:w-6 [&_svg]:h-6"
|
||||||
style={{
|
style={
|
||||||
'--button-color': 'var(--gray-12)',
|
{
|
||||||
'--button-hover-color': 'var(--accent-9)'
|
"--button-color": "var(--gray-12)",
|
||||||
} as React.CSSProperties}
|
"--button-hover-color": "var(--accent-9)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ThemeModeToggle />
|
<ThemeModeToggle />
|
||||||
</Box>
|
</Box>
|
||||||
@ -109,8 +115,11 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<Box
|
<Box
|
||||||
className="login-box backdrop-blur-sm rounded-lg shadow-lg p-8 border transition-colors duration-300"
|
className="login-box backdrop-blur-sm rounded-lg shadow-lg p-8 border transition-colors duration-300"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: mode === 'dark' ? 'var(--gray-2-alpha-80)' : 'var(--white-alpha-80)',
|
backgroundColor:
|
||||||
borderColor: 'var(--gray-6)'
|
mode === "dark"
|
||||||
|
? "var(--gray-2-alpha-80)"
|
||||||
|
: "var(--white-alpha-80)",
|
||||||
|
borderColor: "var(--gray-6)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
@ -118,7 +127,6 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<Heading size="6" className="text-center mb-2">
|
<Heading size="6" className="text-center mb-2">
|
||||||
后台
|
后台
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* 登录表单 */}
|
{/* 登录表单 */}
|
||||||
@ -151,11 +159,13 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
{/* 登录按钮 */}
|
{/* 登录按钮 */}
|
||||||
<Button
|
<Button
|
||||||
className="login-button w-full h-10 transition-colors duration-300 hover:bg-[--hover-bg]"
|
className="login-button w-full h-10 transition-colors duration-300 hover:bg-[--hover-bg]"
|
||||||
style={{
|
style={
|
||||||
backgroundColor: 'var(--accent-9)',
|
{
|
||||||
color: 'white',
|
backgroundColor: "var(--accent-9)",
|
||||||
'--hover-bg': 'var(--accent-10)'
|
color: "white",
|
||||||
} as React.CSSProperties}
|
"--hover-bg": "var(--accent-10)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
size="3"
|
size="3"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@ -168,10 +178,12 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<Text
|
<Text
|
||||||
size="2"
|
size="2"
|
||||||
className="cursor-pointer transition-colors duration-300 hover:text-[--hover-color]"
|
className="cursor-pointer transition-colors duration-300 hover:text-[--hover-color]"
|
||||||
style={{
|
style={
|
||||||
color: 'var(--gray-11)',
|
{
|
||||||
'--hover-color': 'var(--accent-9)'
|
color: "var(--gray-11)",
|
||||||
} as React.CSSProperties}
|
"--hover-color": "var(--accent-9)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
>
|
>
|
||||||
忘记密码?
|
忘记密码?
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
Dialog,
|
Dialog,
|
||||||
Tabs,
|
Tabs,
|
||||||
Switch,
|
Switch,
|
||||||
IconButton
|
IconButton,
|
||||||
} from "@radix-ui/themes";
|
} from "@radix-ui/themes";
|
||||||
import {
|
import {
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
@ -25,13 +25,17 @@ import {
|
|||||||
CheckIcon,
|
CheckIcon,
|
||||||
UpdateIcon,
|
UpdateIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
ExclamationTriangleIcon
|
ExclamationTriangleIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { PluginConfig } from "interface/plugin";
|
import type { PluginConfig } from "interface/plugin";
|
||||||
|
|
||||||
// 模拟插件数据
|
// 模拟插件数据
|
||||||
const mockPlugins: (PluginConfig & { id: number; preview?: string; installed?: boolean })[] = [
|
const mockPlugins: (PluginConfig & {
|
||||||
|
id: number;
|
||||||
|
preview?: string;
|
||||||
|
installed?: boolean;
|
||||||
|
})[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "comment-system",
|
name: "comment-system",
|
||||||
@ -41,7 +45,8 @@ const mockPlugins: (PluginConfig & { id: number; preview?: string; installed?: b
|
|||||||
author: "Admin",
|
author: "Admin",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
icon: "https://api.iconify.design/material-symbols:comment.svg",
|
icon: "https://api.iconify.design/material-symbols:comment.svg",
|
||||||
preview: "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format",
|
preview:
|
||||||
|
"https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format",
|
||||||
managePath: "/dashboard/plugins/comment-system",
|
managePath: "/dashboard/plugins/comment-system",
|
||||||
installed: true,
|
installed: true,
|
||||||
configuration: {
|
configuration: {
|
||||||
@ -51,11 +56,11 @@ const mockPlugins: (PluginConfig & { id: number; preview?: string; installed?: b
|
|||||||
data: {
|
data: {
|
||||||
provider: "gitalk",
|
provider: "gitalk",
|
||||||
clientId: "",
|
clientId: "",
|
||||||
clientSecret: ""
|
clientSecret: "",
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
routes: new Set()
|
},
|
||||||
|
},
|
||||||
|
routes: new Set(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@ -66,7 +71,8 @@ const mockPlugins: (PluginConfig & { id: number; preview?: string; installed?: b
|
|||||||
author: "ThirdParty",
|
author: "ThirdParty",
|
||||||
enabled: false,
|
enabled: false,
|
||||||
icon: "https://api.iconify.design/material-symbols:image.svg",
|
icon: "https://api.iconify.design/material-symbols:image.svg",
|
||||||
preview: "https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?w=500&auto=format",
|
preview:
|
||||||
|
"https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?w=500&auto=format",
|
||||||
installed: true,
|
installed: true,
|
||||||
configuration: {
|
configuration: {
|
||||||
system: {
|
system: {
|
||||||
@ -75,12 +81,12 @@ const mockPlugins: (PluginConfig & { id: number; preview?: string; installed?: b
|
|||||||
data: {
|
data: {
|
||||||
quality: 80,
|
quality: 80,
|
||||||
maxWidth: 1920,
|
maxWidth: 1920,
|
||||||
watermark: false
|
watermark: false,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
routes: new Set(),
|
||||||
},
|
},
|
||||||
routes: new Set()
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// 模拟市场插件数据
|
// 模拟市场插件数据
|
||||||
@ -104,9 +110,10 @@ const marketPlugins: MarketPlugin[] = [
|
|||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
description: "自动优化上传的图片,支持压缩、裁剪、水印等功能",
|
description: "自动优化上传的图片,支持压缩、裁剪、水印等功能",
|
||||||
author: "ThirdParty",
|
author: "ThirdParty",
|
||||||
preview: "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format",
|
preview:
|
||||||
|
"https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format",
|
||||||
downloads: 1200,
|
downloads: 1200,
|
||||||
rating: 4.5
|
rating: 4.5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
@ -115,22 +122,25 @@ const marketPlugins: MarketPlugin[] = [
|
|||||||
version: "2.0.0",
|
version: "2.0.0",
|
||||||
description: "增强的Markdown编辑器,支持更多扩展语法和实时预览",
|
description: "增强的Markdown编辑器,支持更多扩展语法和实时预览",
|
||||||
author: "ThirdParty",
|
author: "ThirdParty",
|
||||||
preview: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=500&auto=format",
|
preview:
|
||||||
|
"https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=500&auto=format",
|
||||||
downloads: 3500,
|
downloads: 3500,
|
||||||
rating: 4.8
|
rating: 4.8,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default new Template({}, ({ http, args }) => {
|
export default new Template({}, ({ http, args }) => {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
const [selectedPlugin, setSelectedPlugin] = useState<typeof mockPlugins[0] | null>(null);
|
const [selectedPlugin, setSelectedPlugin] = useState<
|
||||||
|
(typeof mockPlugins)[0] | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
// 处理插件启用/禁用
|
// 处理插件启用/禁用
|
||||||
const handleTogglePlugin = (pluginId: number) => {
|
const handleTogglePlugin = (pluginId: number) => {
|
||||||
// 这里添加启用/禁用插件的逻辑
|
// 这里添加启用/禁用插件的逻辑
|
||||||
console.log('Toggle plugin:', pluginId);
|
console.log("Toggle plugin:", pluginId);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -159,7 +169,9 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<TextField.Root
|
<TextField.Root
|
||||||
placeholder="搜索插件..."
|
placeholder="搜索插件..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setSearchTerm(e.target.value)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<TextField.Slot>
|
<TextField.Slot>
|
||||||
<MagnifyingGlassIcon height="16" width="16" />
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
@ -170,7 +182,10 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
{/* 插件列表 */}
|
{/* 插件列表 */}
|
||||||
<Box className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<Box className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{mockPlugins.map((plugin) => (
|
{mockPlugins.map((plugin) => (
|
||||||
<Card key={plugin.id} className="p-4 border border-[--gray-6] hover-card">
|
<Card
|
||||||
|
key={plugin.id}
|
||||||
|
className="p-4 border border-[--gray-6] hover-card"
|
||||||
|
>
|
||||||
{/* 插件预览图 */}
|
{/* 插件预览图 */}
|
||||||
{plugin.preview && (
|
{plugin.preview && (
|
||||||
<Box className="aspect-video mb-4 rounded-lg overflow-hidden bg-[--gray-3]">
|
<Box className="aspect-video mb-4 rounded-lg overflow-hidden bg-[--gray-3]">
|
||||||
@ -216,11 +231,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
配置
|
配置
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button variant="soft" color="red" className="flex-1">
|
||||||
variant="soft"
|
|
||||||
color="red"
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
<TrashIcon className="w-4 h-4" />
|
<TrashIcon className="w-4 h-4" />
|
||||||
卸载
|
卸载
|
||||||
</Button>
|
</Button>
|
||||||
@ -249,10 +260,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
console.log(e.target.files);
|
console.log(e.target.files);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<label
|
<label htmlFor="plugin-upload" className="cursor-pointer">
|
||||||
htmlFor="plugin-upload"
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<CodeIcon className="w-12 h-12 mx-auto mb-4 text-[--gray-9]" />
|
<CodeIcon className="w-12 h-12 mx-auto mb-4 text-[--gray-9]" />
|
||||||
<Text className="text-[--gray-11] mb-2">
|
<Text className="text-[--gray-11] mb-2">
|
||||||
点击上传插件包或拖拽到此处
|
点击上传插件包或拖拽到此处
|
||||||
@ -271,9 +279,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
<Dialog.Close>
|
<Dialog.Close>
|
||||||
<Button className="bg-[--accent-9]">
|
<Button className="bg-[--accent-9]">开始安装</Button>
|
||||||
开始安装
|
|
||||||
</Button>
|
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
DataList,
|
DataList,
|
||||||
Badge
|
Badge,
|
||||||
} from "@radix-ui/themes";
|
} from "@radix-ui/themes";
|
||||||
import {
|
import {
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
@ -40,7 +40,7 @@ const mockPosts: PostDisplay[] = [
|
|||||||
metaKeywords: "",
|
metaKeywords: "",
|
||||||
metaDescription: "",
|
metaDescription: "",
|
||||||
categories: [{ name: "前端开发" }],
|
categories: [{ name: "前端开发" }],
|
||||||
tags: [{ name: "工程化" }, { name: "效率提升" }]
|
tags: [{ name: "工程化" }, { name: "效率提升" }],
|
||||||
},
|
},
|
||||||
// ... 可以添加更多模拟数据
|
// ... 可以添加更多模拟数据
|
||||||
];
|
];
|
||||||
@ -71,7 +71,9 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<TextField.Root
|
<TextField.Root
|
||||||
placeholder="搜索文章..."
|
placeholder="搜索文章..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setSearchTerm(e.target.value)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<TextField.Slot side="right">
|
<TextField.Slot side="right">
|
||||||
<MagnifyingGlassIcon height="16" width="16" />
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
@ -82,17 +84,17 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<Button variant="surface">
|
<Button variant="surface">
|
||||||
状态: {selectedStatus === 'all' ? '全部' : selectedStatus}
|
状态: {selectedStatus === "all" ? "全部" : selectedStatus}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
<DropdownMenu.Item onClick={() => setSelectedStatus('all')}>
|
<DropdownMenu.Item onClick={() => setSelectedStatus("all")}>
|
||||||
全部
|
全部
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item onClick={() => setSelectedStatus('published')}>
|
<DropdownMenu.Item onClick={() => setSelectedStatus("published")}>
|
||||||
已发布
|
已发布
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item onClick={() => setSelectedStatus('draft')}>
|
<DropdownMenu.Item onClick={() => setSelectedStatus("draft")}>
|
||||||
草稿
|
草稿
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
@ -117,7 +119,10 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
{mockPosts.map((post) => (
|
{mockPosts.map((post) => (
|
||||||
<Table.Row key={post.id} className="hover:bg-[--gray-3] block sm:table-row mb-4 sm:mb-0">
|
<Table.Row
|
||||||
|
key={post.id}
|
||||||
|
className="hover:bg-[--gray-3] block sm:table-row mb-4 sm:mb-0"
|
||||||
|
>
|
||||||
<Table.Cell className="font-medium block sm:table-cell py-2 sm:py-3 before:content-['标题:'] before:inline-block before:w-20 before:font-normal sm:before:content-none">
|
<Table.Cell className="font-medium block sm:table-cell py-2 sm:py-3 before:content-['标题:'] before:inline-block before:w-20 before:font-normal sm:before:content-none">
|
||||||
{post.title}
|
{post.title}
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
@ -139,7 +144,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell className="block sm:table-cell py-2 sm:py-3 before:content-['状态:'] before:inline-block before:w-20 before:font-normal sm:before:content-none">
|
<Table.Cell className="block sm:table-cell py-2 sm:py-3 before:content-['状态:'] before:inline-block before:w-20 before:font-normal sm:before:content-none">
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
{post.status === 'published' ? (
|
{post.status === "published" ? (
|
||||||
<Badge color="green">已发布</Badge>
|
<Badge color="green">已发布</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge color="orange">草稿</Badge>
|
<Badge color="orange">草稿</Badge>
|
||||||
@ -171,7 +176,10 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
{/* 移动端列表视图 */}
|
{/* 移动端列表视图 */}
|
||||||
<div className="block sm:hidden">
|
<div className="block sm:hidden">
|
||||||
{mockPosts.map((post) => (
|
{mockPosts.map((post) => (
|
||||||
<DataList.Root key={post.id} className="p-4 border-b border-[--gray-6] last:border-b-0">
|
<DataList.Root
|
||||||
|
key={post.id}
|
||||||
|
className="p-4 border-b border-[--gray-6] last:border-b-0"
|
||||||
|
>
|
||||||
<DataList.Item>
|
<DataList.Item>
|
||||||
<DataList.Label minWidth="88px">标题</DataList.Label>
|
<DataList.Label minWidth="88px">标题</DataList.Label>
|
||||||
<DataList.Value>
|
<DataList.Value>
|
||||||
@ -205,7 +213,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<DataList.Label minWidth="88px">状态</DataList.Label>
|
<DataList.Label minWidth="88px">状态</DataList.Label>
|
||||||
<DataList.Value>
|
<DataList.Value>
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
{post.status === 'published' ? (
|
{post.status === "published" ? (
|
||||||
<Badge color="green">已发布</Badge>
|
<Badge color="green">已发布</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge color="orange">草稿</Badge>
|
<Badge color="orange">草稿</Badge>
|
||||||
|
@ -10,14 +10,14 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
Switch,
|
Switch,
|
||||||
Tabs,
|
Tabs,
|
||||||
TextArea
|
TextArea,
|
||||||
} from "@radix-ui/themes";
|
} from "@radix-ui/themes";
|
||||||
import {
|
import {
|
||||||
GearIcon,
|
GearIcon,
|
||||||
PersonIcon,
|
PersonIcon,
|
||||||
LockClosedIcon,
|
LockClosedIcon,
|
||||||
BellIcon,
|
BellIcon,
|
||||||
GlobeIcon
|
GlobeIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
@ -62,7 +62,9 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
value={siteName}
|
value={siteName}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSiteName(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setSiteName(e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@ -72,7 +74,9 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
<TextArea
|
<TextArea
|
||||||
value={siteDescription}
|
value={siteDescription}
|
||||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setSiteDescription(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
|
setSiteDescription(e.target.value)
|
||||||
|
}
|
||||||
className="min-h-[100px]"
|
className="min-h-[100px]"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@ -81,9 +85,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<Text as="label" size="2" weight="bold" className="block mb-2">
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
站点语言
|
站点语言
|
||||||
</Text>
|
</Text>
|
||||||
<select
|
<select className="w-full h-9 px-3 rounded-md bg-[--gray-1] border border-[--gray-6] text-[--gray-12]">
|
||||||
className="w-full h-9 px-3 rounded-md bg-[--gray-1] border border-[--gray-6] text-[--gray-12]"
|
|
||||||
>
|
|
||||||
<option value="zh-CN">简体中文</option>
|
<option value="zh-CN">简体中文</option>
|
||||||
<option value="en-US">English</option>
|
<option value="en-US">English</option>
|
||||||
</select>
|
</select>
|
||||||
@ -93,9 +95,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<Text as="label" size="2" weight="bold" className="block mb-2">
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
时区设置
|
时区设置
|
||||||
</Text>
|
</Text>
|
||||||
<select
|
<select className="w-full h-9 px-3 rounded-md bg-[--gray-1] border border-[--gray-6] text-[--gray-12]">
|
||||||
className="w-full h-9 px-3 rounded-md bg-[--gray-1] border border-[--gray-6] text-[--gray-12]"
|
|
||||||
>
|
|
||||||
<option value="UTC+8">UTC+8 北京时间</option>
|
<option value="UTC+8">UTC+8 北京时间</option>
|
||||||
<option value="UTC+0">UTC+0 格林威治时间</option>
|
<option value="UTC+0">UTC+0 格林威治时间</option>
|
||||||
</select>
|
</select>
|
||||||
@ -156,18 +156,9 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
修改密码
|
修改密码
|
||||||
</Text>
|
</Text>
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<TextField.Root
|
<TextField.Root type="password" placeholder="当前密码" />
|
||||||
type="password"
|
<TextField.Root type="password" placeholder="新密码" />
|
||||||
placeholder="当前密码"
|
<TextField.Root type="password" placeholder="确认新密码" />
|
||||||
/>
|
|
||||||
<TextField.Root
|
|
||||||
type="password"
|
|
||||||
placeholder="新密码"
|
|
||||||
/>
|
|
||||||
<TextField.Root
|
|
||||||
type="password"
|
|
||||||
placeholder="确认新密码"
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@ -221,9 +212,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
|
|
||||||
{/* 保存按钮 */}
|
{/* 保存按钮 */}
|
||||||
<Flex justify="end" className="mt-6">
|
<Flex justify="end" className="mt-6">
|
||||||
<Button className="bg-[--accent-9]">
|
<Button className="bg-[--accent-9]">保存更改</Button>
|
||||||
保存更改
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -25,7 +25,11 @@ import { useState } from "react";
|
|||||||
import type { ThemeConfig } from "interface/theme";
|
import type { ThemeConfig } from "interface/theme";
|
||||||
|
|
||||||
// 模拟主题数据
|
// 模拟主题数据
|
||||||
const mockThemes: (ThemeConfig & { id: number; preview: string; active: boolean })[] = [
|
const mockThemes: (ThemeConfig & {
|
||||||
|
id: number;
|
||||||
|
preview: string;
|
||||||
|
active: boolean;
|
||||||
|
})[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "echoes",
|
name: "echoes",
|
||||||
@ -33,7 +37,8 @@ const mockThemes: (ThemeConfig & { id: number; preview: string; active: boolean
|
|||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
description: "默认主题",
|
description: "默认主题",
|
||||||
author: "Admin",
|
author: "Admin",
|
||||||
preview: "https://images.unsplash.com/photo-1481487196290-c152efe083f5?w=500&auto=format",
|
preview:
|
||||||
|
"https://images.unsplash.com/photo-1481487196290-c152efe083f5?w=500&auto=format",
|
||||||
templates: new Map(),
|
templates: new Map(),
|
||||||
configuration: {
|
configuration: {
|
||||||
theme: {
|
theme: {
|
||||||
@ -42,13 +47,13 @@ const mockThemes: (ThemeConfig & { id: number; preview: string; active: boolean
|
|||||||
data: {
|
data: {
|
||||||
colors: {
|
colors: {
|
||||||
mode: "light",
|
mode: "light",
|
||||||
layout: "default"
|
layout: "default",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
routes: new Map(),
|
routes: new Map(),
|
||||||
active: true
|
active: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@ -57,7 +62,8 @@ const mockThemes: (ThemeConfig & { id: number; preview: string; active: boolean
|
|||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
description: "简约风格主题",
|
description: "简约风格主题",
|
||||||
author: "Admin",
|
author: "Admin",
|
||||||
preview: "https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?w=500&auto=format",
|
preview:
|
||||||
|
"https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?w=500&auto=format",
|
||||||
templates: new Map(),
|
templates: new Map(),
|
||||||
configuration: {
|
configuration: {
|
||||||
theme: {
|
theme: {
|
||||||
@ -66,20 +72,22 @@ const mockThemes: (ThemeConfig & { id: number; preview: string; active: boolean
|
|||||||
data: {
|
data: {
|
||||||
colors: {
|
colors: {
|
||||||
mode: "light",
|
mode: "light",
|
||||||
layout: "default"
|
layout: "default",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
routes: new Map(),
|
routes: new Map(),
|
||||||
active: false
|
active: false,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default new Template({}, ({ http, args }) => {
|
export default new Template({}, ({ http, args }) => {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
const [selectedTheme, setSelectedTheme] = useState<typeof mockThemes[0] | null>(null);
|
const [selectedTheme, setSelectedTheme] = useState<
|
||||||
|
(typeof mockThemes)[0] | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
@ -114,7 +122,10 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
{/* 主题列表 */}
|
{/* 主题列表 */}
|
||||||
<Box className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<Box className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{mockThemes.map((theme) => (
|
{mockThemes.map((theme) => (
|
||||||
<Card key={theme.id} className="p-4 border border-[--gray-6] hover-card">
|
<Card
|
||||||
|
key={theme.id}
|
||||||
|
className="p-4 border border-[--gray-6] hover-card"
|
||||||
|
>
|
||||||
{/* 预览图 */}
|
{/* 预览图 */}
|
||||||
<Box className="aspect-video mb-4 rounded-lg overflow-hidden bg-[--gray-3]">
|
<Box className="aspect-video mb-4 rounded-lg overflow-hidden bg-[--gray-3]">
|
||||||
<img
|
<img
|
||||||
@ -129,7 +140,10 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<Flex justify="between" align="center">
|
<Flex justify="between" align="center">
|
||||||
<Heading size="3">{theme.displayName}</Heading>
|
<Heading size="3">{theme.displayName}</Heading>
|
||||||
{theme.active && (
|
{theme.active && (
|
||||||
<Text size="1" className="px-2 py-1 bg-[--accent-3] text-[--accent-9] rounded">
|
<Text
|
||||||
|
size="1"
|
||||||
|
className="px-2 py-1 bg-[--accent-3] text-[--accent-9] rounded"
|
||||||
|
>
|
||||||
当前使用
|
当前使用
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@ -149,16 +163,16 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="soft"
|
variant="soft"
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
onClick={() => window.location.href = `/dashboard/themes/${theme.name}/settings`}
|
onClick={() =>
|
||||||
|
(window.location.href = `/dashboard/themes/${theme.name}/settings`)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<GearIcon className="w-4 h-4" />
|
<GearIcon className="w-4 h-4" />
|
||||||
配置
|
配置
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button className="flex-1 bg-[--accent-9]">
|
||||||
className="flex-1 bg-[--accent-9]"
|
|
||||||
>
|
|
||||||
<CheckIcon className="w-4 h-4" />
|
<CheckIcon className="w-4 h-4" />
|
||||||
启用
|
启用
|
||||||
</Button>
|
</Button>
|
||||||
@ -168,7 +182,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
className="flex-1"
|
className="flex-1"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// 这里添加卸载主题的处理逻辑
|
// 这里添加卸载主题的处理逻辑
|
||||||
console.log('卸载主题:', theme.name);
|
console.log("卸载主题:", theme.name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Cross2Icon className="w-4 h-4" />
|
<Cross2Icon className="w-4 h-4" />
|
||||||
@ -201,10 +215,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
console.log(e.target.files);
|
console.log(e.target.files);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<label
|
<label htmlFor="theme-upload" className="cursor-pointer">
|
||||||
htmlFor="theme-upload"
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<CodeIcon className="w-12 h-12 mx-auto mb-4 text-[--gray-9]" />
|
<CodeIcon className="w-12 h-12 mx-auto mb-4 text-[--gray-9]" />
|
||||||
<Text className="text-[--gray-11] mb-2">
|
<Text className="text-[--gray-11] mb-2">
|
||||||
点击上传主题包或拖拽到此处
|
点击上传主题包或拖拽到此处
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
Dialog,
|
Dialog,
|
||||||
Avatar,
|
Avatar,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
Badge
|
Badge,
|
||||||
} from "@radix-ui/themes";
|
} from "@radix-ui/themes";
|
||||||
import {
|
import {
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
@ -22,7 +22,7 @@ import {
|
|||||||
PersonIcon,
|
PersonIcon,
|
||||||
DotsHorizontalIcon,
|
DotsHorizontalIcon,
|
||||||
LockClosedIcon,
|
LockClosedIcon,
|
||||||
ExclamationTriangleIcon
|
ExclamationTriangleIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { User } from "interface/fields";
|
import type { User } from "interface/fields";
|
||||||
@ -61,30 +61,32 @@ const mockUsers: (User & { id: number })[] = [
|
|||||||
updatedAt: new Date("2024-03-13"),
|
updatedAt: new Date("2024-03-13"),
|
||||||
lastLoginAt: new Date("2024-03-13"),
|
lastLoginAt: new Date("2024-03-13"),
|
||||||
passwordHash: "",
|
passwordHash: "",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default new Template({}, ({ http, args }) => {
|
export default new Template({}, ({ http, args }) => {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||||
const [selectedUser, setSelectedUser] = useState<typeof mockUsers[0] | null>(null);
|
const [selectedUser, setSelectedUser] = useState<
|
||||||
|
(typeof mockUsers)[0] | null
|
||||||
|
>(null);
|
||||||
const [newUserData, setNewUserData] = useState({
|
const [newUserData, setNewUserData] = useState({
|
||||||
username: "",
|
username: "",
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
role: "user"
|
role: "user",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取角色标签样式
|
// 获取角色标签样式
|
||||||
const getRoleBadgeColor = (role: string) => {
|
const getRoleBadgeColor = (role: string) => {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case 'admin':
|
case "admin":
|
||||||
return 'red';
|
return "red";
|
||||||
case 'editor':
|
case "editor":
|
||||||
return 'blue';
|
return "blue";
|
||||||
default:
|
default:
|
||||||
return 'gray';
|
return "gray";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,9 +98,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<Heading size="6" className="text-[--gray-12] mb-2">
|
<Heading size="6" className="text-[--gray-12] mb-2">
|
||||||
用户管理
|
用户管理
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text className="text-[--gray-11]">
|
<Text className="text-[--gray-11]">共 {mockUsers.length} 个用户</Text>
|
||||||
共 {mockUsers.length} 个用户
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Button
|
<Button
|
||||||
className="bg-[--accent-9]"
|
className="bg-[--accent-9]"
|
||||||
@ -114,7 +114,9 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<TextField.Root
|
<TextField.Root
|
||||||
placeholder="搜索用户..."
|
placeholder="搜索用户..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setSearchTerm(e.target.value)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<TextField.Slot>
|
<TextField.Slot>
|
||||||
<MagnifyingGlassIcon height="16" width="16" />
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
@ -157,9 +159,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
{user.role}
|
{user.role}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>{user.createdAt.toLocaleDateString()}</Table.Cell>
|
||||||
{user.createdAt.toLocaleDateString()}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
{user.lastLoginAt?.toLocaleDateString()}
|
{user.lastLoginAt?.toLocaleDateString()}
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
@ -243,7 +243,9 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<select
|
<select
|
||||||
className="w-full h-9 px-3 rounded-md bg-[--gray-1] border border-[--gray-6] text-[--gray-12]"
|
className="w-full h-9 px-3 rounded-md bg-[--gray-1] border border-[--gray-6] text-[--gray-12]"
|
||||||
value={newUserData.role}
|
value={newUserData.role}
|
||||||
onChange={(e) => setNewUserData({...newUserData, role: e.target.value})}
|
onChange={(e) =>
|
||||||
|
setNewUserData({ ...newUserData, role: e.target.value })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<option value="user">普通用户</option>
|
<option value="user">普通用户</option>
|
||||||
<option value="editor">编辑</option>
|
<option value="editor">编辑</option>
|
||||||
@ -259,9 +261,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
<Dialog.Close>
|
<Dialog.Close>
|
||||||
<Button className="bg-[--accent-9]">
|
<Button className="bg-[--accent-9]">创建</Button>
|
||||||
创建
|
|
||||||
</Button>
|
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
@ -279,25 +279,36 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
|
|
||||||
<Flex direction="column" gap="4">
|
<Flex direction="column" gap="4">
|
||||||
<Box>
|
<Box>
|
||||||
<Text as="label" size="2" weight="bold" className="block mb-2">
|
<Text
|
||||||
|
as="label"
|
||||||
|
size="2"
|
||||||
|
weight="bold"
|
||||||
|
className="block mb-2"
|
||||||
|
>
|
||||||
用户名
|
用户名
|
||||||
</Text>
|
</Text>
|
||||||
<TextField.Root
|
<TextField.Root defaultValue={selectedUser.username} />
|
||||||
defaultValue={selectedUser.username}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text as="label" size="2" weight="bold" className="block mb-2">
|
<Text
|
||||||
|
as="label"
|
||||||
|
size="2"
|
||||||
|
weight="bold"
|
||||||
|
className="block mb-2"
|
||||||
|
>
|
||||||
邮箱
|
邮箱
|
||||||
</Text>
|
</Text>
|
||||||
<TextField.Root
|
<TextField.Root defaultValue={selectedUser.email} />
|
||||||
defaultValue={selectedUser.email}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text as="label" size="2" weight="bold" className="block mb-2">
|
<Text
|
||||||
|
as="label"
|
||||||
|
size="2"
|
||||||
|
weight="bold"
|
||||||
|
className="block mb-2"
|
||||||
|
>
|
||||||
角色
|
角色
|
||||||
</Text>
|
</Text>
|
||||||
<select
|
<select
|
||||||
@ -311,13 +322,15 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text as="label" size="2" weight="bold" className="block mb-2">
|
<Text
|
||||||
|
as="label"
|
||||||
|
size="2"
|
||||||
|
weight="bold"
|
||||||
|
className="block mb-2"
|
||||||
|
>
|
||||||
重置密码
|
重置密码
|
||||||
</Text>
|
</Text>
|
||||||
<TextField.Root
|
<TextField.Root type="password" placeholder="留空则不修改" />
|
||||||
type="password"
|
|
||||||
placeholder="留空则不修改"
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
@ -328,9 +341,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
<Dialog.Close>
|
<Dialog.Close>
|
||||||
<Button className="bg-[--accent-9]">
|
<Button className="bg-[--accent-9]">保存</Button>
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
|
@ -9,8 +9,5 @@ import { startTransition, StrictMode } from "react";
|
|||||||
import { hydrateRoot } from "react-dom/client";
|
import { hydrateRoot } from "react-dom/client";
|
||||||
|
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
hydrateRoot(
|
hydrateRoot(document, <RemixBrowser />);
|
||||||
document,
|
|
||||||
<RemixBrowser />,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import GrowingTree from "../hooks/tide";
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return (
|
|
||||||
<div style={{ position: "relative", minHeight: "100vh" }}>
|
|
||||||
<GrowingTree />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -13,33 +13,10 @@ import "~/index.css";
|
|||||||
|
|
||||||
export function Layout() {
|
export function Layout() {
|
||||||
return (
|
return (
|
||||||
<html
|
<html lang="en" className="h-full" suppressHydrationWarning={true}>
|
||||||
lang="en"
|
|
||||||
className="h-full"
|
|
||||||
suppressHydrationWarning={true}
|
|
||||||
>
|
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta
|
<meta httpEquiv="Expires" content="0" />
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="generator"
|
|
||||||
content="echoes"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
httpEquiv="Cache-Control"
|
|
||||||
content="no-cache, no-store, must-revalidate"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
httpEquiv="Pragma"
|
|
||||||
content="no-cache"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
httpEquiv="Expires"
|
|
||||||
content="0"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<title>Echoes</title>
|
<title>Echoes</title>
|
||||||
<ThemeScript />
|
<ThemeScript />
|
||||||
@ -51,11 +28,7 @@ export function Layout() {
|
|||||||
suppressHydrationWarning={true}
|
suppressHydrationWarning={true}
|
||||||
data-cz-shortcut-listen="false"
|
data-cz-shortcut-listen="false"
|
||||||
>
|
>
|
||||||
<Theme
|
<Theme grayColor="slate" radius="medium" scaling="100%">
|
||||||
grayColor="slate"
|
|
||||||
radius="medium"
|
|
||||||
scaling="100%"
|
|
||||||
>
|
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
@ -70,4 +43,3 @@ export function Layout() {
|
|||||||
export default function App() {
|
export default function App() {
|
||||||
return <Layout />;
|
return <Layout />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { DEFAULT_CONFIG } from "~/env";
|
||||||
export interface ErrorResponse {
|
export interface ErrorResponse {
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
@ -73,18 +74,17 @@ export class HttpClient {
|
|||||||
private async request<T>(
|
private async request<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
options: RequestInit = {},
|
options: RequestInit = {},
|
||||||
prefix = "api",
|
url: string,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const config = await this.setHeaders(options);
|
const config = await this.setHeaders(options);
|
||||||
const url = endpoint.startsWith(`/__/${prefix}`)
|
const baseUrl = url.startsWith("http") ? url : `http://${url}`;
|
||||||
? endpoint
|
const newUrl = new URL(endpoint, baseUrl);
|
||||||
: `/__/${prefix}${endpoint.startsWith("/") ? endpoint : `/${endpoint}`}`;
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(newUrl, {
|
||||||
...config,
|
...config,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
@ -116,11 +116,16 @@ export class HttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async api<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
public async api<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||||
return this.request<T>(endpoint, options, "api");
|
const url =
|
||||||
|
import.meta.env.VITE_API_BASE_URL ?? DEFAULT_CONFIG.VITE_API_BASE_URL;
|
||||||
|
return this.request<T>(endpoint, options, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async dev<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
public async dev<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||||
return this.request<T>(endpoint, options, "express");
|
const address = import.meta.env.VITE_ADDRESS ?? DEFAULT_CONFIG.VITE_ADDRESS;
|
||||||
|
const port =
|
||||||
|
Number(import.meta.env.VITE_PORT ?? DEFAULT_CONFIG.VITE_PORT) + 1;
|
||||||
|
return this.request<T>(endpoint, options, `${address}:${port}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
public async get<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { useEffect, useRef, memo } from 'react';
|
import { useEffect, useRef, memo } from "react";
|
||||||
import { useThemeMode } from 'hooks/ThemeMode';
|
import { useThemeMode } from "hooks/ThemeMode";
|
||||||
|
|
||||||
interface AnimatedBackgroundProps {
|
interface AnimatedBackgroundProps {
|
||||||
onError?: () => void;
|
onError?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) => {
|
export const AnimatedBackground = memo(
|
||||||
|
({ onError }: AnimatedBackgroundProps) => {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const { mode } = useThemeMode();
|
const { mode } = useThemeMode();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
onError?.();
|
onError?.();
|
||||||
@ -21,13 +21,13 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) =>
|
|||||||
canvas.height = window.innerHeight;
|
canvas.height = window.innerHeight;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ctx = canvas.getContext('2d', {
|
const ctx = canvas.getContext("2d", {
|
||||||
alpha: true,
|
alpha: true,
|
||||||
desynchronized: true
|
desynchronized: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
console.error('无法获取 canvas context');
|
console.error("无法获取 canvas context");
|
||||||
onError?.();
|
onError?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -37,7 +37,8 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) =>
|
|||||||
const getRandomHSLColor = () => {
|
const getRandomHSLColor = () => {
|
||||||
const hue = Math.random() * 360;
|
const hue = Math.random() * 360;
|
||||||
const saturation = 90 + Math.random() * 10;
|
const saturation = 90 + Math.random() * 10;
|
||||||
const lightness = mode === 'dark'
|
const lightness =
|
||||||
|
mode === "dark"
|
||||||
? 50 + Math.random() * 15 // 暗色模式:50-65%
|
? 50 + Math.random() * 15 // 暗色模式:50-65%
|
||||||
: 60 + Math.random() * 15; // 亮色模式:60-75%
|
: 60 + Math.random() * 15; // 亮色模式:60-75%
|
||||||
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||||
@ -95,7 +96,7 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Canvas 初始化失败:', error);
|
console.error("Canvas 初始化失败:", error);
|
||||||
onError?.();
|
onError?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -107,13 +108,14 @@ export const AnimatedBackground = memo(({ onError }: AnimatedBackgroundProps) =>
|
|||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
className="w-full h-full opacity-70"
|
className="w-full h-full opacity-70"
|
||||||
style={{
|
style={{
|
||||||
filter: 'blur(100px)',
|
filter: "blur(100px)",
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
willChange: 'transform'
|
willChange: "transform",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -7,18 +7,18 @@ export const Echoes: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 优化动画性能
|
// 优化动画性能
|
||||||
if (svgRef.current) {
|
if (svgRef.current) {
|
||||||
svgRef.current.style.willChange = 'transform';
|
svgRef.current.style.willChange = "transform";
|
||||||
|
|
||||||
// 使用 requestAnimationFrame 来优化动画
|
// 使用 requestAnimationFrame 来优化动画
|
||||||
const paths = svgRef.current.querySelectorAll('path');
|
const paths = svgRef.current.querySelectorAll("path");
|
||||||
paths.forEach(path => {
|
paths.forEach((path) => {
|
||||||
path.style.willChange = 'transform';
|
path.style.willChange = "transform";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (svgRef.current) {
|
if (svgRef.current) {
|
||||||
svgRef.current.style.willChange = 'auto';
|
svgRef.current.style.willChange = "auto";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@ -31,8 +31,8 @@ export const Echoes: React.FC = () => {
|
|||||||
viewBox="50.4 44.600006 234.1 86"
|
viewBox="50.4 44.600006 234.1 86"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
style={{
|
style={{
|
||||||
transform: 'translateZ(0)', // 启用硬件加速
|
transform: "translateZ(0)", // 启用硬件加速
|
||||||
backfaceVisibility: 'hidden'
|
backfaceVisibility: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
|
@ -37,7 +37,7 @@ export const ThemeScript = () => {
|
|||||||
// 客户端专用的 hook
|
// 客户端专用的 hook
|
||||||
const useClientOnly = (callback: () => void, deps: any[] = []) => {
|
const useClientOnly = (callback: () => void, deps: any[] = []) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
}, deps);
|
}, deps);
|
||||||
@ -46,15 +46,15 @@ const useClientOnly = (callback: () => void, deps: any[] = []) => {
|
|||||||
export const ThemeModeToggle: React.FC = () => {
|
export const ThemeModeToggle: React.FC = () => {
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const [theme, setTheme] = useState<string>(() => {
|
const [theme, setTheme] = useState<string>(() => {
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== "undefined") {
|
||||||
return document.documentElement.dataset.theme || 'light';
|
return document.documentElement.dataset.theme || "light";
|
||||||
}
|
}
|
||||||
return 'light';
|
return "light";
|
||||||
});
|
});
|
||||||
|
|
||||||
useClientOnly(() => {
|
useClientOnly(() => {
|
||||||
const currentTheme = document.documentElement.dataset.theme || 'light';
|
const currentTheme = document.documentElement.dataset.theme || "light";
|
||||||
document.documentElement.classList.remove('light', 'dark');
|
document.documentElement.classList.remove("light", "dark");
|
||||||
document.documentElement.classList.add(currentTheme);
|
document.documentElement.classList.add(currentTheme);
|
||||||
setTheme(currentTheme);
|
setTheme(currentTheme);
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
@ -63,28 +63,29 @@ export const ThemeModeToggle: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
|
||||||
const handleSystemThemeChange = (e: MediaQueryListEvent) => {
|
const handleSystemThemeChange = (e: MediaQueryListEvent) => {
|
||||||
if (!localStorage.getItem(THEME_KEY)) {
|
if (!localStorage.getItem(THEME_KEY)) {
|
||||||
const newTheme = e.matches ? 'dark' : 'light';
|
const newTheme = e.matches ? "dark" : "light";
|
||||||
updateTheme(newTheme);
|
updateTheme(newTheme);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mediaQuery.addEventListener('change', handleSystemThemeChange);
|
mediaQuery.addEventListener("change", handleSystemThemeChange);
|
||||||
return () => mediaQuery.removeEventListener('change', handleSystemThemeChange);
|
return () =>
|
||||||
|
mediaQuery.removeEventListener("change", handleSystemThemeChange);
|
||||||
}, [mounted]);
|
}, [mounted]);
|
||||||
|
|
||||||
const updateTheme = (newTheme: string) => {
|
const updateTheme = (newTheme: string) => {
|
||||||
document.documentElement.dataset.theme = newTheme;
|
document.documentElement.dataset.theme = newTheme;
|
||||||
document.documentElement.classList.remove('light', 'dark');
|
document.documentElement.classList.remove("light", "dark");
|
||||||
document.documentElement.classList.add(newTheme);
|
document.documentElement.classList.add(newTheme);
|
||||||
setTheme(newTheme);
|
setTheme(newTheme);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
const newTheme = theme === 'dark' ? 'light' : 'dark';
|
const newTheme = theme === "dark" ? "light" : "dark";
|
||||||
updateTheme(newTheme);
|
updateTheme(newTheme);
|
||||||
localStorage.setItem(THEME_KEY, newTheme);
|
localStorage.setItem(THEME_KEY, newTheme);
|
||||||
};
|
};
|
||||||
@ -108,7 +109,7 @@ export const ThemeModeToggle: React.FC = () => {
|
|||||||
className="w-full h-full p-0 rounded-lg transition-all duration-300 transform"
|
className="w-full h-full p-0 rounded-lg transition-all duration-300 transform"
|
||||||
aria-label="Toggle theme"
|
aria-label="Toggle theme"
|
||||||
>
|
>
|
||||||
{theme === 'dark' ? (
|
{theme === "dark" ? (
|
||||||
<SunIcon className="w-full h-full" />
|
<SunIcon className="w-full h-full" />
|
||||||
) : (
|
) : (
|
||||||
<MoonIcon className="w-full h-full" />
|
<MoonIcon className="w-full h-full" />
|
||||||
@ -123,7 +124,9 @@ export const useThemeMode = () => {
|
|||||||
|
|
||||||
useClientOnly(() => {
|
useClientOnly(() => {
|
||||||
const handleThemeChange = () => {
|
const handleThemeChange = () => {
|
||||||
const currentTheme = document.documentElement.dataset.theme as "light" | "dark";
|
const currentTheme = document.documentElement.dataset.theme as
|
||||||
|
| "light"
|
||||||
|
| "dark";
|
||||||
setMode(currentTheme || "light");
|
setMode(currentTheme || "light");
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -132,7 +135,7 @@ export const useThemeMode = () => {
|
|||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
const observer = new MutationObserver((mutations) => {
|
||||||
mutations.forEach((mutation) => {
|
mutations.forEach((mutation) => {
|
||||||
if (mutation.attributeName === 'data-theme') {
|
if (mutation.attributeName === "data-theme") {
|
||||||
handleThemeChange();
|
handleThemeChange();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -140,7 +143,7 @@ export const useThemeMode = () => {
|
|||||||
|
|
||||||
observer.observe(document.documentElement, {
|
observer.observe(document.documentElement, {
|
||||||
attributes: true,
|
attributes: true,
|
||||||
attributeFilter: ['data-theme']
|
attributeFilter: ["data-theme"],
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
|
@ -52,7 +52,7 @@ export interface Setting {
|
|||||||
|
|
||||||
export interface Metadata {
|
export interface Metadata {
|
||||||
id: number;
|
id: number;
|
||||||
targetType: 'post' | 'page';
|
targetType: "post" | "page";
|
||||||
targetId: number;
|
targetId: number;
|
||||||
metaKey: string;
|
metaKey: string;
|
||||||
metaValue?: string;
|
metaValue?: string;
|
||||||
@ -60,7 +60,7 @@ export interface Metadata {
|
|||||||
|
|
||||||
export interface CustomField {
|
export interface CustomField {
|
||||||
id: number;
|
id: number;
|
||||||
targetType: 'post' | 'page';
|
targetType: "post" | "page";
|
||||||
targetId: number;
|
targetId: number;
|
||||||
fieldKey: string;
|
fieldKey: string;
|
||||||
fieldValue?: string;
|
fieldValue?: string;
|
||||||
@ -70,7 +70,7 @@ export interface CustomField {
|
|||||||
export interface Taxonomy {
|
export interface Taxonomy {
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
type: 'tag' | 'category';
|
type: "tag" | "category";
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import { HttpClient } from "core/http";
|
import { HttpClient } from "core/http";
|
||||||
import { CapabilityService } from "core/capability";
|
import { CapabilityService } from "core/capability";
|
||||||
import { Serializable } from "interface/serializableType";
|
import { Serializable } from "interface/serializableType";
|
||||||
import { createElement, memo } from 'react';
|
import { createElement, memo } from "react";
|
||||||
|
|
||||||
export class Layout {
|
export class Layout {
|
||||||
private http: HttpClient;
|
private http: HttpClient;
|
||||||
private capability: CapabilityService;
|
private capability: CapabilityService;
|
||||||
private readonly MemoizedElement: React.MemoExoticComponent<(props: {
|
private readonly MemoizedElement: React.MemoExoticComponent<
|
||||||
|
(props: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
args?: Serializable;
|
args?: Serializable;
|
||||||
onTouchStart?: (e: TouchEvent) => void;
|
onTouchStart?: (e: TouchEvent) => void;
|
||||||
onTouchEnd?: (e: TouchEvent) => void;
|
onTouchEnd?: (e: TouchEvent) => void;
|
||||||
}) => React.ReactNode>;
|
}) => React.ReactNode
|
||||||
|
>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public element: (props: {
|
public element: (props: {
|
||||||
@ -39,7 +41,7 @@ export class Layout {
|
|||||||
return createElement(this.MemoizedElement, {
|
return createElement(this.MemoizedElement, {
|
||||||
...props,
|
...props,
|
||||||
onTouchStart: props.onTouchStart,
|
onTouchStart: props.onTouchStart,
|
||||||
onTouchEnd: props.onTouchEnd
|
onTouchEnd: props.onTouchEnd,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,10 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "remix vite:build",
|
"build": "remix vite:build",
|
||||||
"dev": "concurrently \"node --trace-warnings ./node_modules/vite/bin/vite.js\" \"cross-env VITE_ADDRESS=localhost VITE_PORT=22100 tsx --trace-warnings server/express.ts\"",
|
"dev": "tsx start.ts",
|
||||||
"format": "prettier --write \"./**/*.{ts,tsx,js,jsx}\"",
|
"format": "prettier --write \"./**/*.{ts,tsx,js,jsx}\"",
|
||||||
"lint": "eslint \"./**/*.{ts,tsx,js,jsx}\" --fix",
|
"lint": "eslint \"./**/*.{ts,tsx,js,jsx}\" --fix",
|
||||||
"start": "remix-serve ./build/server/index.js",
|
"start": "cross-env NODE_ENV=production tsx start.ts",
|
||||||
"typecheck": "tsc"
|
"typecheck": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,40 +1,49 @@
|
|||||||
import { spawn } from "child_process";
|
import { spawn } from "child_process";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
|
import { DEFAULT_CONFIG } from "~/env";
|
||||||
|
|
||||||
// 设置全局最大监听器数量
|
const isDev = process.env.NODE_ENV !== "production";
|
||||||
// EventEmitter.defaultMaxListeners = 20;
|
|
||||||
|
|
||||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
const startServers = async () => {
|
const startServers = async () => {
|
||||||
// 先启动内部服务器
|
// 启动 Express 服务器(无论是开发还是生产环境都需要)
|
||||||
const internalServer = spawn("tsx", ["backend/internalServer.ts"], {
|
const expressServer = spawn(
|
||||||
|
"tsx",
|
||||||
|
["--trace-warnings", "server/express.ts"],
|
||||||
|
{
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
shell: true,
|
shell: true,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
NODE_ENV: process.env.NODE_ENV || "development",
|
PORT: DEFAULT_CONFIG.VITE_PORT + 1,
|
||||||
|
IS_API_SERVER: "true",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expressServer.on("error", (err) => {
|
||||||
|
console.error("Express 服务器启动错误:", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
internalServer.on("error", (err) => {
|
if (isDev) {
|
||||||
console.error("内部服务器启动错误:", err);
|
// 开发环境启动
|
||||||
});
|
console.log("正在以开发模式启动服务器...");
|
||||||
|
|
||||||
// 等待内部服务器启动
|
// 启动 Vite
|
||||||
// console.log("等待内部服务器启动...");
|
const viteProcess = spawn(
|
||||||
// await delay(2000);
|
"node",
|
||||||
|
["--trace-warnings", "./node_modules/vite/bin/vite.js"],
|
||||||
// 然后启动 Vite
|
{
|
||||||
const viteProcess = spawn("npm", ["run", "dev"], {
|
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
shell: true,
|
shell: true,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
NODE_ENV: process.env.NODE_ENV || "development",
|
NODE_ENV: "development",
|
||||||
|
VITE_PORT: DEFAULT_CONFIG.VITE_PORT,
|
||||||
},
|
},
|
||||||
});
|
cwd: process.cwd(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
viteProcess.on("error", (err) => {
|
viteProcess.on("error", (err) => {
|
||||||
console.error("Vite 进程启动错误:", err);
|
console.error("Vite 进程启动错误:", err);
|
||||||
@ -43,12 +52,62 @@ const startServers = async () => {
|
|||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
console.log("正在关闭服务器...");
|
console.log("正在关闭服务器...");
|
||||||
viteProcess.kill();
|
viteProcess.kill();
|
||||||
internalServer.kill();
|
expressServer.kill();
|
||||||
process.exit();
|
process.exit();
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on("SIGINT", cleanup);
|
process.on("SIGINT", cleanup);
|
||||||
process.on("SIGTERM", cleanup);
|
process.on("SIGTERM", cleanup);
|
||||||
|
} else {
|
||||||
|
// 生产环境启动
|
||||||
|
console.log("正在以生产模式启动服务器...");
|
||||||
|
|
||||||
|
// 先执行构建
|
||||||
|
const buildProcess = spawn("npm", ["run", "build"], {
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
buildProcess.on("error", (err) => {
|
||||||
|
console.error("构建错误:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
buildProcess.on("close", (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
console.error("构建失败");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("构建完成,正在启动生产服务器...");
|
||||||
|
|
||||||
|
// 使用 remix-serve 启动生产服务器
|
||||||
|
const prodServer = spawn("remix-serve", ["./build/server/index.js"], {
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: true,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
NODE_ENV: "production",
|
||||||
|
PORT: DEFAULT_CONFIG.VITE_PORT,
|
||||||
|
},
|
||||||
|
cwd: process.cwd(),
|
||||||
|
});
|
||||||
|
|
||||||
|
prodServer.on("error", (err) => {
|
||||||
|
console.error("生产服务器启动错误:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
console.log("正在关闭生产服务器...");
|
||||||
|
prodServer.kill();
|
||||||
|
expressServer.kill();
|
||||||
|
process.exit();
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on("SIGINT", cleanup);
|
||||||
|
process.on("SIGTERM", cleanup);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
startServers().catch((err) => {
|
startServers().catch((err) => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
import typography from '@tailwindcss/typography';
|
import typography from "@tailwindcss/typography";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
@ -9,7 +9,7 @@ export default {
|
|||||||
"./hooks/**/*.{js,jsx,ts,tsx}",
|
"./hooks/**/*.{js,jsx,ts,tsx}",
|
||||||
"./themes/**/*.{js,jsx,ts,tsx}",
|
"./themes/**/*.{js,jsx,ts,tsx}",
|
||||||
],
|
],
|
||||||
darkMode: 'class',
|
darkMode: "class",
|
||||||
important: true,
|
important: true,
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
@ -30,7 +30,5 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [typography],
|
||||||
typography,
|
|
||||||
],
|
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
import { Layout } from "interface/layout";
|
import { Layout } from "interface/layout";
|
||||||
import { ThemeModeToggle } from "hooks/ThemeMode";
|
import { ThemeModeToggle } from "hooks/ThemeMode";
|
||||||
import { Echoes } from "hooks/Echoes";
|
import { Echoes } from "hooks/Echoes";
|
||||||
import { Container, Flex, Box, Link, TextField, Button } from "@radix-ui/themes";
|
import {
|
||||||
|
Container,
|
||||||
|
Flex,
|
||||||
|
Box,
|
||||||
|
Link,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
} from "@radix-ui/themes";
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
HamburgerMenuIcon,
|
HamburgerMenuIcon,
|
||||||
@ -14,12 +21,12 @@ import { useState, useEffect, useCallback } from "react";
|
|||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
import throttle from "lodash/throttle";
|
import throttle from "lodash/throttle";
|
||||||
import "./styles/layouts.css";
|
import "./styles/layouts.css";
|
||||||
import parse from 'html-react-parser';
|
import parse from "html-react-parser";
|
||||||
|
|
||||||
// 直接导出 Layout 实例
|
// 直接导出 Layout 实例
|
||||||
export default new Layout(({ children, args }) => {
|
export default new Layout(({ children, args }) => {
|
||||||
const [moreState, setMoreState] = useState(() => {
|
const [moreState, setMoreState] = useState(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
return window.innerWidth >= 1024 ? false : false;
|
return window.innerWidth >= 1024 ? false : false;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -28,7 +35,7 @@ export default new Layout(({ children, args }) => {
|
|||||||
const [scrollProgress, setScrollProgress] = useState(0);
|
const [scrollProgress, setScrollProgress] = useState(0);
|
||||||
|
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
const container = document.querySelector('#main-content');
|
const container = document.querySelector("#main-content");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const scrollTop = container.scrollTop;
|
const scrollTop = container.scrollTop;
|
||||||
@ -39,11 +46,11 @@ export default new Layout(({ children, args }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
const container = document.querySelector('#main-content');
|
const container = document.querySelector("#main-content");
|
||||||
if (container) {
|
if (container) {
|
||||||
container.addEventListener('scroll', handleScroll);
|
container.addEventListener("scroll", handleScroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
const throttledResize = throttle(() => {
|
const throttledResize = throttle(() => {
|
||||||
@ -58,22 +65,25 @@ export default new Layout(({ children, args }) => {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (container) {
|
if (container) {
|
||||||
container.removeEventListener('scroll', handleScroll);
|
container.removeEventListener("scroll", handleScroll);
|
||||||
}
|
}
|
||||||
window.removeEventListener("resize", throttledResize);
|
window.removeEventListener("resize", throttledResize);
|
||||||
throttledResize.cancel();
|
throttledResize.cancel();
|
||||||
};
|
};
|
||||||
}, [handleScroll]);
|
}, [handleScroll]);
|
||||||
|
|
||||||
const navString = typeof args === 'object' && args && 'nav' in args ? args.nav as string : '';
|
const navString =
|
||||||
|
typeof args === "object" && args && "nav" in args
|
||||||
|
? (args.nav as string)
|
||||||
|
: "";
|
||||||
|
|
||||||
// 添加回到顶部的处理函数
|
// 添加回到顶部的处理函数
|
||||||
const scrollToTop = () => {
|
const scrollToTop = () => {
|
||||||
const container = document.querySelector('#main-content');
|
const container = document.querySelector("#main-content");
|
||||||
if (container) {
|
if (container) {
|
||||||
container.scrollTo({
|
container.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: 'smooth'
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -84,9 +94,7 @@ export default new Layout(({ children, args }) => {
|
|||||||
{/* 添加移动端进度指示器 */}
|
{/* 添加移动端进度指示器 */}
|
||||||
<Box
|
<Box
|
||||||
className={`w-10 h-10 flex items-center justify-center ${
|
className={`w-10 h-10 flex items-center justify-center ${
|
||||||
scrollProgress > 0
|
scrollProgress > 0 ? "block" : "hidden"
|
||||||
? 'block'
|
|
||||||
: 'hidden'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@ -94,10 +102,7 @@ export default new Layout(({ children, args }) => {
|
|||||||
className="w-10 h-10 p-0 text-[--gray-12] hover:text-[--accent-9] transition-colors flex items-center justify-center [&_text]:text-[--gray-12] [&_text:hover]:text-[--accent-9]"
|
className="w-10 h-10 p-0 text-[--gray-12] hover:text-[--accent-9] transition-colors flex items-center justify-center [&_text]:text-[--gray-12] [&_text:hover]:text-[--accent-9]"
|
||||||
onClick={scrollToTop}
|
onClick={scrollToTop}
|
||||||
>
|
>
|
||||||
<svg
|
<svg className="w-6 h-6" viewBox="0 0 100 100">
|
||||||
className="w-6 h-6"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
>
|
|
||||||
<text
|
<text
|
||||||
x="50"
|
x="50"
|
||||||
y="55"
|
y="55"
|
||||||
@ -105,8 +110,8 @@ export default new Layout(({ children, args }) => {
|
|||||||
dominantBaseline="middle"
|
dominantBaseline="middle"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
style={{
|
style={{
|
||||||
fontSize: '56px',
|
fontSize: "56px",
|
||||||
fill: 'currentColor'
|
fill: "currentColor",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Math.round(scrollProgress)}
|
{Math.round(scrollProgress)}
|
||||||
@ -149,10 +154,7 @@ export default new Layout(({ children, args }) => {
|
|||||||
placeholder="搜索..."
|
placeholder="搜索..."
|
||||||
className="w-full [&_input]:pl-3 hover:border-[--accent-9] border transition-colors group"
|
className="w-full [&_input]:pl-3 hover:border-[--accent-9] border transition-colors group"
|
||||||
>
|
>
|
||||||
<TextField.Slot
|
<TextField.Slot side="right" className="p-2">
|
||||||
side="right"
|
|
||||||
className="p-2"
|
|
||||||
>
|
|
||||||
<MagnifyingGlassIcon className="h-4 w-4 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
|
<MagnifyingGlassIcon className="h-4 w-4 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
@ -200,10 +202,7 @@ export default new Layout(({ children, args }) => {
|
|||||||
radius="large"
|
radius="large"
|
||||||
panelBackground="solid"
|
panelBackground="solid"
|
||||||
>
|
>
|
||||||
<Box
|
<Box className="h-screen flex flex-col overflow-hidden" id="nav">
|
||||||
className="h-screen flex flex-col overflow-hidden"
|
|
||||||
id="nav"
|
|
||||||
>
|
|
||||||
{/* 导航栏 */}
|
{/* 导航栏 */}
|
||||||
<Box
|
<Box
|
||||||
asChild
|
asChild
|
||||||
@ -211,17 +210,10 @@ export default new Layout(({ children, args }) => {
|
|||||||
>
|
>
|
||||||
<nav>
|
<nav>
|
||||||
<Container size="4">
|
<Container size="4">
|
||||||
<Flex
|
<Flex justify="between" align="center" className="h-20 px-4">
|
||||||
justify="between"
|
|
||||||
align="center"
|
|
||||||
className="h-20 px-4"
|
|
||||||
>
|
|
||||||
{/* Logo 区域 */}
|
{/* Logo 区域 */}
|
||||||
<Flex align="center">
|
<Flex align="center">
|
||||||
<Link
|
<Link href="/" className="hover-text flex items-center">
|
||||||
href="/"
|
|
||||||
className="hover-text flex items-center"
|
|
||||||
>
|
|
||||||
<Box className="w-20 h-20 [&_path]:transition-all [&_path]:duration-200 group-hover:[&_path]:stroke-[--accent-9]">
|
<Box className="w-20 h-20 [&_path]:transition-all [&_path]:duration-200 group-hover:[&_path]:stroke-[--accent-9]">
|
||||||
<Echoes />
|
<Echoes />
|
||||||
</Box>
|
</Box>
|
||||||
@ -306,9 +298,7 @@ export default new Layout(({ children, args }) => {
|
|||||||
{/* 读进度指示器 */}
|
{/* 读进度指示器 */}
|
||||||
<Box
|
<Box
|
||||||
className={`w-10 h-10 flex items-center justify-center ${
|
className={`w-10 h-10 flex items-center justify-center ${
|
||||||
scrollProgress > 0
|
scrollProgress > 0 ? "block" : "hidden"
|
||||||
? 'block'
|
|
||||||
: 'hidden'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@ -316,10 +306,7 @@ export default new Layout(({ children, args }) => {
|
|||||||
className="w-10 h-10 p-0 text-[--gray-12] hover:text-[--accent-9] transition-colors flex items-center justify-center [&_text]:text-[--gray-12] [&_text:hover]:text-[--accent-9]"
|
className="w-10 h-10 p-0 text-[--gray-12] hover:text-[--accent-9] transition-colors flex items-center justify-center [&_text]:text-[--gray-12] [&_text:hover]:text-[--accent-9]"
|
||||||
onClick={scrollToTop}
|
onClick={scrollToTop}
|
||||||
>
|
>
|
||||||
<svg
|
<svg className="w-6 h-6" viewBox="0 0 100 100">
|
||||||
className="w-6 h-6"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
>
|
|
||||||
<text
|
<text
|
||||||
x="50"
|
x="50"
|
||||||
y="55"
|
y="55"
|
||||||
@ -327,8 +314,8 @@ export default new Layout(({ children, args }) => {
|
|||||||
dominantBaseline="middle"
|
dominantBaseline="middle"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
style={{
|
style={{
|
||||||
fontSize: '56px',
|
fontSize: "56px",
|
||||||
fill: 'currentColor'
|
fill: "currentColor",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Math.round(scrollProgress)}
|
{Math.round(scrollProgress)}
|
||||||
@ -349,14 +336,8 @@ export default new Layout(({ children, args }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 主要内容区域 */}
|
{/* 主要内容区域 */}
|
||||||
<Box
|
<Box id="main-content" className="flex-1 w-full overflow-auto">
|
||||||
id="main-content"
|
<Container size="4" className="py-8">
|
||||||
className="flex-1 w-full overflow-auto"
|
|
||||||
>
|
|
||||||
<Container
|
|
||||||
size="4"
|
|
||||||
className="py-8"
|
|
||||||
>
|
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
</Container>
|
</Container>
|
||||||
</Box>
|
</Box>
|
||||||
@ -364,4 +345,3 @@ export default new Layout(({ children, args }) => {
|
|||||||
</Theme>
|
</Theme>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import React, { useMemo, useState, useCallback, useRef, useEffect } from "react";
|
import React, {
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
} from "react";
|
||||||
import { Template } from "interface/template";
|
import { Template } from "interface/template";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
@ -16,13 +22,13 @@ import { CalendarIcon, CodeIcon } from "@radix-ui/react-icons";
|
|||||||
import type { PostDisplay } from "interface/fields";
|
import type { PostDisplay } from "interface/fields";
|
||||||
import type { MetaFunction } from "@remix-run/node";
|
import type { MetaFunction } from "@remix-run/node";
|
||||||
import { getColorScheme } from "themes/echoes/utils/colorScheme";
|
import { getColorScheme } from "themes/echoes/utils/colorScheme";
|
||||||
import MarkdownIt from 'markdown-it';
|
import MarkdownIt from "markdown-it";
|
||||||
import { ComponentPropsWithoutRef } from 'react';
|
import { ComponentPropsWithoutRef } from "react";
|
||||||
import remarkGfm from 'remark-gfm';
|
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';
|
import ReactDOMServer from "react-dom/server";
|
||||||
|
|
||||||
// 示例文章数据
|
// 示例文章数据
|
||||||
const mockPost: PostDisplay = {
|
const mockPost: PostDisplay = {
|
||||||
@ -373,22 +379,25 @@ function greet(user: User): string {
|
|||||||
`,
|
`,
|
||||||
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",
|
||||||
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"),
|
||||||
taxonomies: {
|
taxonomies: {
|
||||||
categories: [{
|
categories: [
|
||||||
|
{
|
||||||
name: "教程",
|
name: "教程",
|
||||||
slug: "tutorial",
|
slug: "tutorial",
|
||||||
type: "category"
|
type: "category",
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "Markdown", slug: "markdown", type: "tag" },
|
{ name: "Markdown", slug: "markdown", type: "tag" },
|
||||||
{ name: "排版", slug: "typography", type: "tag" },
|
{ name: "排版", slug: "typography", type: "tag" },
|
||||||
{ name: "写作", slug: "writing", type: "tag" }
|
{ name: "写作", slug: "writing", type: "tag" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
metadata: [
|
metadata: [
|
||||||
{
|
{
|
||||||
@ -396,22 +405,25 @@ function greet(user: User): string {
|
|||||||
targetType: "post",
|
targetType: "post",
|
||||||
targetId: 1,
|
targetId: 1,
|
||||||
metaKey: "description",
|
metaKey: "description",
|
||||||
metaValue: "从基础语法到高级排版,全面了解 Markdown 的各种用法和技巧。"
|
metaValue: "从基础语法到高级排版,全面了解 Markdown 的各种用法和技巧。",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
targetType: "post",
|
targetType: "post",
|
||||||
targetId: 1,
|
targetId: 1,
|
||||||
metaKey: "keywords",
|
metaKey: "keywords",
|
||||||
metaValue: "Markdown,基础语法,高级排版,布局设计"
|
metaValue: "Markdown,基础语法,高级排版,布局设计",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添 meta 函数
|
// 添 meta 函数
|
||||||
export const meta: MetaFunction = () => {
|
export const meta: MetaFunction = () => {
|
||||||
const description = mockPost.metadata?.find(m => m.metaKey === "description")?.metaValue || "";
|
const description =
|
||||||
const keywords = mockPost.metadata?.find(m => m.metaKey === "keywords")?.metaValue || "";
|
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 },
|
||||||
@ -430,7 +442,6 @@ export const meta: MetaFunction = () => {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// 添加复制能的接口
|
// 添加复制能的接口
|
||||||
interface CopyButtonProps {
|
interface CopyButtonProps {
|
||||||
code: string;
|
code: string;
|
||||||
@ -449,7 +460,7 @@ const CopyButton: React.FC<CopyButtonProps> = ({ code }) => {
|
|||||||
setCopied(false);
|
setCopied(false);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('复制失败:', err);
|
console.error("复制失败:", err);
|
||||||
toast.error("复制失败", "请检查浏览器权限设置");
|
toast.error("复制失败", "请检查浏览器权限设置");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -483,7 +494,7 @@ const generateSequentialId = (() => {
|
|||||||
return (postId: string, reset = false) => {
|
return (postId: string, reset = false) => {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
idMap.delete(postId);
|
idMap.delete(postId);
|
||||||
return '';
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!idMap.has(postId)) {
|
if (!idMap.has(postId)) {
|
||||||
@ -498,15 +509,15 @@ const generateSequentialId = (() => {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
export default new Template({}, ({ http, args }) => {
|
export default new Template({}, ({ http, args }) => {
|
||||||
|
|
||||||
|
|
||||||
const [toc, setToc] = useState<string[]>([]);
|
const [toc, setToc] = useState<string[]>([]);
|
||||||
const [tocItems, setTocItems] = useState<TocItem[]>([]);
|
const [tocItems, setTocItems] = useState<TocItem[]>([]);
|
||||||
const [activeId, setActiveId] = useState<string>("");
|
const [activeId, setActiveId] = useState<string>("");
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
const [showToc, setShowToc] = useState(false);
|
const [showToc, setShowToc] = useState(false);
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
const [headingIdsArrays, setHeadingIdsArrays] = useState<{[key: string]: string[]}>({});
|
const [headingIdsArrays, setHeadingIdsArrays] = useState<{
|
||||||
|
[key: string]: string[];
|
||||||
|
}>({});
|
||||||
const headingIds = useRef<string[]>([]); // 保持原有的 ref
|
const headingIds = useRef<string[]>([]); // 保持原有的 ref
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const isClickScrolling = useRef(false);
|
const isClickScrolling = useRef(false);
|
||||||
@ -516,7 +527,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
const md = new MarkdownIt();
|
const md = new MarkdownIt();
|
||||||
const tocArray: TocItem[] = [];
|
const tocArray: TocItem[] = [];
|
||||||
@ -541,11 +552,11 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
const content = tokens[idx + 1].content;
|
const content = tokens[idx + 1].content;
|
||||||
const id = generateSequentialId(mockPost.id.toString());
|
const id = generateSequentialId(mockPost.id.toString());
|
||||||
|
|
||||||
token.attrSet('id', id);
|
token.attrSet("id", id);
|
||||||
tocArray.push({
|
tocArray.push({
|
||||||
id,
|
id,
|
||||||
text: content,
|
text: content,
|
||||||
level
|
level,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return md.renderer.renderToken(tokens, idx, md.options);
|
return md.renderer.renderToken(tokens, idx, md.options);
|
||||||
@ -554,12 +565,12 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
md.render(mockPost.content);
|
md.render(mockPost.content);
|
||||||
|
|
||||||
// 只在 ID 数组发生变化时更新
|
// 只在 ID 数组发生变化时更新
|
||||||
const newIds = tocArray.map(item => item.id);
|
const newIds = tocArray.map((item) => item.id);
|
||||||
if (JSON.stringify(headingIds.current) !== JSON.stringify(newIds)) {
|
if (JSON.stringify(headingIds.current) !== JSON.stringify(newIds)) {
|
||||||
headingIds.current = [...newIds];
|
headingIds.current = [...newIds];
|
||||||
setHeadingIdsArrays(prev => ({
|
setHeadingIdsArrays((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[mockPost.id]: [...newIds]
|
[mockPost.id]: [...newIds],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -579,62 +590,94 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
|
|
||||||
const components = useMemo(() => {
|
const components = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
h1: ({ children, ...props }: ComponentPropsWithoutRef<'h1'>) => {
|
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}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</h1>
|
</h1>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
h2: ({ children, ...props }: ComponentPropsWithoutRef<'h2'>) => {
|
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}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
h3: ({ children, ...props }: ComponentPropsWithoutRef<'h3'>) => {
|
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}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</h3>
|
</h3>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
p: ({ node, ...props }: ComponentPropsWithoutRef<'p'> & { node?: any }) => (
|
p: ({
|
||||||
|
node,
|
||||||
|
...props
|
||||||
|
}: ComponentPropsWithoutRef<"p"> & { node?: any }) => (
|
||||||
<p
|
<p
|
||||||
className="text-sm sm:text-base md:text-lg leading-relaxed mb-3 sm:mb-4 text-[--gray-11]"
|
className="text-sm sm:text-base md:text-lg leading-relaxed mb-3 sm:mb-4 text-[--gray-11]"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
ul: ({ children, ...props }: ComponentPropsWithoutRef<'ul'>) => (
|
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, ...props }: ComponentPropsWithoutRef<'ol'>) => (
|
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, ...props }: ComponentPropsWithoutRef<'li'>) => (
|
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, ...props }: ComponentPropsWithoutRef<'blockquote'>) => (
|
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}>
|
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}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
),
|
),
|
||||||
pre: ({ children, ...props }: ComponentPropsWithoutRef<'pre'>) => {
|
pre: ({ children, ...props }: ComponentPropsWithoutRef<"pre">) => {
|
||||||
const childArray = React.Children.toArray(children);
|
const childArray = React.Children.toArray(children);
|
||||||
|
|
||||||
// 检查是否包含代码块
|
// 检查是否包含代码块
|
||||||
const codeElement = childArray.find(
|
const codeElement = childArray.find(
|
||||||
child => React.isValidElement(child) && child.props.className?.includes('language-')
|
(child) =>
|
||||||
|
React.isValidElement(child) &&
|
||||||
|
child.props.className?.includes("language-"),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 如果是代码块,让 code 组件处理
|
// 如果是代码块,让 code 组件处理
|
||||||
@ -643,33 +686,39 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取内容
|
// 获取内容
|
||||||
let content = '';
|
let content = "";
|
||||||
if (typeof children === 'string') {
|
if (typeof children === "string") {
|
||||||
content = children;
|
content = children;
|
||||||
} else if (Array.isArray(children)) {
|
} else if (Array.isArray(children)) {
|
||||||
content = children.map(child => {
|
content = children
|
||||||
if (typeof child === 'string') return child;
|
.map((child) => {
|
||||||
|
if (typeof child === "string") return child;
|
||||||
if (React.isValidElement(child)) {
|
if (React.isValidElement(child)) {
|
||||||
// 使用 renderToString 而不是 renderToStaticMarkup
|
// 使用 renderToString 而不是 renderToStaticMarkup
|
||||||
return ReactDOMServer.renderToString(child as React.ReactElement)
|
return (
|
||||||
|
ReactDOMServer.renderToString(child as React.ReactElement)
|
||||||
// 移除 React 添加的 data 属性
|
// 移除 React 添加的 data 属性
|
||||||
.replace(/\s+data-reactroot=""/g, '')
|
.replace(/\s+data-reactroot=""/g, "")
|
||||||
// 移除已经存在的 HTML 实体编码
|
// 移除已经存在的 HTML 实体编码
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, "&")
|
||||||
.replace(/</g, '<')
|
.replace(/</g, "<")
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, ">")
|
||||||
.replace(/'/g, "'");
|
.replace(/'/g, "'")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return '';
|
return "";
|
||||||
}).join('');
|
})
|
||||||
|
.join("");
|
||||||
} else if (React.isValidElement(children)) {
|
} else if (React.isValidElement(children)) {
|
||||||
content = ReactDOMServer.renderToString(children as React.ReactElement)
|
content = ReactDOMServer.renderToString(
|
||||||
.replace(/\s+data-reactroot=""/g, '')
|
children as React.ReactElement,
|
||||||
|
)
|
||||||
|
.replace(/\s+data-reactroot=""/g, "")
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, "&")
|
||||||
.replace(/</g, '<')
|
.replace(/</g, "<")
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, ">")
|
||||||
.replace(/'/g, "'");
|
.replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -681,17 +730,21 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
text-sm leading-relaxed font-mono"
|
text-sm leading-relaxed font-mono"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{content
|
{content}
|
||||||
}
|
|
||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
code: ({ inline, className, children, ...props }: ComponentPropsWithoutRef<'code'> & {
|
code: ({
|
||||||
inline?: boolean,
|
inline,
|
||||||
className?: string
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ComponentPropsWithoutRef<"code"> & {
|
||||||
|
inline?: boolean;
|
||||||
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const match = /language-(\w+)/.exec(className || '');
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
const code = String(children).replace(/\n$/, '');
|
const code = String(children).replace(/\n$/, "");
|
||||||
|
|
||||||
// 如果是内联代码
|
// 如果是内联代码
|
||||||
if (!className || inline) {
|
if (!className || inline) {
|
||||||
@ -705,14 +758,16 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const language = match ? match[1] : '';
|
const language = match ? match[1] : "";
|
||||||
|
|
||||||
// 特殊处理表格语法
|
// 特殊处理表格语法
|
||||||
const isTable = code.includes('|') && code.includes('\n') && code.includes('---');
|
const isTable =
|
||||||
|
code.includes("|") && code.includes("\n") && code.includes("---");
|
||||||
|
|
||||||
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"
|
||||||
@ -731,8 +786,8 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<pre
|
<pre
|
||||||
className="m-0 p-0 bg-transparent font-mono text-[0.9rem] leading-relaxed overflow-x-auto"
|
className="m-0 p-0 bg-transparent font-mono text-[0.9rem] leading-relaxed overflow-x-auto"
|
||||||
style={{
|
style={{
|
||||||
color: 'inherit',
|
color: "inherit",
|
||||||
whiteSpace: 'pre'
|
whiteSpace: "pre",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{code}
|
{code}
|
||||||
@ -743,15 +798,15 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
language={language || "text"}
|
language={language || "text"}
|
||||||
style={{
|
style={{
|
||||||
...oneLight,
|
...oneLight,
|
||||||
'punctuation': {
|
punctuation: {
|
||||||
color: 'var(--gray-12)'
|
color: "var(--gray-12)",
|
||||||
},
|
},
|
||||||
'operator': {
|
operator: {
|
||||||
color: 'var(--gray-12)'
|
color: "var(--gray-12)",
|
||||||
|
},
|
||||||
|
symbol: {
|
||||||
|
color: "var(--gray-12)",
|
||||||
},
|
},
|
||||||
'symbol': {
|
|
||||||
color: 'var(--gray-12)'
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
customStyle={{
|
customStyle={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
@ -763,8 +818,8 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
codeTagProps={{
|
codeTagProps={{
|
||||||
className: "dark:text-[--gray-12]",
|
className: "dark:text-[--gray-12]",
|
||||||
style: {
|
style: {
|
||||||
color: "inherit"
|
color: "inherit",
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{code}
|
{code}
|
||||||
@ -777,7 +832,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
// 修改表格相关组件的响应式设计
|
// 修改表格相关组件的响应式设计
|
||||||
table: ({ children, ...props }: ComponentPropsWithoutRef<'table'>) => (
|
table: ({ children, ...props }: ComponentPropsWithoutRef<"table">) => (
|
||||||
<div className="w-full my-4 sm:my-6 -mx-4 sm:mx-0 overflow-hidden">
|
<div className="w-full my-4 sm:my-6 -mx-4 sm:mx-0 overflow-hidden">
|
||||||
<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">
|
||||||
@ -791,12 +846,16 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
||||||
th: ({ children, style, ...props }: ComponentPropsWithoutRef<'th'> & { style?: React.CSSProperties }) => {
|
th: ({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: ComponentPropsWithoutRef<"th"> & { style?: React.CSSProperties }) => {
|
||||||
// 获取对齐方式
|
// 获取对齐方式
|
||||||
const getAlignment = () => {
|
const getAlignment = () => {
|
||||||
if (style?.textAlign === 'center') return 'text-center';
|
if (style?.textAlign === "center") return "text-center";
|
||||||
if (style?.textAlign === 'right') return 'text-right';
|
if (style?.textAlign === "right") return "text-right";
|
||||||
return 'text-left';
|
return "text-left";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -814,12 +873,16 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
td: ({ children, style, ...props }: ComponentPropsWithoutRef<'td'> & { style?: React.CSSProperties }) => {
|
td: ({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: ComponentPropsWithoutRef<"td"> & { style?: React.CSSProperties }) => {
|
||||||
// 获取父级 th 的对齐方式
|
// 获取父级 th 的对齐方式
|
||||||
const getAlignment = () => {
|
const getAlignment = () => {
|
||||||
if (style?.textAlign === 'center') return 'text-center';
|
if (style?.textAlign === "center") return "text-center";
|
||||||
if (style?.textAlign === 'right') return 'text-right';
|
if (style?.textAlign === "right") return "text-right";
|
||||||
return 'text-left';
|
return "text-left";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -834,7 +897,10 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
// 修改 details 组件
|
// 修改 details 组件
|
||||||
details: ({ node, ...props }: ComponentPropsWithoutRef<'details'> & { node?: any }) => (
|
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]
|
||||||
@ -845,7 +911,10 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// 修改 summary 组件
|
// 修改 summary 组件
|
||||||
summary: ({ node, ...props }: ComponentPropsWithoutRef<'summary'> & { node?: any }) => (
|
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
|
||||||
@ -859,7 +928,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
|
|
||||||
// 修改滚动监听逻辑
|
// 修改滚动监听逻辑
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
let scrollTimeout: NodeJS.Timeout;
|
let scrollTimeout: NodeJS.Timeout;
|
||||||
|
|
||||||
@ -873,14 +942,16 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
|
|
||||||
// 添加防抖,等待滚动结束后再更新高亮
|
// 添加防抖,等待滚动结束后再更新高亮
|
||||||
scrollTimeout = setTimeout(() => {
|
scrollTimeout = setTimeout(() => {
|
||||||
const visibleEntries = entries.filter(entry => entry.isIntersecting);
|
const visibleEntries = entries.filter(
|
||||||
|
(entry) => entry.isIntersecting,
|
||||||
|
);
|
||||||
|
|
||||||
if (visibleEntries.length > 0) {
|
if (visibleEntries.length > 0) {
|
||||||
const visibleHeadings = visibleEntries
|
const visibleHeadings = visibleEntries
|
||||||
.map(entry => ({
|
.map((entry) => ({
|
||||||
id: entry.target.id,
|
id: entry.target.id,
|
||||||
top: entry.boundingClientRect.top,
|
top: entry.boundingClientRect.top,
|
||||||
y: entry.intersectionRatio
|
y: entry.intersectionRatio,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (Math.abs(a.y - b.y) < 0.1) {
|
if (Math.abs(a.y - b.y) < 0.1) {
|
||||||
@ -891,7 +962,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
|
|
||||||
const mostVisible = visibleHeadings[0];
|
const mostVisible = visibleHeadings[0];
|
||||||
|
|
||||||
setActiveId(currentActiveId => {
|
setActiveId((currentActiveId) => {
|
||||||
if (mostVisible.id !== currentActiveId) {
|
if (mostVisible.id !== currentActiveId) {
|
||||||
return mostVisible.id;
|
return mostVisible.id;
|
||||||
}
|
}
|
||||||
@ -902,9 +973,9 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
root: document.querySelector("#main-content"),
|
root: document.querySelector("#main-content"),
|
||||||
rootMargin: '-10% 0px -70% 0px',
|
rootMargin: "-10% 0px -70% 0px",
|
||||||
threshold: [0, 0.25, 0.5, 0.75, 1]
|
threshold: [0, 0.25, 0.5, 0.75, 1],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
@ -990,7 +1061,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
className="absolute right-0 top-0 bottom-0 w-72 bg-white dark:bg-[--gray-1] shadow-xl
|
className="absolute right-0 top-0 bottom-0 w-72 bg-white dark:bg-[--gray-1] shadow-xl
|
||||||
transform transition-transform duration-300 ease-out
|
transform transition-transform duration-300 ease-out
|
||||||
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()}
|
||||||
>
|
>
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
type="hover"
|
type="hover"
|
||||||
@ -1006,24 +1077,32 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<a
|
<a
|
||||||
key={`${item.id}-${index}`}
|
key={`${item.id}-${index}`}
|
||||||
href={`#${item.id}`}
|
href={`#${item.id}`}
|
||||||
ref={node => {
|
ref={(node) => {
|
||||||
// 当目录打开且是当前高亮项时,将其滚动到居中位置
|
// 当目录打开且是当前高亮项时,将其滚动到居中位置
|
||||||
if (node && isActive && showToc) {
|
if (node && isActive && showToc) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
// 直接查找最近的滚动容器
|
// 直接查找最近的滚动容器
|
||||||
const scrollContainer = node.closest('.rt-ScrollAreaViewport');
|
const scrollContainer = node.closest(
|
||||||
|
".rt-ScrollAreaViewport",
|
||||||
|
);
|
||||||
if (scrollContainer) {
|
if (scrollContainer) {
|
||||||
const containerHeight = scrollContainer.clientHeight;
|
const containerHeight =
|
||||||
|
scrollContainer.clientHeight;
|
||||||
const elementTop = node.offsetTop;
|
const elementTop = node.offsetTop;
|
||||||
const elementHeight = node.clientHeight;
|
const elementHeight = node.clientHeight;
|
||||||
|
|
||||||
// 确保计算的滚动位置是正数
|
// 确保计算的滚动位置是正数
|
||||||
const scrollTop = Math.max(0, elementTop - (containerHeight / 2) + (elementHeight / 2));
|
const scrollTop = Math.max(
|
||||||
|
0,
|
||||||
|
elementTop -
|
||||||
|
containerHeight / 2 +
|
||||||
|
elementHeight / 2,
|
||||||
|
);
|
||||||
|
|
||||||
// 使用 scrollContainer 而不是 container
|
// 使用 scrollContainer 而不是 container
|
||||||
scrollContainer.scrollTo({
|
scrollContainer.scrollTo({
|
||||||
top: scrollTop,
|
top: scrollTop,
|
||||||
behavior: 'smooth'
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1050,7 +1129,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
const element = document.getElementById(item.id);
|
const element = document.getElementById(item.id);
|
||||||
if (element) {
|
if (element) {
|
||||||
const yOffset = -80;
|
const yOffset = -80;
|
||||||
element.scrollIntoView({ behavior: 'smooth' });
|
element.scrollIntoView({ behavior: "smooth" });
|
||||||
window.scrollBy(0, yOffset);
|
window.scrollBy(0, yOffset);
|
||||||
setActiveId(item.id);
|
setActiveId(item.id);
|
||||||
setShowToc(false);
|
setShowToc(false);
|
||||||
@ -1077,7 +1156,8 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="prose dark:prose-invert max-w-none
|
<Box
|
||||||
|
className="prose dark:prose-invert max-w-none
|
||||||
[&_pre]:!bg-transparent [&_pre]:!p-0 [&_pre]:!m-0 [&_pre]:!border-0
|
[&_pre]:!bg-transparent [&_pre]:!p-0 [&_pre]:!m-0 [&_pre]:!border-0
|
||||||
[&_.prism-code]:!bg-transparent [&_.prism-code]:!shadow-none
|
[&_.prism-code]:!bg-transparent [&_.prism-code]:!shadow-none
|
||||||
[&_pre_.prism-code]:!bg-transparent [&_pre_.prism-code]:!shadow-none
|
[&_pre_.prism-code]:!bg-transparent [&_pre_.prism-code]:!shadow-none
|
||||||
@ -1086,7 +1166,8 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
[&_:not(pre)>code]:![&::before]:hidden [&_:not(pre)>code]:![&::after]:hidden
|
[&_:not(pre)>code]:![&::before]:hidden [&_:not(pre)>code]:![&::after]:hidden
|
||||||
[&_:not(pre)>code]:[&::before]:content-none [&_:not(pre)>code]:[&::after]:content-none
|
[&_:not(pre)>code]:[&::before]:content-none [&_:not(pre)>code]:[&::after]:content-none
|
||||||
[&_:not(pre)>code]:!bg-[--gray-4] [&_:not(pre)>code]:!text-[--accent-11]
|
[&_:not(pre)>code]:!bg-[--gray-4] [&_:not(pre)>code]:!text-[--accent-11]
|
||||||
">
|
"
|
||||||
|
>
|
||||||
<div ref={contentRef}>
|
<div ref={contentRef}>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
components={components}
|
components={components}
|
||||||
@ -1130,10 +1211,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
className="items-center text-[--gray-11] flex-wrap"
|
className="items-center text-[--gray-11] flex-wrap"
|
||||||
>
|
>
|
||||||
{/* 作者名字 */}
|
{/* 作者名字 */}
|
||||||
<Text
|
<Text size="2" weight="medium">
|
||||||
size="2"
|
|
||||||
weight="medium"
|
|
||||||
>
|
|
||||||
{mockPost.authorName}
|
{mockPost.authorName}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@ -1141,10 +1219,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<Box className="w-px h-4 bg-[--gray-6]" />
|
<Box className="w-px h-4 bg-[--gray-6]" />
|
||||||
|
|
||||||
{/* 发布日期 */}
|
{/* 发布日期 */}
|
||||||
<Flex
|
<Flex align="center" gap="2">
|
||||||
align="center"
|
|
||||||
gap="2"
|
|
||||||
>
|
|
||||||
<CalendarIcon className="w-3.5 h-3.5" />
|
<CalendarIcon className="w-3.5 h-3.5" />
|
||||||
<Text size="2">
|
<Text size="2">
|
||||||
{mockPost.publishedAt?.toLocaleDateString("zh-CN", {
|
{mockPost.publishedAt?.toLocaleDateString("zh-CN", {
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
import { Template } from "interface/template";
|
import { Template } from "interface/template";
|
||||||
import { Container, Heading, Text, Flex, Card, Button, ScrollArea } from "@radix-ui/themes";
|
import {
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Flex,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
ScrollArea,
|
||||||
|
} from "@radix-ui/themes";
|
||||||
import {
|
import {
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
@ -25,19 +33,18 @@ const mockArticles: PostDisplay[] = [
|
|||||||
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: {
|
taxonomies: {
|
||||||
categories: [
|
categories: [{ name: "前端开发", slug: "frontend", type: "category" }],
|
||||||
{ name: "前端开发", slug: "frontend", type: "category" }
|
|
||||||
],
|
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "工程化", slug: "engineering", type: "tag" },
|
{ name: "工程化", slug: "engineering", type: "tag" },
|
||||||
{ name: "效率提升", slug: "efficiency", type: "tag" }
|
{ name: "效率提升", slug: "efficiency", type: "tag" },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: "React 18 新特性详解",
|
title: "React 18 新特性详解",
|
||||||
content: "React 18 带来了许多令人兴奋的新特性,包括并发渲染、自动批处理更新...",
|
content:
|
||||||
|
"React 18 带来了许多令人兴奋的新特性,包括并发渲染、自动批处理更新...",
|
||||||
authorName: "李四",
|
authorName: "李四",
|
||||||
publishedAt: new Date("2024-03-14"),
|
publishedAt: new Date("2024-03-14"),
|
||||||
coverImage: "",
|
coverImage: "",
|
||||||
@ -46,19 +53,18 @@ const mockArticles: PostDisplay[] = [
|
|||||||
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: {
|
taxonomies: {
|
||||||
categories: [
|
categories: [{ name: "前端开发", slug: "frontend", type: "category" }],
|
||||||
{ name: "前端开发", slug: "frontend", type: "category" }
|
|
||||||
],
|
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "React", slug: "react", type: "tag" },
|
{ name: "React", slug: "react", type: "tag" },
|
||||||
{ name: "JavaScript", slug: "javascript", type: "tag" }
|
{ name: "JavaScript", slug: "javascript", type: "tag" },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: "JavaScript 性能优化技巧",
|
title: "JavaScript 性能优化技巧",
|
||||||
content: "在这篇文章中,我们将探讨一些提高 JavaScript 性能的技巧和最佳实践...",
|
content:
|
||||||
|
"在这篇文章中,我们将探讨一些提高 JavaScript 性能的技巧和最佳实践...",
|
||||||
authorName: "王五",
|
authorName: "王五",
|
||||||
publishedAt: new Date("2024-03-13"),
|
publishedAt: new Date("2024-03-13"),
|
||||||
coverImage: "ssssxx",
|
coverImage: "ssssxx",
|
||||||
@ -68,13 +74,17 @@ const mockArticles: PostDisplay[] = [
|
|||||||
updatedAt: new Date("2024-03-13"),
|
updatedAt: new Date("2024-03-13"),
|
||||||
taxonomies: {
|
taxonomies: {
|
||||||
categories: [
|
categories: [
|
||||||
{ name: "性能优化", slug: "performance-optimization", type: "category" }
|
{
|
||||||
|
name: "性能优化",
|
||||||
|
slug: "performance-optimization",
|
||||||
|
type: "category",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "JavaScript", slug: "javascript", type: "tag" },
|
{ name: "JavaScript", slug: "javascript", type: "tag" },
|
||||||
{ name: "性能", slug: "performance", type: "tag" }
|
{ name: "性能", slug: "performance", type: "tag" },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
@ -82,28 +92,31 @@ const mockArticles: PostDisplay[] = [
|
|||||||
content: "移动端开发中的各种适配问题及解决方案...",
|
content: "移动端开发中的各种适配问题及解决方案...",
|
||||||
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",
|
||||||
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: {
|
taxonomies: {
|
||||||
categories: [
|
categories: [
|
||||||
{ name: "移动开发", slug: "mobile-development", type: "category" }
|
{ name: "移动开发", slug: "mobile-development", type: "category" },
|
||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "移动端", slug: "mobile", type: "tag" },
|
{ name: "移动端", slug: "mobile", type: "tag" },
|
||||||
{ name: "响应式", slug: "responsive", type: "tag" }
|
{ name: "响应式", slug: "responsive", type: "tag" },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
title: "全栈开发:从前端到云原生的完整指南",
|
title: "全栈开发:从前端到云原生的完整指南",
|
||||||
content: "本文将深入探讨现代全栈开发的各个方面,包括前端框架选择、后端架构设计、数据库优化、微服务部署以及云原生实践...",
|
content:
|
||||||
|
"本文将深入探讨现代全栈开发的各个方面,包括前端框架选择、后端架构设计、数据库优化、微服务部署以及云原生实践...",
|
||||||
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",
|
||||||
status: "published",
|
status: "published",
|
||||||
isEditor: false,
|
isEditor: false,
|
||||||
createdAt: new Date("2024-03-12"),
|
createdAt: new Date("2024-03-12"),
|
||||||
@ -114,7 +127,7 @@ const mockArticles: PostDisplay[] = [
|
|||||||
{ name: "云原生", slug: "cloud-native", type: "category" },
|
{ name: "云原生", slug: "cloud-native", type: "category" },
|
||||||
{ name: "微服务", slug: "microservices", type: "category" },
|
{ name: "微服务", slug: "microservices", type: "category" },
|
||||||
{ name: "DevOps", slug: "devops", type: "category" },
|
{ name: "DevOps", slug: "devops", type: "category" },
|
||||||
{ name: "系统架构", slug: "system-architecture", type: "category" }
|
{ name: "系统架构", slug: "system-architecture", type: "category" },
|
||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "React", slug: "react", type: "tag" },
|
{ name: "React", slug: "react", type: "tag" },
|
||||||
@ -124,17 +137,19 @@ const mockArticles: PostDisplay[] = [
|
|||||||
{ name: "MongoDB", slug: "mongodb", type: "tag" },
|
{ name: "MongoDB", slug: "mongodb", type: "tag" },
|
||||||
{ name: "微服务", slug: "microservices", type: "tag" },
|
{ name: "微服务", slug: "microservices", type: "tag" },
|
||||||
{ name: "CI/CD", slug: "ci-cd", type: "tag" },
|
{ name: "CI/CD", slug: "ci-cd", type: "tag" },
|
||||||
{ name: "云计算", slug: "cloud-computing", type: "tag" }
|
{ name: "云计算", slug: "cloud-computing", type: "tag" },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
title: "深入浅出 TypeScript 高级特性",
|
title: "深入浅出 TypeScript 高级特性",
|
||||||
content: "探索 TypeScript 的高级类型系统、装饰器、类型编程等特性,以及在大型项目中的最佳实践...",
|
content:
|
||||||
|
"探索 TypeScript 的高级类型系统、装饰器、类型编程等特性,以及在大型项目中的最佳实践...",
|
||||||
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",
|
||||||
status: "published",
|
status: "published",
|
||||||
isEditor: false,
|
isEditor: false,
|
||||||
createdAt: new Date("2024-03-11"),
|
createdAt: new Date("2024-03-11"),
|
||||||
@ -142,47 +157,55 @@ const mockArticles: PostDisplay[] = [
|
|||||||
taxonomies: {
|
taxonomies: {
|
||||||
categories: [
|
categories: [
|
||||||
{ name: "TypeScript", slug: "typescript", type: "category" },
|
{ name: "TypeScript", slug: "typescript", type: "category" },
|
||||||
{ name: "编程语言", slug: "programming-languages", type: "category" }
|
{ name: "编程语言", slug: "programming-languages", type: "category" },
|
||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "类型系统", slug: "type-system", type: "tag" },
|
{ name: "类型系统", slug: "type-system", type: "tag" },
|
||||||
{ name: "泛型编程", slug: "generic-programming", type: "tag" },
|
{ name: "泛型编程", slug: "generic-programming", type: "tag" },
|
||||||
{ name: "装饰器", slug: "decorators", type: "tag" },
|
{ name: "装饰器", slug: "decorators", type: "tag" },
|
||||||
{ name: "类型推导", slug: "type-inference", type: "tag" }
|
{ name: "类型推导", slug: "type-inference", type: "tag" },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
title: "Web 性能优化:从理论到实践",
|
title: "Web 性能优化:从理论到实践",
|
||||||
content: "全面解析 Web 性能优化策略,包括资源加载优化、渲染性能优化、网络优化等多个...",
|
content:
|
||||||
|
"全面解析 Web 性能优化策略,包括资源加载优化、渲染性能优化、网络优化等多个...",
|
||||||
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",
|
||||||
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: {
|
taxonomies: {
|
||||||
categories: [
|
categories: [
|
||||||
{ name: "性能优化", slug: "performance-optimization", type: "category" },
|
{
|
||||||
{ name: "前端开发", slug: "frontend-development", type: "category" }
|
name: "性能优化",
|
||||||
|
slug: "performance-optimization",
|
||||||
|
type: "category",
|
||||||
|
},
|
||||||
|
{ name: "前端开发", slug: "frontend-development", type: "category" },
|
||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "性能监控", slug: "performance-monitoring", type: "tag" },
|
{ name: "性能监控", slug: "performance-monitoring", type: "tag" },
|
||||||
{ name: "懒加载", slug: "lazy-loading", type: "tag" },
|
{ name: "懒加载", slug: "lazy-loading", type: "tag" },
|
||||||
{ name: "缓存策略", slug: "caching-strategies", type: "tag" },
|
{ name: "缓存策略", slug: "caching-strategies", type: "tag" },
|
||||||
{ name: "代码分割", slug: "code-splitting", type: "tag" }
|
{ name: "代码分割", slug: "code-splitting", type: "tag" },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
id: 8,
|
||||||
title: "微前端架构实践指南",
|
title: "微前端架构实践指南",
|
||||||
content: "详细介绍微前端的架构设计、实现方案、应用集成以及实际项目中的经验总结...",
|
content:
|
||||||
|
"详细介绍微前端的架构设计、实现方案、应用集成以及实际项目中的经验总结...",
|
||||||
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",
|
||||||
status: "published",
|
status: "published",
|
||||||
isEditor: false,
|
isEditor: false,
|
||||||
createdAt: new Date("2024-03-09"),
|
createdAt: new Date("2024-03-09"),
|
||||||
@ -190,23 +213,25 @@ const mockArticles: PostDisplay[] = [
|
|||||||
taxonomies: {
|
taxonomies: {
|
||||||
categories: [
|
categories: [
|
||||||
{ name: "架构设计", slug: "architecture-design", type: "category" },
|
{ name: "架构设计", slug: "architecture-design", type: "category" },
|
||||||
{ name: "微前端", slug: "micro-frontends", type: "category" }
|
{ name: "微前端", slug: "micro-frontends", type: "category" },
|
||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "qiankun", slug: "qiankun", type: "tag" },
|
{ name: "qiankun", slug: "qiankun", type: "tag" },
|
||||||
{ name: "single-spa", slug: "single-spa", type: "tag" },
|
{ name: "single-spa", slug: "single-spa", type: "tag" },
|
||||||
{ name: "模块联邦", slug: "module-federation", type: "tag" },
|
{ name: "模块联邦", slug: "module-federation", type: "tag" },
|
||||||
{ name: "应用通信", slug: "application-communication", type: "tag" }
|
{ name: "应用通信", slug: "application-communication", type: "tag" },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
id: 9,
|
||||||
title: "AI 驱动的前端开发:从概念到实践",
|
title: "AI 驱动的前端开发:从概念到实践",
|
||||||
content: "探索如何将人工智能技术融入前端开发流程,包括智能代码补全、自动化测试、UI 生成、性能优化建议等实践应用...",
|
content:
|
||||||
|
"探索如何将人工智能技术融入前端开发流程,包括智能代码补全、自动化测试、UI 生成、性能优化建议等实践应用...",
|
||||||
authorName: "陈十一",
|
authorName: "陈十一",
|
||||||
publishedAt: new Date("2024-03-08"),
|
publishedAt: new Date("2024-03-08"),
|
||||||
coverImage: "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=500&auto=format",
|
coverImage:
|
||||||
|
"https://images.unsplash.com/photo-1677442136019-21780ecad995?w=500&auto=format",
|
||||||
status: "published",
|
status: "published",
|
||||||
isEditor: false,
|
isEditor: false,
|
||||||
createdAt: new Date("2024-03-08"),
|
createdAt: new Date("2024-03-08"),
|
||||||
@ -214,16 +239,16 @@ const mockArticles: PostDisplay[] = [
|
|||||||
taxonomies: {
|
taxonomies: {
|
||||||
categories: [
|
categories: [
|
||||||
{ name: "人工智能", slug: "artificial-intelligence", type: "category" },
|
{ name: "人工智能", slug: "artificial-intelligence", type: "category" },
|
||||||
{ name: "前端开发", slug: "frontend-development", type: "category" }
|
{ name: "前端开发", slug: "frontend-development", type: "category" },
|
||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "AI开发", slug: "ai-development", type: "tag" },
|
{ name: "AI开发", slug: "ai-development", type: "tag" },
|
||||||
{ name: "智能化", slug: "intelligence", type: "tag" },
|
{ name: "智能化", slug: "intelligence", type: "tag" },
|
||||||
{ name: "自动化", slug: "automation", type: "tag" },
|
{ name: "自动化", slug: "automation", type: "tag" },
|
||||||
{ name: "开发效率", slug: "development-efficiency", type: "tag" }
|
{ name: "开发效率", slug: "development-efficiency", type: "tag" },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default new Template({}, ({ http, args }) => {
|
export default new Template({}, ({ http, args }) => {
|
||||||
@ -240,14 +265,15 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
const renderPageNumbers = () => {
|
const renderPageNumbers = () => {
|
||||||
const pages = getPageNumbers(totalPages);
|
const pages = getPageNumbers(totalPages);
|
||||||
|
|
||||||
return pages.map(page => (
|
return pages.map((page) => (
|
||||||
<div
|
<div
|
||||||
key={page}
|
key={page}
|
||||||
className={`min-w-[32px] h-8 rounded-md transition-all duration-300 cursor-pointer
|
className={`min-w-[32px] h-8 rounded-md transition-all duration-300 cursor-pointer
|
||||||
flex items-center justify-center group/item whitespace-nowrap
|
flex items-center justify-center group/item whitespace-nowrap
|
||||||
${page === currentPage
|
${
|
||||||
? 'bg-[--accent-9] text-[--text-primary]'
|
page === currentPage
|
||||||
: 'text-[--text-secondary] hover:text-[--text-primary] hover:bg-[--accent-3]'
|
? "bg-[--accent-9] text-[--text-primary]"
|
||||||
|
: "text-[--text-secondary] hover:text-[--text-primary] hover:bg-[--accent-3]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
@ -290,8 +316,10 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
{article.title}
|
{article.title}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Text className="text-[--text-secondary] text-xs
|
<Text
|
||||||
line-clamp-2 leading-relaxed">
|
className="text-[--text-secondary] text-xs
|
||||||
|
line-clamp-2 leading-relaxed"
|
||||||
|
>
|
||||||
{article.content}
|
{article.content}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@ -322,7 +350,11 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
<Flex gap="2" align="center" className="text-[--text-tertiary] flex-shrink-0">
|
<Flex
|
||||||
|
gap="2"
|
||||||
|
align="center"
|
||||||
|
className="text-[--text-tertiary] flex-shrink-0"
|
||||||
|
>
|
||||||
<CalendarIcon className="w-4 h-4" />
|
<CalendarIcon className="w-4 h-4" />
|
||||||
<Text size="2">
|
<Text size="2">
|
||||||
{article.publishedAt?.toLocaleDateString("zh-CN", {
|
{article.publishedAt?.toLocaleDateString("zh-CN", {
|
||||||
@ -352,7 +384,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
className={`inline-block w-1 h-1 rounded-full ${getColorScheme(tag.name).dot}`}
|
className={`inline-block w-1 h-1 rounded-full ${getColorScheme(tag.name).dot}`}
|
||||||
style={{
|
style={{
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
opacity: 0.8
|
opacity: 0.8,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
@ -388,7 +420,11 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<ChevronLeftIcon className="w-4 h-4 text-[--text-tertiary]" />
|
<ChevronLeftIcon className="w-4 h-4 text-[--text-tertiary]" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Flex align="center" gap="2" className="flex-1 md:flex-none justify-center">
|
<Flex
|
||||||
|
align="center"
|
||||||
|
gap="2"
|
||||||
|
className="flex-1 md:flex-none justify-center"
|
||||||
|
>
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
type="hover"
|
type="hover"
|
||||||
scrollbars="horizontal"
|
scrollbars="horizontal"
|
||||||
@ -399,7 +435,10 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
<Text size="1" className="text-[--text-tertiary] whitespace-nowrap hidden md:block">
|
<Text
|
||||||
|
size="1"
|
||||||
|
className="text-[--text-tertiary] whitespace-nowrap hidden md:block"
|
||||||
|
>
|
||||||
共 {totalPages} 页
|
共 {totalPages} 页
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -412,10 +451,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<ChevronRightIcon className="w-4 h-4 md:ml-1 text-[--text-tertiary] group-hover/nav:translate-x-0.5 transition-transform" />
|
<ChevronRightIcon className="w-4 h-4 md:ml-1 text-[--text-tertiary] group-hover/nav:translate-x-0.5 transition-transform" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button variant="ghost" className="group/nav w-8 h-8 md:hidden">
|
||||||
variant="ghost"
|
|
||||||
className="group/nav w-8 h-8 md:hidden"
|
|
||||||
>
|
|
||||||
<ChevronRightIcon className="w-4 h-4 text-[--text-tertiary]" />
|
<ChevronRightIcon className="w-4 h-4 text-[--text-tertiary]" />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -3,7 +3,7 @@ export function hashString(str: string): number {
|
|||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
const char = str.charCodeAt(i);
|
const char = str.charCodeAt(i);
|
||||||
hash = ((hash << 5) - hash) + char;
|
hash = (hash << 5) - hash + char;
|
||||||
hash = hash & hash;
|
hash = hash & hash;
|
||||||
}
|
}
|
||||||
return Math.abs(hash);
|
return Math.abs(hash);
|
||||||
@ -11,16 +11,66 @@ export function hashString(str: string): number {
|
|||||||
|
|
||||||
export function getColorScheme(name: string) {
|
export function getColorScheme(name: string) {
|
||||||
const colorSchemes = [
|
const colorSchemes = [
|
||||||
{ name: 'amber', bg: 'bg-[--amber-a3]', text: 'text-[--amber-11]', border: 'border-[--amber-a6]' },
|
{
|
||||||
{ name: 'blue', bg: 'bg-[--blue-a3]', text: 'text-[--blue-11]', border: 'border-[--blue-a6]' },
|
name: "amber",
|
||||||
{ name: 'crimson', bg: 'bg-[--crimson-a3]', text: 'text-[--crimson-11]', border: 'border-[--crimson-a6]' },
|
bg: "bg-[--amber-a3]",
|
||||||
{ name: 'cyan', bg: 'bg-[--cyan-a3]', text: 'text-[--cyan-11]', border: 'border-[--cyan-a6]' },
|
text: "text-[--amber-11]",
|
||||||
{ name: 'grass', bg: 'bg-[--grass-a3]', text: 'text-[--grass-11]', border: 'border-[--grass-a6]' },
|
border: "border-[--amber-a6]",
|
||||||
{ name: 'mint', bg: 'bg-[--mint-a3]', text: 'text-[--mint-11]', border: 'border-[--mint-a6]' },
|
},
|
||||||
{ name: 'orange', bg: 'bg-[--orange-a3]', text: 'text-[--orange-11]', border: 'border-[--orange-a6]' },
|
{
|
||||||
{ name: 'pink', bg: 'bg-[--pink-a3]', text: 'text-[--pink-11]', border: 'border-[--pink-a6]' },
|
name: "blue",
|
||||||
{ name: 'plum', bg: 'bg-[--plum-a3]', text: 'text-[--plum-11]', border: 'border-[--plum-a6]' },
|
bg: "bg-[--blue-a3]",
|
||||||
{ name: 'violet', bg: 'bg-[--violet-a3]', text: 'text-[--violet-11]', border: 'border-[--violet-a6]' }
|
text: "text-[--blue-11]",
|
||||||
|
border: "border-[--blue-a6]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "crimson",
|
||||||
|
bg: "bg-[--crimson-a3]",
|
||||||
|
text: "text-[--crimson-11]",
|
||||||
|
border: "border-[--crimson-a6]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cyan",
|
||||||
|
bg: "bg-[--cyan-a3]",
|
||||||
|
text: "text-[--cyan-11]",
|
||||||
|
border: "border-[--cyan-a6]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "grass",
|
||||||
|
bg: "bg-[--grass-a3]",
|
||||||
|
text: "text-[--grass-11]",
|
||||||
|
border: "border-[--grass-a6]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mint",
|
||||||
|
bg: "bg-[--mint-a3]",
|
||||||
|
text: "text-[--mint-11]",
|
||||||
|
border: "border-[--mint-a6]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "orange",
|
||||||
|
bg: "bg-[--orange-a3]",
|
||||||
|
text: "text-[--orange-11]",
|
||||||
|
border: "border-[--orange-a6]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pink",
|
||||||
|
bg: "bg-[--pink-a3]",
|
||||||
|
text: "text-[--pink-11]",
|
||||||
|
border: "border-[--pink-a6]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "plum",
|
||||||
|
bg: "bg-[--plum-a3]",
|
||||||
|
text: "text-[--plum-11]",
|
||||||
|
border: "border-[--plum-a6]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "violet",
|
||||||
|
bg: "bg-[--violet-a3]",
|
||||||
|
text: "text-[--violet-11]",
|
||||||
|
border: "border-[--violet-a6]",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const index = hashString(name) % colorSchemes.length;
|
const index = hashString(name) % colorSchemes.length;
|
||||||
@ -30,7 +80,7 @@ export function getColorScheme(name: string) {
|
|||||||
bg: scheme.bg,
|
bg: scheme.bg,
|
||||||
text: scheme.text,
|
text: scheme.text,
|
||||||
border: scheme.border,
|
border: scheme.border,
|
||||||
hover: `hover:${scheme.bg.replace('3', '4')}`,
|
hover: `hover:${scheme.bg.replace("3", "4")}`,
|
||||||
dot: `bg-current`
|
dot: `bg-current`,
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -30,7 +30,8 @@ const createDefineConfig = (config: EnvConfig) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> => {
|
export default defineConfig(
|
||||||
|
async ({ mode }: ConfigEnv): Promise<UserConfig> => {
|
||||||
// 确保每次都读取最新的环境变量
|
// 确保每次都读取最新的环境变量
|
||||||
const currentConfig = await getLatestEnv();
|
const currentConfig = await getLatestEnv();
|
||||||
const env = loadEnv(mode, process.cwd(), "VITE_");
|
const env = loadEnv(mode, process.cwd(), "VITE_");
|
||||||
@ -71,18 +72,6 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
|
|||||||
watch: {
|
watch: {
|
||||||
usePolling: true,
|
usePolling: true,
|
||||||
},
|
},
|
||||||
proxy: {
|
|
||||||
"/__/api": {
|
|
||||||
target: currentConfig.VITE_API_BASE_URL,
|
|
||||||
changeOrigin: true,
|
|
||||||
rewrite: (path: string) => path.replace(/^\/__\/api/, ""),
|
|
||||||
},
|
|
||||||
"/__/express": {
|
|
||||||
target: `http://${currentConfig.VITE_ADDRESS}:${Number(currentConfig.VITE_PORT) + 1}`,
|
|
||||||
changeOrigin: true,
|
|
||||||
rewrite: (path: string) => path.replace(/^\/__\/express/, ""),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
publicDir: resolve(__dirname, "public"),
|
publicDir: resolve(__dirname, "public"),
|
||||||
envPrefix: "VITE_",
|
envPrefix: "VITE_",
|
||||||
@ -91,28 +80,34 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
|
|||||||
output: {
|
output: {
|
||||||
manualChunks(id) {
|
manualChunks(id) {
|
||||||
// 根据模块路径进行代码分割
|
// 根据模块路径进行代码分割
|
||||||
if (id.includes('node_modules')) {
|
if (id.includes("node_modules")) {
|
||||||
return 'vendor';
|
return "vendor";
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
chunkSizeWarningLimit: 1500,
|
chunkSizeWarningLimit: 1500,
|
||||||
minify: 'terser',
|
minify: "terser",
|
||||||
terserOptions: {
|
terserOptions: {
|
||||||
compress: {
|
compress: {
|
||||||
drop_console: true,
|
drop_console: true,
|
||||||
drop_debugger: true
|
drop_debugger: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
cssMinify: true,
|
cssMinify: true,
|
||||||
cssCodeSplit: true,
|
cssCodeSplit: true,
|
||||||
sourcemap: mode !== 'production',
|
sourcemap: mode !== "production",
|
||||||
assetsInlineLimit: 4096,
|
assetsInlineLimit: 4096,
|
||||||
reportCompressedSize: false,
|
reportCompressedSize: false,
|
||||||
},
|
},
|
||||||
ssr: {
|
ssr: {
|
||||||
noExternal: ['three', '@react-three/fiber', '@react-three/drei', 'gsap']
|
noExternal: [
|
||||||
}
|
"three",
|
||||||
|
"@react-three/fiber",
|
||||||
|
"@react-three/drei",
|
||||||
|
"gsap",
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user