前端:主题提取公共样式,新增多个管理页面组件,优化背景动画,改进样式和交互效果,更新文章内容和排版,主题模新增管理页面
This commit is contained in:
parent
66d47f6cfa
commit
14f50467f0
209
frontend/app/dashboard/categories.tsx
Normal file
209
frontend/app/dashboard/categories.tsx
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import { Template } from "interface/template";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
ScrollArea,
|
||||||
|
Dialog,
|
||||||
|
IconButton
|
||||||
|
} from "@radix-ui/themes";
|
||||||
|
import {
|
||||||
|
PlusIcon,
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
Pencil1Icon,
|
||||||
|
TrashIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { Category } from "interface/fields";
|
||||||
|
|
||||||
|
// 模拟分类数据
|
||||||
|
const mockCategories: (Category & { id: number; count: number })[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "前端开发",
|
||||||
|
parentId: undefined,
|
||||||
|
count: 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "React",
|
||||||
|
parentId: "1",
|
||||||
|
count: 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "Vue",
|
||||||
|
parentId: "1",
|
||||||
|
count: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "后端开发",
|
||||||
|
parentId: undefined,
|
||||||
|
count: 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: "Node.js",
|
||||||
|
parentId: "4",
|
||||||
|
count: 6
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default new Template({}, ({ http, args }) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
|
const [newCategoryName, setNewCategoryName] = useState("");
|
||||||
|
const [selectedParentId, setSelectedParentId] = useState<string | undefined>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* 页面标题和操作栏 */}
|
||||||
|
<Flex justify="between" align="center" className="mb-6">
|
||||||
|
<Box>
|
||||||
|
<Heading size="6" className="text-[--gray-12] mb-2">
|
||||||
|
分类管理
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-[--gray-11]">
|
||||||
|
共 {mockCategories.length} 个分类
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
className="bg-[--accent-9]"
|
||||||
|
onClick={() => setIsAddDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<PlusIcon className="w-4 h-4" />
|
||||||
|
新建分类
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 搜索栏 */}
|
||||||
|
<Box className="w-full sm:w-64 mb-6">
|
||||||
|
<TextField.Root
|
||||||
|
placeholder="搜索分类..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
||||||
|
>
|
||||||
|
<TextField.Slot>
|
||||||
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
|
</TextField.Slot>
|
||||||
|
</TextField.Root>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 分类列表 */}
|
||||||
|
<Box className="border border-[--gray-6] rounded-lg overflow-hidden">
|
||||||
|
<ScrollArea>
|
||||||
|
<Table.Root>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeaderCell>分类名称</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>文章数量</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>父分类</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>操作</Table.ColumnHeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
|
||||||
|
<Table.Body>
|
||||||
|
{mockCategories.map((category) => (
|
||||||
|
<Table.Row key={category.id}>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
{category.parentId && (
|
||||||
|
<ChevronRightIcon className="w-4 h-4 text-[--gray-11]" />
|
||||||
|
)}
|
||||||
|
<Text>{category.name}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Text>{category.count} 篇</Text>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Text>
|
||||||
|
{category.parentId
|
||||||
|
? mockCategories.find(c => c.id.toString() === category.parentId)?.name
|
||||||
|
: '-'
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex gap="2">
|
||||||
|
<Button variant="ghost" size="1">
|
||||||
|
<Pencil1Icon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="1" color="red">
|
||||||
|
<TrashIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 新建分类对话框 */}
|
||||||
|
<Dialog.Root open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||||
|
<Dialog.Content style={{ maxWidth: 450 }}>
|
||||||
|
<Dialog.Title>新建分类</Dialog.Title>
|
||||||
|
<Dialog.Description size="2" mb="4">
|
||||||
|
创建一个新的文章分类
|
||||||
|
</Dialog.Description>
|
||||||
|
|
||||||
|
<Flex direction="column" gap="3">
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" mb="1" weight="bold">
|
||||||
|
分类名称
|
||||||
|
</Text>
|
||||||
|
<TextField.Root
|
||||||
|
placeholder="输入分类名称"
|
||||||
|
value={newCategoryName}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewCategoryName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" mb="1" weight="bold">
|
||||||
|
父分类
|
||||||
|
</Text>
|
||||||
|
<select
|
||||||
|
className="w-full h-9 px-3 rounded-md bg-[--gray-1] border border-[--gray-6] text-[--gray-12]"
|
||||||
|
value={selectedParentId}
|
||||||
|
onChange={(e) => setSelectedParentId(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">无</option>
|
||||||
|
{mockCategories
|
||||||
|
.filter(c => !c.parentId)
|
||||||
|
.map(category => (
|
||||||
|
<option key={category.id} value={category.id}>
|
||||||
|
{category.name}
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap="3" mt="4" justify="end">
|
||||||
|
<Dialog.Close>
|
||||||
|
<Button variant="soft" color="gray">
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Dialog.Close>
|
||||||
|
<Dialog.Close>
|
||||||
|
<Button className="bg-[--accent-9]">
|
||||||
|
创建
|
||||||
|
</Button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</Flex>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
297
frontend/app/dashboard/comments.tsx
Normal file
297
frontend/app/dashboard/comments.tsx
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
import { Template } from "interface/template";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
DropdownMenu,
|
||||||
|
ScrollArea,
|
||||||
|
DataList,
|
||||||
|
Avatar,
|
||||||
|
Badge
|
||||||
|
} from "@radix-ui/themes";
|
||||||
|
import {
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
CheckIcon,
|
||||||
|
Cross2Icon,
|
||||||
|
ChatBubbleIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
// 模拟评论数据
|
||||||
|
const mockComments = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
content: "这篇文章写得很好,对我帮助很大!",
|
||||||
|
author: "张三",
|
||||||
|
postTitle: "构建现代化的前端开发工作流",
|
||||||
|
createdAt: new Date("2024-03-15"),
|
||||||
|
status: "pending", // pending, approved, rejected
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
content: "文章内容很专业,讲解得很清楚。",
|
||||||
|
author: "李四",
|
||||||
|
postTitle: "React 18 新特性详解",
|
||||||
|
createdAt: new Date("2024-03-14"),
|
||||||
|
status: "approved",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2"
|
||||||
|
},
|
||||||
|
// 可以添加更多模拟数据
|
||||||
|
];
|
||||||
|
|
||||||
|
export default new Template({}, ({ http, args }) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [selectedStatus, setSelectedStatus] = useState<string>("all");
|
||||||
|
|
||||||
|
const getStatusStyle = (status: string) => {
|
||||||
|
switch(status) {
|
||||||
|
case 'approved':
|
||||||
|
return 'bg-[--green-3] text-[--green-11]';
|
||||||
|
case 'rejected':
|
||||||
|
return 'bg-[--red-3] text-[--red-11]';
|
||||||
|
default:
|
||||||
|
return 'bg-[--yellow-3] text-[--yellow-11]';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
switch(status) {
|
||||||
|
case 'approved':
|
||||||
|
return '已通过';
|
||||||
|
case 'rejected':
|
||||||
|
return '已拒绝';
|
||||||
|
default:
|
||||||
|
return '待审核';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* 页面标题和统计 */}
|
||||||
|
<Flex justify="between" align="center" className="mb-6">
|
||||||
|
<Box>
|
||||||
|
<Heading size="6" className="text-[--gray-12] mb-2">
|
||||||
|
评论管理
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-[--gray-11]">
|
||||||
|
共 {mockComments.length} 条评论
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 搜索和筛选栏 */}
|
||||||
|
<Flex
|
||||||
|
gap="4"
|
||||||
|
className="mb-6 flex-col sm:flex-row"
|
||||||
|
>
|
||||||
|
<Box className="w-full sm:w-64">
|
||||||
|
<TextField.Root
|
||||||
|
placeholder="搜索评论..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
||||||
|
>
|
||||||
|
<TextField.Slot>
|
||||||
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
|
</TextField.Slot>
|
||||||
|
</TextField.Root>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<Button variant="surface">
|
||||||
|
状态: {selectedStatus === 'all' ? '全部' : selectedStatus}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Item onClick={() => setSelectedStatus('all')}>
|
||||||
|
全部
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onClick={() => setSelectedStatus('pending')}>
|
||||||
|
待审核
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onClick={() => setSelectedStatus('approved')}>
|
||||||
|
已通过
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onClick={() => setSelectedStatus('rejected')}>
|
||||||
|
已拒绝
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 评论列表 */}
|
||||||
|
<Box className="border border-[--gray-6] rounded-lg overflow-hidden">
|
||||||
|
<ScrollArea>
|
||||||
|
{/* 桌面端表格视图 */}
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<Table.Root>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeaderCell>评论者</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>评论内容</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>文章</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>状态</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>时间</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>操作</Table.ColumnHeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
|
||||||
|
<Table.Body>
|
||||||
|
{mockComments.map((comment) => (
|
||||||
|
<Table.Row key={comment.id}>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Avatar
|
||||||
|
src={comment.avatar}
|
||||||
|
fallback="U"
|
||||||
|
size="2"
|
||||||
|
radius="full"
|
||||||
|
/>
|
||||||
|
<Text>{comment.author}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Text className="line-clamp-2">
|
||||||
|
{comment.content}
|
||||||
|
</Text>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Text className="line-clamp-1">
|
||||||
|
{comment.postTitle}
|
||||||
|
</Text>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex gap="2">
|
||||||
|
{comment.status === 'approved' && <Badge color="green">已通过</Badge>}
|
||||||
|
{comment.status === 'pending' && <Badge color="orange">待审核</Badge>}
|
||||||
|
{comment.status === 'rejected' && <Badge color="red">已拒绝</Badge>}
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{comment.createdAt.toLocaleDateString()}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex gap="2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="1"
|
||||||
|
className="text-[--green-11] hover:text-[--green-12]"
|
||||||
|
>
|
||||||
|
<CheckIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="1"
|
||||||
|
className="text-[--red-11] hover:text-[--red-12]"
|
||||||
|
>
|
||||||
|
<Cross2Icon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="1"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
<TrashIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 移动端列表视图 */}
|
||||||
|
<div className="block sm:hidden">
|
||||||
|
{mockComments.map((comment) => (
|
||||||
|
<DataList.Root key={comment.id} className="p-4 border-b border-[--gray-6] last:border-b-0">
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">评论者</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Avatar
|
||||||
|
src={comment.avatar}
|
||||||
|
fallback="U"
|
||||||
|
size="2"
|
||||||
|
radius="full"
|
||||||
|
/>
|
||||||
|
<Text>{comment.author}</Text>
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">评论内容</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Text className="line-clamp-3">{comment.content}</Text>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">文章</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Text className="line-clamp-1">{comment.postTitle}</Text>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">状态</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex gap="2">
|
||||||
|
{comment.status === 'approved' && <Badge color="green">已通过</Badge>}
|
||||||
|
{comment.status === 'pending' && <Badge color="orange">待审核</Badge>}
|
||||||
|
{comment.status === 'rejected' && <Badge color="red">已拒绝</Badge>}
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">时间</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
{comment.createdAt.toLocaleDateString()}
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">操作</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex gap="2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="1"
|
||||||
|
className="text-[--green-11] hover:text-[--green-12]"
|
||||||
|
>
|
||||||
|
<CheckIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="1"
|
||||||
|
className="text-[--red-11] hover:text-[--red-12]"
|
||||||
|
>
|
||||||
|
<Cross2Icon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="1"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
<TrashIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
</DataList.Root>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
305
frontend/app/dashboard/files.tsx
Normal file
305
frontend/app/dashboard/files.tsx
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
import { Template } from "interface/template";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
DropdownMenu,
|
||||||
|
ScrollArea,
|
||||||
|
Dialog,
|
||||||
|
DataList
|
||||||
|
} from "@radix-ui/themes";
|
||||||
|
import {
|
||||||
|
PlusIcon,
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
FileIcon,
|
||||||
|
TrashIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
DotsHorizontalIcon,
|
||||||
|
FileTextIcon,
|
||||||
|
ImageIcon,
|
||||||
|
VideoIcon
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { Resource } from "interface/fields";
|
||||||
|
|
||||||
|
// 模拟文件数据
|
||||||
|
const mockFiles: Resource[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
authorId: "1",
|
||||||
|
name: "前端开发规范.pdf",
|
||||||
|
sizeBytes: 1024 * 1024 * 2, // 2MB
|
||||||
|
storagePath: "/files/frontend-guide.pdf",
|
||||||
|
fileType: "application/pdf",
|
||||||
|
category: "documents",
|
||||||
|
description: "前端开发规范文档",
|
||||||
|
createdAt: new Date("2024-03-15")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
authorId: "1",
|
||||||
|
name: "项目架构图.png",
|
||||||
|
sizeBytes: 1024 * 512, // 512KB
|
||||||
|
storagePath: "/files/architecture.png",
|
||||||
|
fileType: "image/png",
|
||||||
|
category: "images",
|
||||||
|
description: "项目整体架构示意图",
|
||||||
|
createdAt: new Date("2024-03-14")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
authorId: "1",
|
||||||
|
name: "API文档.md",
|
||||||
|
sizeBytes: 1024 * 256, // 256KB
|
||||||
|
storagePath: "/files/api-doc.md",
|
||||||
|
fileType: "text/markdown",
|
||||||
|
category: "documents",
|
||||||
|
description: "API接口文档",
|
||||||
|
createdAt: new Date("2024-03-13")
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default new Template({}, ({ http, args }) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [selectedType, setSelectedType] = useState<string>("all");
|
||||||
|
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatFileSize = (bytes: number) => {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取文件图标
|
||||||
|
const getFileIcon = (fileType: string) => {
|
||||||
|
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('text/')) return <FileTextIcon className="w-4 h-4" />;
|
||||||
|
return <FileIcon className="w-4 h-4" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* 页面标题和操作栏 */}
|
||||||
|
<Flex justify="between" align="center" className="mb-6">
|
||||||
|
<Box>
|
||||||
|
<Heading size="6" className="text-[--gray-12] mb-2">
|
||||||
|
文件管理
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-[--gray-11]">
|
||||||
|
共 {mockFiles.length} 个文件
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
className="bg-[--accent-9]"
|
||||||
|
onClick={() => setIsUploadDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<PlusIcon className="w-4 h-4" />
|
||||||
|
上传文件
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 搜索和筛选栏 */}
|
||||||
|
<Flex
|
||||||
|
gap="4"
|
||||||
|
className="mb-6 flex-col sm:flex-row"
|
||||||
|
>
|
||||||
|
<Box className="w-full sm:w-64">
|
||||||
|
<TextField.Root
|
||||||
|
placeholder="搜索文件..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
||||||
|
>
|
||||||
|
<TextField.Slot>
|
||||||
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
|
</TextField.Slot>
|
||||||
|
</TextField.Root>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<Button variant="surface">
|
||||||
|
类型: {selectedType === 'all' ? '全部' : selectedType}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Item onClick={() => setSelectedType('all')}>
|
||||||
|
全部
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onClick={() => setSelectedType('documents')}>
|
||||||
|
文档
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onClick={() => setSelectedType('images')}>
|
||||||
|
图片
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onClick={() => setSelectedType('others')}>
|
||||||
|
其他
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 文件列表 */}
|
||||||
|
<Box className="border border-[--gray-6] rounded-lg overflow-hidden">
|
||||||
|
<ScrollArea>
|
||||||
|
{/* 桌面端表格视图 */}
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<Table.Root>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeaderCell>文件名</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>大小</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>类型</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>上传时间</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>操作</Table.ColumnHeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
|
||||||
|
<Table.Body>
|
||||||
|
{mockFiles.map((file) => (
|
||||||
|
<Table.Row key={file.id}>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
{getFileIcon(file.fileType)}
|
||||||
|
<Text className="font-medium">{file.name}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{formatFileSize(file.sizeBytes)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Text className="text-[--gray-11]">
|
||||||
|
{file.fileType.split('/')[1].toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{file.createdAt.toLocaleDateString()}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex gap="2">
|
||||||
|
<Button variant="ghost" size="1">
|
||||||
|
<DownloadIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="1" color="red">
|
||||||
|
<TrashIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 移动端列表视图 */}
|
||||||
|
<div className="block sm:hidden">
|
||||||
|
{mockFiles.map((file) => (
|
||||||
|
<DataList.Root key={file.id} className="p-4 border-b border-[--gray-6] last:border-b-0">
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">文件名</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
{getFileIcon(file.fileType)}
|
||||||
|
<Text className="font-medium">{file.name}</Text>
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">大小</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
{formatFileSize(file.sizeBytes)}
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">类型</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
{file.fileType.split('/')[1].toUpperCase()}
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">上传时间</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
{file.createdAt.toLocaleDateString()}
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">操作</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex gap="2">
|
||||||
|
<Button variant="ghost" size="1">
|
||||||
|
<DownloadIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="1" color="red">
|
||||||
|
<TrashIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
</DataList.Root>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 上传对话框 */}
|
||||||
|
<Dialog.Root open={isUploadDialogOpen} onOpenChange={setIsUploadDialogOpen}>
|
||||||
|
<Dialog.Content style={{ maxWidth: 450 }}>
|
||||||
|
<Dialog.Title>上传文件</Dialog.Title>
|
||||||
|
<Dialog.Description size="2" mb="4">
|
||||||
|
选择要上传的文件
|
||||||
|
</Dialog.Description>
|
||||||
|
|
||||||
|
<Box className="border-2 border-dashed border-[--gray-6] rounded-lg p-8 text-center">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
id="file-upload"
|
||||||
|
multiple
|
||||||
|
onChange={(e) => {
|
||||||
|
// 处理文件上传
|
||||||
|
console.log(e.target.files);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="file-upload"
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<FileIcon className="w-12 h-12 mx-auto mb-4 text-[--gray-9]" />
|
||||||
|
<Text className="text-[--gray-11] mb-2">
|
||||||
|
拖拽文件到此处或点击上传
|
||||||
|
</Text>
|
||||||
|
<Text size="1" className="text-[--gray-10]">
|
||||||
|
支持所有常见文件格式
|
||||||
|
</Text>
|
||||||
|
</label>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Flex gap="3" mt="4" justify="end">
|
||||||
|
<Dialog.Close>
|
||||||
|
<Button variant="soft" color="gray">
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Dialog.Close>
|
||||||
|
<Dialog.Close>
|
||||||
|
<Button className="bg-[--accent-9]">
|
||||||
|
开始上传
|
||||||
|
</Button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</Flex>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
169
frontend/app/dashboard/index.tsx
Normal file
169
frontend/app/dashboard/index.tsx
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import { Template } from "interface/template";
|
||||||
|
import { Container, Heading, Text, Box, Flex, Card } from "@radix-ui/themes";
|
||||||
|
import {
|
||||||
|
BarChartIcon,
|
||||||
|
ReaderIcon,
|
||||||
|
ChatBubbleIcon,
|
||||||
|
PersonIcon,
|
||||||
|
EyeOpenIcon,
|
||||||
|
HeartIcon,
|
||||||
|
RocketIcon,
|
||||||
|
LayersIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
// 模拟统计数据
|
||||||
|
const stats = [
|
||||||
|
{
|
||||||
|
label: "文章总数",
|
||||||
|
value: "128",
|
||||||
|
icon: <ReaderIcon className="w-5 h-5" />,
|
||||||
|
trend: "+12%",
|
||||||
|
color: "var(--accent-9)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "总访问量",
|
||||||
|
value: "25,438",
|
||||||
|
icon: <EyeOpenIcon className="w-5 h-5" />,
|
||||||
|
trend: "+8.2%",
|
||||||
|
color: "var(--green-9)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "评论数",
|
||||||
|
value: "1,024",
|
||||||
|
icon: <ChatBubbleIcon className="w-5 h-5" />,
|
||||||
|
trend: "+5.4%",
|
||||||
|
color: "var(--blue-9)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "用户互动",
|
||||||
|
value: "3,842",
|
||||||
|
icon: <HeartIcon className="w-5 h-5" />,
|
||||||
|
trend: "+15.3%",
|
||||||
|
color: "var(--pink-9)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 模拟最近文章数据
|
||||||
|
const recentPosts = [
|
||||||
|
{
|
||||||
|
title: "构建现代化的前端开发工作流",
|
||||||
|
views: 1234,
|
||||||
|
comments: 23,
|
||||||
|
likes: 89,
|
||||||
|
status: "published",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "React 18 新特性详解",
|
||||||
|
views: 892,
|
||||||
|
comments: 15,
|
||||||
|
likes: 67,
|
||||||
|
status: "published",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "TypeScript 高级特性指南",
|
||||||
|
views: 756,
|
||||||
|
comments: 12,
|
||||||
|
likes: 45,
|
||||||
|
status: "draft",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "前端性能优化实践",
|
||||||
|
views: 645,
|
||||||
|
comments: 8,
|
||||||
|
likes: 34,
|
||||||
|
status: "published",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default new Template({}, ({ http, args }) => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* 页面标题 */}
|
||||||
|
<Heading size="6" className="mb-6 text-[--gray-12]">
|
||||||
|
仪表盘
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<Box className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||||
|
{stats.map((stat, index) => (
|
||||||
|
<Card key={index} className="p-4 hover-card border border-[--gray-6]">
|
||||||
|
<Flex justify="between" align="center">
|
||||||
|
<Box>
|
||||||
|
<Text className="text-[--gray-11] mb-1" size="2">
|
||||||
|
{stat.label}
|
||||||
|
</Text>
|
||||||
|
<Heading size="6" className="text-[--gray-12]">
|
||||||
|
{stat.value}
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-[--green-9]" size="2">
|
||||||
|
{stat.trend}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
className="w-10 h-10 rounded-full flex items-center justify-center"
|
||||||
|
style={{ backgroundColor: `color-mix(in srgb, ${stat.color} 15%, transparent)` }}
|
||||||
|
>
|
||||||
|
<Box style={{ color: stat.color }}>
|
||||||
|
{stat.icon}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 最近文章列表 */}
|
||||||
|
<Card className="w-full p-4 border border-[--gray-6] hover-card">
|
||||||
|
<Heading size="3" className="mb-4 text-[--gray-12]">
|
||||||
|
最近文章
|
||||||
|
</Heading>
|
||||||
|
<Box className="space-y-4">
|
||||||
|
{recentPosts.map((post, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
className="p-3 rounded-lg border border-[--gray-6] hover:border-[--accent-9] transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<Flex justify="between" align="start" gap="3">
|
||||||
|
<Box className="flex-1 min-w-0">
|
||||||
|
<Text className="text-[--gray-12] font-medium mb-2 truncate">
|
||||||
|
{post.title}
|
||||||
|
</Text>
|
||||||
|
<Flex gap="3">
|
||||||
|
<Flex align="center" gap="1">
|
||||||
|
<EyeOpenIcon className="w-3 h-3 text-[--gray-11]" />
|
||||||
|
<Text size="1" className="text-[--gray-11]">
|
||||||
|
{post.views}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex align="center" gap="1">
|
||||||
|
<ChatBubbleIcon className="w-3 h-3 text-[--gray-11]" />
|
||||||
|
<Text size="1" className="text-[--gray-11]">
|
||||||
|
{post.comments}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex align="center" gap="1">
|
||||||
|
<HeartIcon className="w-3 h-3 text-[--gray-11]" />
|
||||||
|
<Text size="1" className="text-[--gray-11]">
|
||||||
|
{post.likes}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
className={`px-2 py-1 rounded-full text-xs
|
||||||
|
${post.status === 'published'
|
||||||
|
? 'bg-[--green-3] text-[--green-11]'
|
||||||
|
: 'bg-[--gray-3] text-[--gray-11]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{post.status === 'published' ? '已发布' : '草稿'}
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
@ -9,9 +9,11 @@ import {
|
|||||||
DashboardIcon,
|
DashboardIcon,
|
||||||
GearIcon,
|
GearIcon,
|
||||||
FileTextIcon,
|
FileTextIcon,
|
||||||
ImageIcon,
|
|
||||||
ReaderIcon,
|
ReaderIcon,
|
||||||
LayersIcon,
|
LayersIcon,
|
||||||
|
FileIcon,
|
||||||
|
ColorWheelIcon,
|
||||||
|
HomeIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { Theme } from "@radix-ui/themes";
|
import { Theme } from "@radix-ui/themes";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
@ -30,11 +32,6 @@ const menuItems = [
|
|||||||
label: "文章管理",
|
label: "文章管理",
|
||||||
path: "/dashboard/posts",
|
path: "/dashboard/posts",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: <ImageIcon className="w-4 h-4" />,
|
|
||||||
label: "媒体管理",
|
|
||||||
path: "/dashboard/media",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: <ReaderIcon className="w-4 h-4" />,
|
icon: <ReaderIcon className="w-4 h-4" />,
|
||||||
label: "评论管理",
|
label: "评论管理",
|
||||||
@ -45,11 +42,31 @@ const menuItems = [
|
|||||||
label: "分类管理",
|
label: "分类管理",
|
||||||
path: "/dashboard/categories",
|
path: "/dashboard/categories",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: <FileIcon className="w-4 h-4" />,
|
||||||
|
label: "文件管理",
|
||||||
|
path: "/dashboard/files",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ColorWheelIcon className="w-4 h-4" />,
|
||||||
|
label: "主题管理",
|
||||||
|
path: "/dashboard/themes",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: <GearIcon className="w-4 h-4" />,
|
icon: <GearIcon className="w-4 h-4" />,
|
||||||
label: "系统设置",
|
label: "系统设置",
|
||||||
path: "/dashboard/settings",
|
path: "/dashboard/settings",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: <PersonIcon className="w-4 h-4" />,
|
||||||
|
label: "用户管理",
|
||||||
|
path: "/dashboard/users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <LayersIcon className="w-4 h-4" />,
|
||||||
|
label: "插件管理",
|
||||||
|
path: "/dashboard/plugins",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default new Layout(({ children }) => {
|
export default new Layout(({ children }) => {
|
||||||
@ -187,46 +204,36 @@ export default new Layout(({ children }) => {
|
|||||||
|
|
||||||
{/* 右侧用户菜单 */}
|
{/* 右侧用户菜单 */}
|
||||||
<Flex align="center" gap="4">
|
<Flex align="center" gap="4">
|
||||||
<Box className="flex items-center border-r border-[--gray-6] pr-4">
|
<Box className="flex items-center border-r border-[--gray-6] pr-4 [&_button]:w-10 [&_button]:h-10 [&_svg]:w-6 [&_svg]:h-6">
|
||||||
<ThemeModeToggle />
|
<ThemeModeToggle />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Root>
|
{/* 返回主页按钮 */}
|
||||||
<DropdownMenuPrimitive.Trigger asChild>
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="3"
|
size="3"
|
||||||
className="gap-2 text-base"
|
className="gap-2 text-base"
|
||||||
|
onClick={() => {
|
||||||
|
window.location.href = '/';
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PersonIcon className="w-5 h-5" />
|
<HomeIcon className="w-5 h-5" />
|
||||||
<span className="hidden sm:inline">管理员</span>
|
<span className="hidden sm:inline">返回主页</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuPrimitive.Trigger>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
{/* 退出登录按钮 */}
|
||||||
<DropdownMenuPrimitive.Content
|
<Button
|
||||||
align="end"
|
variant="ghost"
|
||||||
sideOffset={5}
|
size="3"
|
||||||
className="min-w-[180px] p-1 rounded-md bg-[--gray-1] border border-[--gray-6] shadow-lg"
|
className="gap-2 text-base"
|
||||||
>
|
onClick={() => {
|
||||||
<DropdownMenuPrimitive.Item
|
// 这里添加退出登录的逻辑
|
||||||
className="flex items-center gap-2 px-3 py-2 text-[--gray-12] hover:bg-[--gray-3] rounded outline-none cursor-pointer text-base"
|
console.log('退出登录');
|
||||||
>
|
}}
|
||||||
<GearIcon className="w-5 h-5" />
|
|
||||||
个人设置
|
|
||||||
</DropdownMenuPrimitive.Item>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Separator className="h-px my-1 bg-[--gray-6]" />
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
className="flex items-center gap-2 px-3 py-2 text-[--gray-12] hover:bg-[--gray-3] rounded outline-none cursor-pointer text-base"
|
|
||||||
>
|
>
|
||||||
<ExitIcon className="w-5 h-5" />
|
<ExitIcon className="w-5 h-5" />
|
||||||
退出登录
|
<span className="hidden sm:inline">退出登录</span>
|
||||||
</DropdownMenuPrimitive.Item>
|
</Button>
|
||||||
</DropdownMenuPrimitive.Content>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
</DropdownMenuPrimitive.Root>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -2,10 +2,11 @@ import "./styles/login.css";
|
|||||||
import { Template } from "interface/template";
|
import { Template } from "interface/template";
|
||||||
import { Container, Heading, Text, Box, Flex, Button } from "@radix-ui/themes";
|
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 } 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';
|
||||||
|
|
||||||
export default new Template({}, ({ http, args }) => {
|
export default new Template({}, ({ http, args }) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@ -14,6 +15,8 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { mode } = useThemeMode();
|
const { mode } = useThemeMode();
|
||||||
|
const [hasBackgroundError, setHasBackgroundError] = useState(false);
|
||||||
|
const notification = useNotification();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsVisible(true);
|
setIsVisible(true);
|
||||||
@ -58,20 +61,36 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
// 这里添加登录逻辑
|
// 这里添加登录逻辑
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500)); // 模拟API请求
|
await new Promise(resolve => setTimeout(resolve, 1500)); // 模拟API请求
|
||||||
|
|
||||||
|
// 登录成功的通知
|
||||||
|
notification.success('登录成功', '欢迎回来!');
|
||||||
|
|
||||||
// 登录成功后的处理
|
// 登录成功后的处理
|
||||||
console.log("Login successful");
|
console.log("Login successful");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 登录失败的通知
|
||||||
|
notification.error('登录失败', '用户名或密码错误');
|
||||||
console.error("Login failed:", error);
|
console.error("Login failed:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBackgroundError = () => {
|
||||||
|
console.log('Background failed to load, switching to fallback');
|
||||||
|
setHasBackgroundError(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用 useMemo 包裹背景组件
|
||||||
|
const backgroundComponent = useMemo(() => (
|
||||||
|
!hasBackgroundError && <AnimatedBackground onError={handleBackgroundError} />
|
||||||
|
), [hasBackgroundError]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="relative min-h-screen">
|
||||||
<AnimatedBackground />
|
{backgroundComponent}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
className="fixed top-4 right-4 z-10 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-color': 'var(--gray-12)',
|
||||||
'--button-hover-color': 'var(--accent-9)'
|
'--button-hover-color': 'var(--accent-9)'
|
||||||
@ -82,7 +101,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
|
|
||||||
<Container
|
<Container
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={`h-screen w-full flex items-center justify-center transition-all duration-300 ${
|
className={`relative z-10 h-screen w-full flex items-center justify-center transition-all duration-300 ${
|
||||||
isVisible ? "opacity-100" : "opacity-0"
|
isVisible ? "opacity-100" : "opacity-0"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -106,36 +125,32 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<form onSubmit={handleLogin}>
|
<form onSubmit={handleLogin}>
|
||||||
<Flex direction="column" gap="4">
|
<Flex direction="column" gap="4">
|
||||||
{/* 用户名输入框 */}
|
{/* 用户名输入框 */}
|
||||||
<Box className="form-element relative">
|
<Box className="form-element input-box relative">
|
||||||
<PersonIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4"
|
|
||||||
style={{ color: 'var(--gray-11)' }} />
|
|
||||||
<input
|
<input
|
||||||
className="login-input pl-10"
|
className="login-input"
|
||||||
placeholder="请输入用户名"
|
|
||||||
type="text"
|
type="text"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<label>用户名</label>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 密码输入框 */}
|
{/* 密码输入框 */}
|
||||||
<Box className="form-element relative">
|
<Box className="form-element input-box relative">
|
||||||
<LockClosedIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4"
|
|
||||||
style={{ color: 'var(--gray-11)' }} />
|
|
||||||
<input
|
<input
|
||||||
className="login-input pl-10"
|
className="login-input"
|
||||||
placeholder="请输入密码"
|
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<label>密码</label>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 登录按钮 */}
|
{/* 登录按钮 */}
|
||||||
<Button
|
<Button
|
||||||
className="login-button w-full h-10 transition-colors duration-300"
|
className="login-button w-full h-10 transition-colors duration-300 hover:bg-[--hover-bg]"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--accent-9)',
|
backgroundColor: 'var(--accent-9)',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
@ -152,7 +167,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<Flex justify="center" className="form-element">
|
<Flex justify="center" className="form-element">
|
||||||
<Text
|
<Text
|
||||||
size="2"
|
size="2"
|
||||||
className="cursor-pointer transition-colors duration-300"
|
className="cursor-pointer transition-colors duration-300 hover:text-[--hover-color]"
|
||||||
style={{
|
style={{
|
||||||
color: 'var(--gray-11)',
|
color: 'var(--gray-11)',
|
||||||
'--hover-color': 'var(--accent-9)'
|
'--hover-color': 'var(--accent-9)'
|
||||||
@ -166,6 +181,6 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
283
frontend/app/dashboard/plugins.tsx
Normal file
283
frontend/app/dashboard/plugins.tsx
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
import { Template } from "interface/template";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
DropdownMenu,
|
||||||
|
ScrollArea,
|
||||||
|
Dialog,
|
||||||
|
Tabs,
|
||||||
|
Switch,
|
||||||
|
IconButton
|
||||||
|
} from "@radix-ui/themes";
|
||||||
|
import {
|
||||||
|
PlusIcon,
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
GearIcon,
|
||||||
|
CodeIcon,
|
||||||
|
Cross2Icon,
|
||||||
|
CheckIcon,
|
||||||
|
UpdateIcon,
|
||||||
|
TrashIcon,
|
||||||
|
ExclamationTriangleIcon
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { PluginConfig } from "interface/plugin";
|
||||||
|
|
||||||
|
// 模拟插件数据
|
||||||
|
const mockPlugins: (PluginConfig & { id: number; preview?: string; installed?: boolean })[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "comment-system",
|
||||||
|
displayName: "评论系统",
|
||||||
|
version: "1.0.0",
|
||||||
|
description: "支持多种评论系统集成,包括Disqus、Gitalk等",
|
||||||
|
author: "Admin",
|
||||||
|
enabled: true,
|
||||||
|
icon: "https://api.iconify.design/material-symbols:comment.svg",
|
||||||
|
preview: "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format",
|
||||||
|
managePath: "/dashboard/plugins/comment-system",
|
||||||
|
installed: true,
|
||||||
|
configuration: {
|
||||||
|
system: {
|
||||||
|
title: "评论系统配置",
|
||||||
|
description: "配置评论系统参数",
|
||||||
|
data: {
|
||||||
|
provider: "gitalk",
|
||||||
|
clientId: "",
|
||||||
|
clientSecret: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
routes: new Set()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "image-optimization",
|
||||||
|
displayName: "图片优化",
|
||||||
|
version: "1.0.0",
|
||||||
|
description: "自动优化上传的图片,支持压缩、裁剪、水印等功能",
|
||||||
|
author: "ThirdParty",
|
||||||
|
enabled: false,
|
||||||
|
icon: "https://api.iconify.design/material-symbols:image.svg",
|
||||||
|
preview: "https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?w=500&auto=format",
|
||||||
|
installed: true,
|
||||||
|
configuration: {
|
||||||
|
system: {
|
||||||
|
title: "图片优化配置",
|
||||||
|
description: "配置图片优化参数",
|
||||||
|
data: {
|
||||||
|
quality: 80,
|
||||||
|
maxWidth: 1920,
|
||||||
|
watermark: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
routes: new Set()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 模拟市场插件数据
|
||||||
|
interface MarketPlugin {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
version: string;
|
||||||
|
description: string;
|
||||||
|
author: string;
|
||||||
|
preview?: string;
|
||||||
|
downloads: number;
|
||||||
|
rating: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const marketPlugins: MarketPlugin[] = [
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "image-optimization",
|
||||||
|
displayName: "图片优化",
|
||||||
|
version: "1.0.0",
|
||||||
|
description: "自动优化上传的图片,支持压缩、裁剪、水印等功能",
|
||||||
|
author: "ThirdParty",
|
||||||
|
preview: "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format",
|
||||||
|
downloads: 1200,
|
||||||
|
rating: 4.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: "markdown-plus",
|
||||||
|
displayName: "Markdown增强",
|
||||||
|
version: "2.0.0",
|
||||||
|
description: "增强的Markdown编辑器,支持更多扩展语法和实时预览",
|
||||||
|
author: "ThirdParty",
|
||||||
|
preview: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=500&auto=format",
|
||||||
|
downloads: 3500,
|
||||||
|
rating: 4.8
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default new Template({}, ({ http, args }) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
const [selectedPlugin, setSelectedPlugin] = useState<typeof mockPlugins[0] | null>(null);
|
||||||
|
|
||||||
|
// 处理插件启用/禁用
|
||||||
|
const handleTogglePlugin = (pluginId: number) => {
|
||||||
|
// 这里添加启用/禁用插件的逻辑
|
||||||
|
console.log('Toggle plugin:', pluginId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* 页面标题和操作栏 */}
|
||||||
|
<Flex justify="between" align="center" className="mb-6">
|
||||||
|
<Box>
|
||||||
|
<Heading size="6" className="text-[--gray-12] mb-2">
|
||||||
|
插件管理
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-[--gray-11]">
|
||||||
|
共 {mockPlugins.length} 个插件
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
className="bg-[--accent-9]"
|
||||||
|
onClick={() => setIsAddDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<PlusIcon className="w-4 h-4" />
|
||||||
|
安装插件
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 搜索栏 */}
|
||||||
|
<Box className="w-full sm:w-64 mb-6">
|
||||||
|
<TextField.Root
|
||||||
|
placeholder="搜索插件..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
||||||
|
>
|
||||||
|
<TextField.Slot>
|
||||||
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
|
</TextField.Slot>
|
||||||
|
</TextField.Root>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 插件列表 */}
|
||||||
|
<Box className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{mockPlugins.map((plugin) => (
|
||||||
|
<Card key={plugin.id} className="p-4 border border-[--gray-6] hover-card">
|
||||||
|
{/* 插件预览图 */}
|
||||||
|
{plugin.preview && (
|
||||||
|
<Box className="aspect-video mb-4 rounded-lg overflow-hidden bg-[--gray-3]">
|
||||||
|
<img
|
||||||
|
src={plugin.preview}
|
||||||
|
alt={plugin.displayName}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 插件信息 */}
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
<Flex justify="between" align="center">
|
||||||
|
<Heading size="3">{plugin.displayName}</Heading>
|
||||||
|
<Switch
|
||||||
|
checked={plugin.enabled}
|
||||||
|
onCheckedChange={() => handleTogglePlugin(plugin.id)}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Text size="1" className="text-[--gray-11]">
|
||||||
|
版本 {plugin.version} · 作者 {plugin.author}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text size="2" className="text-[--gray-11] line-clamp-2">
|
||||||
|
{plugin.description}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<Flex gap="2" mt="2">
|
||||||
|
{plugin.managePath && plugin.enabled && (
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => {
|
||||||
|
if(plugin.managePath) {
|
||||||
|
window.location.href = plugin.managePath;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GearIcon className="w-4 h-4" />
|
||||||
|
配置
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
color="red"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<TrashIcon className="w-4 h-4" />
|
||||||
|
卸载
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 安装插件对话框 */}
|
||||||
|
<Dialog.Root open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||||
|
<Dialog.Content style={{ maxWidth: 500 }}>
|
||||||
|
<Dialog.Title>安装插件</Dialog.Title>
|
||||||
|
<Dialog.Description size="2" mb="4">
|
||||||
|
上传插件包进行安装
|
||||||
|
</Dialog.Description>
|
||||||
|
|
||||||
|
<Box className="mt-4">
|
||||||
|
<Box className="border-2 border-dashed border-[--gray-6] rounded-lg p-8 text-center">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
id="plugin-upload"
|
||||||
|
accept=".zip"
|
||||||
|
onChange={(e) => {
|
||||||
|
console.log(e.target.files);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="plugin-upload"
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<CodeIcon className="w-12 h-12 mx-auto mb-4 text-[--gray-9]" />
|
||||||
|
<Text className="text-[--gray-11] mb-2">
|
||||||
|
点击上传插件包或拖拽到此处
|
||||||
|
</Text>
|
||||||
|
<Text size="1" className="text-[--gray-10]">
|
||||||
|
支持 .zip 格式的插件包
|
||||||
|
</Text>
|
||||||
|
</label>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Flex gap="3" mt="4" justify="end">
|
||||||
|
<Dialog.Close>
|
||||||
|
<Button variant="soft" color="gray">
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Dialog.Close>
|
||||||
|
<Dialog.Close>
|
||||||
|
<Button className="bg-[--accent-9]">
|
||||||
|
开始安装
|
||||||
|
</Button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</Flex>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
247
frontend/app/dashboard/posts.tsx
Normal file
247
frontend/app/dashboard/posts.tsx
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
import { Template } from "interface/template";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
DropdownMenu,
|
||||||
|
ScrollArea,
|
||||||
|
DataList,
|
||||||
|
Badge
|
||||||
|
} from "@radix-ui/themes";
|
||||||
|
import {
|
||||||
|
PlusIcon,
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
DotsHorizontalIcon,
|
||||||
|
Pencil1Icon,
|
||||||
|
TrashIcon,
|
||||||
|
EyeOpenIcon,
|
||||||
|
ReaderIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { PostDisplay } from "interface/fields";
|
||||||
|
|
||||||
|
// 模拟文章数据
|
||||||
|
const mockPosts: PostDisplay[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "构建现代化的前端开发工作流",
|
||||||
|
content: "在现代前端开发中...",
|
||||||
|
authorName: "张三",
|
||||||
|
publishedAt: new Date("2024-03-15"),
|
||||||
|
status: "published",
|
||||||
|
isEditor: false,
|
||||||
|
createdAt: new Date("2024-03-15"),
|
||||||
|
updatedAt: new Date("2024-03-15"),
|
||||||
|
metaKeywords: "",
|
||||||
|
metaDescription: "",
|
||||||
|
categories: [{ name: "前端开发" }],
|
||||||
|
tags: [{ name: "工程化" }, { name: "效率提升" }]
|
||||||
|
},
|
||||||
|
// ... 可以添加更多模拟数据
|
||||||
|
];
|
||||||
|
|
||||||
|
export default new Template({}, ({ http, args }) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [selectedStatus, setSelectedStatus] = useState<string>("all");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* 页面标题和操作栏 */}
|
||||||
|
<Flex justify="between" align="center" className="mb-6">
|
||||||
|
<Heading size="6" className="text-[--gray-12]">
|
||||||
|
文章管理
|
||||||
|
</Heading>
|
||||||
|
<Button className="bg-[--accent-9]">
|
||||||
|
<PlusIcon className="w-4 h-4" />
|
||||||
|
新建文章
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 搜索和筛选栏 */}
|
||||||
|
<Flex
|
||||||
|
gap="4"
|
||||||
|
className="mb-6 flex-col sm:flex-row" // 移动端垂直布局,桌面端水平布局
|
||||||
|
>
|
||||||
|
<Box className="w-full sm:w-64">
|
||||||
|
<TextField.Root
|
||||||
|
placeholder="搜索文章..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
||||||
|
>
|
||||||
|
<TextField.Slot side="right">
|
||||||
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
|
</TextField.Slot>
|
||||||
|
</TextField.Root>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<Button variant="surface">
|
||||||
|
状态: {selectedStatus === 'all' ? '全部' : selectedStatus}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Item onClick={() => setSelectedStatus('all')}>
|
||||||
|
全部
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onClick={() => setSelectedStatus('published')}>
|
||||||
|
已发布
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onClick={() => setSelectedStatus('draft')}>
|
||||||
|
草稿
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 文章列表 */}
|
||||||
|
<Box className="border border-[--gray-6] rounded-lg overflow-hidden">
|
||||||
|
<ScrollArea className="w-full">
|
||||||
|
{/* 桌面端表格视图 */}
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<Table.Root>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeaderCell>标题</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>作者</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>分类</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>状态</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>发布时间</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>操作</Table.ColumnHeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{mockPosts.map((post) => (
|
||||||
|
<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">
|
||||||
|
{post.title}
|
||||||
|
</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">
|
||||||
|
{post.authorName}
|
||||||
|
</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">
|
||||||
|
<Flex gap="2" className="inline-flex">
|
||||||
|
{post.categories?.map((category) => (
|
||||||
|
<Text
|
||||||
|
key={category.name}
|
||||||
|
size="1"
|
||||||
|
className="px-2 py-0.5 bg-[--gray-4] rounded"
|
||||||
|
>
|
||||||
|
{category.name}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</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">
|
||||||
|
<Flex gap="2">
|
||||||
|
{post.status === 'published' ? (
|
||||||
|
<Badge color="green">已发布</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge color="orange">草稿</Badge>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</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">
|
||||||
|
{post.publishedAt?.toLocaleDateString()}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell className="block sm:table-cell py-2 sm:py-3 border-b sm:border-b-0 before:content-['操作:'] before:inline-block before:w-20 before:font-normal sm:before:content-none">
|
||||||
|
<Flex gap="2">
|
||||||
|
<Button variant="ghost" size="1">
|
||||||
|
<Pencil1Icon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="1">
|
||||||
|
<EyeOpenIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="1" color="red">
|
||||||
|
<TrashIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 移动端列表视图 */}
|
||||||
|
<div className="block sm:hidden">
|
||||||
|
{mockPosts.map((post) => (
|
||||||
|
<DataList.Root key={post.id} className="p-4 border-b border-[--gray-6] last:border-b-0">
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">标题</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Text weight="medium">{post.title}</Text>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">作者</DataList.Label>
|
||||||
|
<DataList.Value>{post.authorName}</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">分类</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex gap="2">
|
||||||
|
{post.categories?.map((category) => (
|
||||||
|
<Text
|
||||||
|
key={category.name}
|
||||||
|
size="1"
|
||||||
|
className="px-2 py-0.5 bg-[--gray-4] rounded"
|
||||||
|
>
|
||||||
|
{category.name}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">状态</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex gap="2">
|
||||||
|
{post.status === 'published' ? (
|
||||||
|
<Badge color="green">已发布</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge color="orange">草稿</Badge>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">发布时间</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
{post.publishedAt?.toLocaleDateString()}
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label minWidth="88px">操作</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex gap="2">
|
||||||
|
<Button variant="ghost" size="1">
|
||||||
|
<Pencil1Icon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="1">
|
||||||
|
<EyeOpenIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="1" color="red">
|
||||||
|
<TrashIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
</DataList.Root>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
230
frontend/app/dashboard/settings.tsx
Normal file
230
frontend/app/dashboard/settings.tsx
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import { Template } from "interface/template";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
Switch,
|
||||||
|
Tabs,
|
||||||
|
TextArea
|
||||||
|
} from "@radix-ui/themes";
|
||||||
|
import {
|
||||||
|
GearIcon,
|
||||||
|
PersonIcon,
|
||||||
|
LockClosedIcon,
|
||||||
|
BellIcon,
|
||||||
|
GlobeIcon
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default new Template({}, ({ http, args }) => {
|
||||||
|
const [siteName, setSiteName] = useState("我的博客");
|
||||||
|
const [siteDescription, setSiteDescription] = useState("一个优雅的博客系统");
|
||||||
|
const [emailNotifications, setEmailNotifications] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Heading size="6" className="text-[--gray-12] mb-6">
|
||||||
|
系统设置
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Tabs.Root defaultValue="general">
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Trigger value="general">
|
||||||
|
<GearIcon className="w-4 h-4 mr-2" />
|
||||||
|
常规设置
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="profile">
|
||||||
|
<PersonIcon className="w-4 h-4 mr-2" />
|
||||||
|
个人资料
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="security">
|
||||||
|
<LockClosedIcon className="w-4 h-4 mr-2" />
|
||||||
|
安全设置
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="notifications">
|
||||||
|
<BellIcon className="w-4 h-4 mr-2" />
|
||||||
|
通知设置
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
{/* 常规设置 */}
|
||||||
|
<Tabs.Content value="general">
|
||||||
|
<Card className="mt-6 p-6 border border-[--gray-6]">
|
||||||
|
<Flex direction="column" gap="4">
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
站点名称
|
||||||
|
</Text>
|
||||||
|
<TextField.Root
|
||||||
|
value={siteName}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSiteName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
站点描述
|
||||||
|
</Text>
|
||||||
|
<TextArea
|
||||||
|
value={siteDescription}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setSiteDescription(e.target.value)}
|
||||||
|
className="min-h-[100px]"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
站点语言
|
||||||
|
</Text>
|
||||||
|
<select
|
||||||
|
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="en-US">English</option>
|
||||||
|
</select>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
时区设置
|
||||||
|
</Text>
|
||||||
|
<select
|
||||||
|
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+0">UTC+0 格林威治时间</option>
|
||||||
|
</select>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
{/* 个人资料 */}
|
||||||
|
<Tabs.Content value="profile">
|
||||||
|
<Card className="mt-6 p-6 border border-[--gray-6]">
|
||||||
|
<Flex direction="column" gap="4">
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
头像
|
||||||
|
</Text>
|
||||||
|
<Flex align="center" gap="4">
|
||||||
|
<Box className="w-20 h-20 rounded-full bg-[--gray-3] flex items-center justify-center">
|
||||||
|
<PersonIcon className="w-8 h-8 text-[--gray-9]" />
|
||||||
|
</Box>
|
||||||
|
<Button variant="soft">更换头像</Button>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
用户名
|
||||||
|
</Text>
|
||||||
|
<TextField.Root defaultValue="admin" />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
邮箱
|
||||||
|
</Text>
|
||||||
|
<TextField.Root defaultValue="admin@example.com" />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
个人简介
|
||||||
|
</Text>
|
||||||
|
<TextArea
|
||||||
|
placeholder="介绍一下自己..."
|
||||||
|
className="min-h-[100px]"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
{/* 安全设置 */}
|
||||||
|
<Tabs.Content value="security">
|
||||||
|
<Card className="mt-6 p-6 border border-[--gray-6]">
|
||||||
|
<Flex direction="column" gap="4">
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
修改密码
|
||||||
|
</Text>
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
<TextField.Root
|
||||||
|
type="password"
|
||||||
|
placeholder="当前密码"
|
||||||
|
/>
|
||||||
|
<TextField.Root
|
||||||
|
type="password"
|
||||||
|
placeholder="新密码"
|
||||||
|
/>
|
||||||
|
<TextField.Root
|
||||||
|
type="password"
|
||||||
|
placeholder="确认新密码"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
两步验证
|
||||||
|
</Text>
|
||||||
|
<Flex align="center" gap="4">
|
||||||
|
<Switch defaultChecked />
|
||||||
|
<Text>启用两步验证</Text>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
{/* 通知设置 */}
|
||||||
|
<Tabs.Content value="notifications">
|
||||||
|
<Card className="mt-6 p-6 border border-[--gray-6]">
|
||||||
|
<Flex direction="column" gap="4">
|
||||||
|
<Box>
|
||||||
|
<Flex justify="between" align="center">
|
||||||
|
<Box>
|
||||||
|
<Text weight="bold">邮件通知</Text>
|
||||||
|
<Text size="1" className="text-[--gray-11]">
|
||||||
|
接收新评论和系统通知的邮件提醒
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Switch
|
||||||
|
checked={emailNotifications}
|
||||||
|
onCheckedChange={setEmailNotifications}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Flex justify="between" align="center">
|
||||||
|
<Box>
|
||||||
|
<Text weight="bold">浏览器通知</Text>
|
||||||
|
<Text size="1" className="text-[--gray-11]">
|
||||||
|
在浏览器中接收实时通知
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
|
|
||||||
|
{/* 保存按钮 */}
|
||||||
|
<Flex justify="end" className="mt-6">
|
||||||
|
<Button className="bg-[--accent-9]">
|
||||||
|
保存更改
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
@ -1,53 +1,63 @@
|
|||||||
.login-input {
|
.login-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40px;
|
padding: 10px 0;
|
||||||
padding: 8px 12px;
|
font-size: 16px;
|
||||||
border: 1px solid var(--gray-6);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
color: var(--gray-12);
|
color: var(--gray-12);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--gray-8);
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: all 0.2s;
|
background: transparent;
|
||||||
backdrop-filter: blur(8px);
|
transition: .5s;
|
||||||
|
caret-color: var(--accent-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 黑暗模式下的输入框样式 */
|
/* 输入框的标签动画 */
|
||||||
|
.input-box {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box label {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 10px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--gray-11);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: .5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box input:focus ~ label,
|
||||||
|
.input-box input:valid ~ label {
|
||||||
|
top: -20px;
|
||||||
|
left: 0;
|
||||||
|
color: var(--accent-9);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框底部边框动画 */
|
||||||
|
.input-box input:focus,
|
||||||
|
.input-box input:valid {
|
||||||
|
border-bottom: 2px solid var(--accent-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式适配 */
|
||||||
:root[class~="dark"] .login-input {
|
:root[class~="dark"] .login-input {
|
||||||
background: rgba(0, 0, 0, 0.2);
|
color: var(--gray-11);
|
||||||
border-color: var(--gray-7);
|
border-bottom-color: var(--gray-7);
|
||||||
backdrop-filter: blur(8px);
|
caret-color: var(--accent-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-input:hover {
|
:root[class~="dark"] .input-box label {
|
||||||
border-color: var(--gray-7);
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[class~="dark"] .login-input:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
border-color: var(--gray-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-input:focus {
|
|
||||||
border-color: var(--accent-8);
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
box-shadow: 0 0 0 1px var(--accent-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[class~="dark"] .login-input:focus {
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
border-color: var(--accent-8);
|
|
||||||
box-shadow: 0 0 0 1px var(--accent-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-input::placeholder {
|
|
||||||
color: var(--gray-9);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[class~="dark"] .login-input::placeholder {
|
|
||||||
color: var(--gray-8);
|
color: var(--gray-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[class~="dark"] .input-box input:focus ~ label,
|
||||||
|
:root[class~="dark"] .input-box input:valid ~ label {
|
||||||
|
color: var(--accent-9);
|
||||||
|
}
|
||||||
|
|
||||||
.login-button {
|
.login-button {
|
||||||
background-color: var(--accent-9);
|
background-color: var(--accent-9);
|
||||||
color: white;
|
color: white;
|
||||||
|
230
frontend/app/dashboard/themes.tsx
Normal file
230
frontend/app/dashboard/themes.tsx
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import { Template } from "interface/template";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
ScrollArea,
|
||||||
|
Dialog,
|
||||||
|
Tabs,
|
||||||
|
} from "@radix-ui/themes";
|
||||||
|
import {
|
||||||
|
PlusIcon,
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
CodeIcon,
|
||||||
|
CheckIcon,
|
||||||
|
Cross2Icon,
|
||||||
|
DownloadIcon,
|
||||||
|
GearIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { ThemeConfig } from "interface/theme";
|
||||||
|
|
||||||
|
// 模拟主题数据
|
||||||
|
const mockThemes: (ThemeConfig & { id: number; preview: string; active: boolean })[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "echoes",
|
||||||
|
displayName: "Echoes",
|
||||||
|
version: "1.0.0",
|
||||||
|
description: "默认主题",
|
||||||
|
author: "Admin",
|
||||||
|
preview: "https://images.unsplash.com/photo-1481487196290-c152efe083f5?w=500&auto=format",
|
||||||
|
templates: new Map(),
|
||||||
|
configuration: {
|
||||||
|
theme: {
|
||||||
|
title: "主题配置",
|
||||||
|
description: "Echoes主题配置项",
|
||||||
|
data: {
|
||||||
|
colors: {
|
||||||
|
mode: "light",
|
||||||
|
layout: "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
routes: new Map(),
|
||||||
|
active: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "minimal",
|
||||||
|
displayName: "Minimal",
|
||||||
|
version: "1.0.0",
|
||||||
|
description: "简约风格主题",
|
||||||
|
author: "Admin",
|
||||||
|
preview: "https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?w=500&auto=format",
|
||||||
|
templates: new Map(),
|
||||||
|
configuration: {
|
||||||
|
theme: {
|
||||||
|
title: "主题配置",
|
||||||
|
description: "Echoes主题配置项",
|
||||||
|
data: {
|
||||||
|
colors: {
|
||||||
|
mode: "light",
|
||||||
|
layout: "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
routes: new Map(),
|
||||||
|
active: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default new Template({}, ({ http, args }) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
|
const [selectedTheme, setSelectedTheme] = useState<typeof mockThemes[0] | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* 页面标题和操作栏 */}
|
||||||
|
<Flex justify="between" align="center" className="mb-6">
|
||||||
|
<Box>
|
||||||
|
<Heading size="6" className="text-[--gray-12] mb-2">
|
||||||
|
主题管理
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-[--gray-11]">
|
||||||
|
共 {mockThemes.length} 个主题
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
className="bg-[--accent-9]"
|
||||||
|
onClick={() => setIsAddDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<PlusIcon className="w-4 h-4" />
|
||||||
|
安装主题
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 搜索栏 */}
|
||||||
|
<Box className="w-full sm:w-64 mb-6">
|
||||||
|
<TextField.Root placeholder="搜索主题...">
|
||||||
|
<TextField.Slot>
|
||||||
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
|
</TextField.Slot>
|
||||||
|
</TextField.Root>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 主题列表 */}
|
||||||
|
<Box className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{mockThemes.map((theme) => (
|
||||||
|
<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]">
|
||||||
|
<img
|
||||||
|
src={theme.preview}
|
||||||
|
alt={theme.displayName}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 主题信息 */}
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
<Flex justify="between" align="center">
|
||||||
|
<Heading size="3">{theme.displayName}</Heading>
|
||||||
|
{theme.active && (
|
||||||
|
<Text size="1" className="px-2 py-1 bg-[--accent-3] text-[--accent-9] rounded">
|
||||||
|
当前使用
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Text size="1" className="text-[--gray-11]">
|
||||||
|
版本 {theme.version} · 作者 {theme.author}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text size="2" className="text-[--gray-11] line-clamp-2">
|
||||||
|
{theme.description}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<Flex gap="2" mt="2">
|
||||||
|
{theme.active ? (
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => window.location.href = `/dashboard/themes/${theme.name}/settings`}
|
||||||
|
>
|
||||||
|
<GearIcon className="w-4 h-4" />
|
||||||
|
配置
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
className="flex-1 bg-[--accent-9]"
|
||||||
|
>
|
||||||
|
<CheckIcon className="w-4 h-4" />
|
||||||
|
启用
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
color="red"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => {
|
||||||
|
// 这里添加卸载主题的处理逻辑
|
||||||
|
console.log('卸载主题:', theme.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Cross2Icon className="w-4 h-4" />
|
||||||
|
卸载
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 安装主题对话框 */}
|
||||||
|
<Dialog.Root open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||||
|
<Dialog.Content style={{ maxWidth: 500 }}>
|
||||||
|
<Dialog.Title>安装主题</Dialog.Title>
|
||||||
|
<Dialog.Description size="2" mb="4">
|
||||||
|
请上传主题包进行安装
|
||||||
|
</Dialog.Description>
|
||||||
|
|
||||||
|
<Box className="mt-4">
|
||||||
|
<Box className="border-2 border-dashed border-[--gray-6] rounded-lg p-8 text-center">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
id="theme-upload"
|
||||||
|
accept=".zip"
|
||||||
|
onChange={(e) => {
|
||||||
|
console.log(e.target.files);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="theme-upload"
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<CodeIcon className="w-12 h-12 mx-auto mb-4 text-[--gray-9]" />
|
||||||
|
<Text className="text-[--gray-11] mb-2">
|
||||||
|
点击上传主题包或拖拽到此处
|
||||||
|
</Text>
|
||||||
|
<Text size="1" className="text-[--gray-10]">
|
||||||
|
支持 .zip 格式的主题包
|
||||||
|
</Text>
|
||||||
|
</label>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Flex gap="3" mt="4" justify="end">
|
||||||
|
<Dialog.Close>
|
||||||
|
<Button variant="soft" color="gray">
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</Flex>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
342
frontend/app/dashboard/users.tsx
Normal file
342
frontend/app/dashboard/users.tsx
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
import { Template } from "interface/template";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
ScrollArea,
|
||||||
|
Dialog,
|
||||||
|
Avatar,
|
||||||
|
DropdownMenu,
|
||||||
|
Badge
|
||||||
|
} from "@radix-ui/themes";
|
||||||
|
import {
|
||||||
|
PlusIcon,
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
Pencil1Icon,
|
||||||
|
TrashIcon,
|
||||||
|
PersonIcon,
|
||||||
|
DotsHorizontalIcon,
|
||||||
|
LockClosedIcon,
|
||||||
|
ExclamationTriangleIcon
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { User } from "interface/fields";
|
||||||
|
|
||||||
|
// 模拟用户数据
|
||||||
|
const mockUsers: (User & { id: number })[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
username: "admin",
|
||||||
|
email: "admin@example.com",
|
||||||
|
avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=1",
|
||||||
|
role: "admin",
|
||||||
|
createdAt: new Date("2024-01-01"),
|
||||||
|
updatedAt: new Date("2024-03-15"),
|
||||||
|
lastLoginAt: new Date("2024-03-15"),
|
||||||
|
passwordHash: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
username: "editor",
|
||||||
|
email: "editor@example.com",
|
||||||
|
avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
|
||||||
|
role: "editor",
|
||||||
|
createdAt: new Date("2024-02-01"),
|
||||||
|
updatedAt: new Date("2024-03-14"),
|
||||||
|
lastLoginAt: new Date("2024-03-14"),
|
||||||
|
passwordHash: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
username: "user",
|
||||||
|
email: "user@example.com",
|
||||||
|
avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=3",
|
||||||
|
role: "user",
|
||||||
|
createdAt: new Date("2024-03-01"),
|
||||||
|
updatedAt: new Date("2024-03-13"),
|
||||||
|
lastLoginAt: new Date("2024-03-13"),
|
||||||
|
passwordHash: "",
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default new Template({}, ({ http, args }) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||||
|
const [selectedUser, setSelectedUser] = useState<typeof mockUsers[0] | null>(null);
|
||||||
|
const [newUserData, setNewUserData] = useState({
|
||||||
|
username: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
role: "user"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取角色标签样式
|
||||||
|
const getRoleBadgeColor = (role: string) => {
|
||||||
|
switch(role) {
|
||||||
|
case 'admin':
|
||||||
|
return 'red';
|
||||||
|
case 'editor':
|
||||||
|
return 'blue';
|
||||||
|
default:
|
||||||
|
return 'gray';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* 页面标题和操作栏 */}
|
||||||
|
<Flex justify="between" align="center" className="mb-6">
|
||||||
|
<Box>
|
||||||
|
<Heading size="6" className="text-[--gray-12] mb-2">
|
||||||
|
用户管理
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-[--gray-11]">
|
||||||
|
共 {mockUsers.length} 个用户
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
className="bg-[--accent-9]"
|
||||||
|
onClick={() => setIsAddDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<PlusIcon className="w-4 h-4" />
|
||||||
|
新建用户
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 搜索栏 */}
|
||||||
|
<Box className="w-full sm:w-64 mb-6">
|
||||||
|
<TextField.Root
|
||||||
|
placeholder="搜索用户..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
||||||
|
>
|
||||||
|
<TextField.Slot>
|
||||||
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
|
</TextField.Slot>
|
||||||
|
</TextField.Root>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 用户列表 */}
|
||||||
|
<Box className="border border-[--gray-6] rounded-lg overflow-hidden">
|
||||||
|
<ScrollArea>
|
||||||
|
<Table.Root>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeaderCell>用户</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>邮箱</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>角色</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>注册时间</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>最后登录</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>操作</Table.ColumnHeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
|
||||||
|
<Table.Body>
|
||||||
|
{mockUsers.map((user) => (
|
||||||
|
<Table.Row key={user.id}>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Avatar
|
||||||
|
src={user.avatarUrl}
|
||||||
|
fallback={user.username[0].toUpperCase()}
|
||||||
|
size="2"
|
||||||
|
radius="full"
|
||||||
|
/>
|
||||||
|
<Text weight="medium">{user.username}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>{user.email}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Badge color={getRoleBadgeColor(user.role)}>
|
||||||
|
{user.role}
|
||||||
|
</Badge>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{user.createdAt.toLocaleDateString()}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{user.lastLoginAt?.toLocaleDateString()}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex gap="2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="1"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedUser(user);
|
||||||
|
setIsEditDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil1Icon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="1" color="red">
|
||||||
|
<TrashIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 新建用户对话框 */}
|
||||||
|
<Dialog.Root open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||||
|
<Dialog.Content style={{ maxWidth: 450 }}>
|
||||||
|
<Dialog.Title>新建用户</Dialog.Title>
|
||||||
|
<Dialog.Description size="2" mb="4">
|
||||||
|
创建一个新的用户账号
|
||||||
|
</Dialog.Description>
|
||||||
|
|
||||||
|
<Flex direction="column" gap="4">
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
用户名
|
||||||
|
</Text>
|
||||||
|
<TextField.Root
|
||||||
|
placeholder="输入用户名"
|
||||||
|
value={newUserData.username}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setNewUserData({...newUserData, username: e.target.value})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
邮箱
|
||||||
|
</Text>
|
||||||
|
<TextField.Root
|
||||||
|
placeholder="输入邮箱"
|
||||||
|
value={newUserData.email}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setNewUserData({...newUserData, email: e.target.value})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
密码
|
||||||
|
</Text>
|
||||||
|
<TextField.Root
|
||||||
|
type="password"
|
||||||
|
placeholder="输入密码"
|
||||||
|
value={newUserData.password}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setNewUserData({...newUserData, password: e.target.value})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
角色
|
||||||
|
</Text>
|
||||||
|
<select
|
||||||
|
className="w-full h-9 px-3 rounded-md bg-[--gray-1] border border-[--gray-6] text-[--gray-12]"
|
||||||
|
value={newUserData.role}
|
||||||
|
onChange={(e) => setNewUserData({...newUserData, role: e.target.value})}
|
||||||
|
>
|
||||||
|
<option value="user">普通用户</option>
|
||||||
|
<option value="editor">编辑</option>
|
||||||
|
<option value="admin">管理员</option>
|
||||||
|
</select>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap="3" mt="4" justify="end">
|
||||||
|
<Dialog.Close>
|
||||||
|
<Button variant="soft" color="gray">
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Dialog.Close>
|
||||||
|
<Dialog.Close>
|
||||||
|
<Button className="bg-[--accent-9]">
|
||||||
|
创建
|
||||||
|
</Button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</Flex>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
||||||
|
{/* 编辑用户对话框 */}
|
||||||
|
<Dialog.Root open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||||||
|
<Dialog.Content style={{ maxWidth: 450 }}>
|
||||||
|
{selectedUser && (
|
||||||
|
<>
|
||||||
|
<Dialog.Title>编辑用户</Dialog.Title>
|
||||||
|
<Dialog.Description size="2" mb="4">
|
||||||
|
修改用户信息
|
||||||
|
</Dialog.Description>
|
||||||
|
|
||||||
|
<Flex direction="column" gap="4">
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
用户名
|
||||||
|
</Text>
|
||||||
|
<TextField.Root
|
||||||
|
defaultValue={selectedUser.username}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
邮箱
|
||||||
|
</Text>
|
||||||
|
<TextField.Root
|
||||||
|
defaultValue={selectedUser.email}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
角色
|
||||||
|
</Text>
|
||||||
|
<select
|
||||||
|
className="w-full h-9 px-3 rounded-md bg-[--gray-1] border border-[--gray-6] text-[--gray-12]"
|
||||||
|
defaultValue={selectedUser.role}
|
||||||
|
>
|
||||||
|
<option value="user">普通用户</option>
|
||||||
|
<option value="editor">编辑</option>
|
||||||
|
<option value="admin">管理员</option>
|
||||||
|
</select>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
||||||
|
重置密码
|
||||||
|
</Text>
|
||||||
|
<TextField.Root
|
||||||
|
type="password"
|
||||||
|
placeholder="留空则不修改"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap="3" mt="4" justify="end">
|
||||||
|
<Dialog.Close>
|
||||||
|
<Button variant="soft" color="gray">
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Dialog.Close>
|
||||||
|
<Dialog.Close>
|
||||||
|
<Button className="bg-[--accent-9]">
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
@ -7,11 +7,20 @@ import post from "themes/echoes/post";
|
|||||||
import { memo, useCallback } from "react";
|
import { memo, useCallback } from "react";
|
||||||
import login from "~/dashboard/login";
|
import login from "~/dashboard/login";
|
||||||
import adminLayout from "~/dashboard/layout";
|
import adminLayout from "~/dashboard/layout";
|
||||||
|
import dashboard from "~/dashboard/index";
|
||||||
|
import posts from "~/dashboard/posts";
|
||||||
|
import comments from "~/dashboard/comments";
|
||||||
|
import categories from "./dashboard/categories";
|
||||||
|
import settings from "./dashboard/settings";
|
||||||
|
import files from "./dashboard/files";
|
||||||
|
import themes from "./dashboard/themes";
|
||||||
|
import users from "~/dashboard/users";
|
||||||
|
import plugins from "./dashboard/plugins";
|
||||||
|
|
||||||
const args = {
|
const args = {
|
||||||
title: "我的页面",
|
title: "我的页面",
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
nav: '<a href="/">index</a><a href="/error">error</a><a href="/about">about</a><a href="/post">post</a>',
|
nav: '<a href="/">index</a><a href="/error">error</a><a href="/about">about</a><a href="/post">post</a><a href="/login">login</a><a href="/dashboard">dashboard</a>',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// 创建布局渲染器的工厂函数
|
// 创建布局渲染器的工厂函数
|
||||||
@ -26,11 +35,11 @@ const createLayoutRenderer = (layoutComponent: any) => {
|
|||||||
|
|
||||||
// 使用工厂函数创建不同的布局渲染器
|
// 使用工厂函数创建不同的布局渲染器
|
||||||
const renderLayout = createLayoutRenderer(layout);
|
const renderLayout = createLayoutRenderer(layout);
|
||||||
const renderAdminLayout = createLayoutRenderer(adminLayout);
|
const renderDashboardLayout = createLayoutRenderer(adminLayout);
|
||||||
|
|
||||||
const Routes = memo(() => {
|
const Routes = memo(() => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const path = location.pathname.split("/")[1];
|
const [mainPath, subPath] = location.pathname.split("/").filter(Boolean);
|
||||||
|
|
||||||
// 使用 useCallback 缓存渲染函数
|
// 使用 useCallback 缓存渲染函数
|
||||||
const renderContent = useCallback((Component: any) => {
|
const renderContent = useCallback((Component: any) => {
|
||||||
@ -38,55 +47,50 @@ const Routes = memo(() => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 添加管理后台内容渲染函数
|
// 添加管理后台内容渲染函数
|
||||||
const renderAdminContent = useCallback((Component: any) => {
|
const renderDashboardContent = useCallback((Component: any) => {
|
||||||
return renderAdminLayout(Component.render(args));
|
return renderDashboardLayout(Component.render(args));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 根据路径返回对应组件
|
// 前台路由
|
||||||
if (path === "error") {
|
switch (mainPath) {
|
||||||
|
case "error":
|
||||||
return renderContent(ErrorPage);
|
return renderContent(ErrorPage);
|
||||||
}
|
case "about":
|
||||||
|
|
||||||
if (path === "about") {
|
|
||||||
return renderContent(about);
|
return renderContent(about);
|
||||||
}
|
case "post":
|
||||||
|
|
||||||
if (path === "post") {
|
|
||||||
return renderContent(post);
|
return renderContent(post);
|
||||||
}
|
case "login":
|
||||||
|
|
||||||
if (path === "login") {
|
|
||||||
return login.render(args);
|
return login.render(args);
|
||||||
}
|
case "dashboard":
|
||||||
|
// 管理后台路由
|
||||||
// 添加管理后台路由判断
|
|
||||||
if (path === "admin") {
|
|
||||||
// 这里可以根据实际需要添加不同的管理页面组件
|
|
||||||
const subPath = location.pathname.split("/")[2];
|
|
||||||
|
|
||||||
// 如果没有子路径,显示仪表盘
|
|
||||||
if (!subPath) {
|
if (!subPath) {
|
||||||
return renderAdminLayout(<div>仪表盘内容</div>);
|
return renderDashboardContent(dashboard);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据子路径返回对应的管理页面
|
// 根据子路径返回对应的管理页面
|
||||||
switch (subPath) {
|
switch (subPath) {
|
||||||
case "posts":
|
case "posts":
|
||||||
return renderAdminLayout(<div>文章管理</div>);
|
return renderDashboardContent(posts);
|
||||||
case "media":
|
|
||||||
return renderAdminLayout(<div>媒体管理</div>);
|
|
||||||
case "comments":
|
case "comments":
|
||||||
return renderAdminLayout(<div>评论管理</div>);
|
return renderDashboardContent(comments);
|
||||||
case "categories":
|
case "categories":
|
||||||
return renderAdminLayout(<div>分类管理</div>);
|
return renderDashboardContent(categories);
|
||||||
|
case "files":
|
||||||
|
return renderDashboardContent(files);
|
||||||
case "settings":
|
case "settings":
|
||||||
return renderAdminLayout(<div>系统设置</div>);
|
return renderDashboardContent(settings);
|
||||||
|
case "themes":
|
||||||
|
return renderDashboardContent(themes);
|
||||||
|
case "users":
|
||||||
|
return renderDashboardContent(users);
|
||||||
|
case "plugins":
|
||||||
|
return renderDashboardContent(plugins);
|
||||||
default:
|
default:
|
||||||
return renderAdminLayout(<div>404 未找到页面</div>);
|
return renderDashboardContent(<div>404 未找到页面</div>);
|
||||||
}
|
}
|
||||||
}
|
default:
|
||||||
|
|
||||||
return renderContent(article);
|
return renderContent(article);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Routes;
|
export default Routes;
|
||||||
|
@ -1,27 +1,41 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef, memo } from 'react';
|
||||||
import { useThemeMode } from 'hooks/ThemeMode';
|
import { useThemeMode } from 'hooks/ThemeMode';
|
||||||
|
|
||||||
export const AnimatedBackground = () => {
|
interface AnimatedBackgroundProps {
|
||||||
|
onError?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) return;
|
if (!canvas) {
|
||||||
|
onError?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d')!;
|
try {
|
||||||
if (!ctx) return;
|
const ctx = canvas.getContext('2d', {
|
||||||
|
alpha: true,
|
||||||
|
desynchronized: true
|
||||||
|
});
|
||||||
|
|
||||||
const canvasElement = canvas!;
|
if (!ctx) {
|
||||||
|
console.error('无法获取 canvas context');
|
||||||
|
onError?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 生成随机HSL颜色
|
// 添加非空断言
|
||||||
|
const context = ctx!;
|
||||||
|
|
||||||
|
// 添加必要的变量定义
|
||||||
const getRandomHSLColor = () => {
|
const getRandomHSLColor = () => {
|
||||||
const hue = Math.random() * 360;
|
const hue = Math.random() * 360;
|
||||||
const saturation = 70 + Math.random() * 30;
|
const saturation = 70 + Math.random() * 30;
|
||||||
const lightness = mode === 'dark'
|
const lightness = mode === 'dark' ? 40 + Math.random() * 20 : 60 + Math.random() * 20;
|
||||||
? 40 + Math.random() * 20
|
|
||||||
: 60 + Math.random() * 20;
|
|
||||||
|
|
||||||
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -32,40 +46,40 @@ export const AnimatedBackground = () => {
|
|||||||
let dx = 0.2;
|
let dx = 0.2;
|
||||||
let dy = -0.2;
|
let dy = -0.2;
|
||||||
|
|
||||||
// 设置canvas尺寸为窗口大小
|
// 添加 drawBall 函数
|
||||||
const resizeCanvas = () => {
|
function drawBall() {
|
||||||
// 保存调整前的相对位置
|
context.beginPath();
|
||||||
const relativeX = x / canvas.width;
|
context.arc(x, y, ballRadius, 0, Math.PI * 2);
|
||||||
const relativeY = y / canvas.height;
|
context.fillStyle = ballColor;
|
||||||
|
context.fill();
|
||||||
|
context.closePath();
|
||||||
|
}
|
||||||
|
|
||||||
// 更新canvas尺寸
|
// 设置 canvas 尺寸
|
||||||
canvas.width = window.innerWidth;
|
canvas.width = window.innerWidth;
|
||||||
canvas.height = window.innerHeight;
|
canvas.height = window.innerHeight;
|
||||||
|
|
||||||
// 根据新尺寸更新球的位置
|
// 性能优化:降低动画帧率
|
||||||
x = canvas.width * relativeX;
|
const fps = 30;
|
||||||
y = canvas.height * relativeY;
|
const interval = 1000 / fps;
|
||||||
|
let then = Date.now();
|
||||||
|
|
||||||
// 立即重绘
|
const draw = () => {
|
||||||
drawBall();
|
const now = Date.now();
|
||||||
};
|
const delta = now - then;
|
||||||
|
|
||||||
function drawBall() {
|
if (delta > interval) {
|
||||||
ctx.beginPath();
|
// 更新时间戳
|
||||||
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
|
then = now - (delta % interval);
|
||||||
ctx.fillStyle = ballColor;
|
|
||||||
ctx.fill();
|
|
||||||
ctx.closePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw() {
|
// 绘制逻辑...
|
||||||
ctx.clearRect(0, 0, canvasElement.width, canvasElement.height);
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
drawBall();
|
drawBall();
|
||||||
|
|
||||||
if (x + dx > canvasElement.width - ballRadius || x + dx < ballRadius) {
|
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
|
||||||
dx = -dx;
|
dx = -dx;
|
||||||
}
|
}
|
||||||
if (y + dy > canvasElement.height - ballRadius || y + dy < ballRadius) {
|
if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
|
||||||
dy = -dy;
|
dy = -dy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,23 +87,39 @@ export const AnimatedBackground = () => {
|
|||||||
y += dy;
|
y += dy;
|
||||||
}
|
}
|
||||||
|
|
||||||
resizeCanvas();
|
// 使用 requestAnimationFrame 代替 setInterval
|
||||||
window.addEventListener('resize', resizeCanvas);
|
animationFrameId = requestAnimationFrame(draw);
|
||||||
const interval = setInterval(draw, 10);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
window.removeEventListener('resize', resizeCanvas);
|
|
||||||
};
|
};
|
||||||
}, [mode]);
|
|
||||||
|
let animationFrameId: number;
|
||||||
|
draw();
|
||||||
|
|
||||||
|
// 清理函数
|
||||||
|
return () => {
|
||||||
|
if (animationFrameId) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Canvas 初始化失败:', error);
|
||||||
|
onError?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [mode, onError]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 -z-10">
|
<div className="fixed inset-0 -z-10 overflow-hidden">
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
className="w-full h-full"
|
className="w-full h-full opacity-50"
|
||||||
style={{ filter: 'blur(150px)' }}
|
style={{
|
||||||
|
filter: 'blur(150px)',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
willChange: 'transform'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
@ -11,5 +11,6 @@ export interface ThemeConfig {
|
|||||||
layout?: string;
|
layout?: string;
|
||||||
configuration: Configuration;
|
configuration: Configuration;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
manage?: string;
|
||||||
routes: Map<string, string>;
|
routes: Map<string, string>;
|
||||||
}
|
}
|
||||||
|
@ -148,7 +148,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
href={link.url}
|
href={link.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="social-link p-3 rounded-full hover:bg-[--gray-3] transition-colors"
|
className="social-link hover-bg p-3 rounded-full"
|
||||||
>
|
>
|
||||||
{link.icon}
|
{link.icon}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -62,7 +62,7 @@ const mockArticles: PostDisplay[] = [
|
|||||||
content: "在这篇文章中,我们将探讨一些提高 JavaScript 性能的技巧和最佳实践...",
|
content: "在这篇文章中,我们将探讨一些提高 JavaScript 性能的技巧和最佳实践...",
|
||||||
authorName: "王五",
|
authorName: "王五",
|
||||||
publishedAt: new Date("2024-03-13"),
|
publishedAt: new Date("2024-03-13"),
|
||||||
coverImage: "https://mages.unsplash.com/photo-1592609931095-54a2168ae893?w=500&auto=format",
|
coverImage: "ssssxx",
|
||||||
metaKeywords: "",
|
metaKeywords: "",
|
||||||
metaDescription: "",
|
metaDescription: "",
|
||||||
status: "published",
|
status: "published",
|
||||||
@ -156,7 +156,7 @@ const mockArticles: PostDisplay[] = [
|
|||||||
{
|
{
|
||||||
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",
|
||||||
@ -244,10 +244,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
{articleData.map((article) => (
|
{articleData.map((article) => (
|
||||||
<Card
|
<Card
|
||||||
key={article.id}
|
key={article.id}
|
||||||
className="group cursor-pointer transition-all duration-300
|
className="group cursor-pointer hover-card border border-[--gray-a3]"
|
||||||
bg-[--card-bg] border-[--border-color]
|
|
||||||
hover:shadow-lg hover:shadow-[--card-bg]/10
|
|
||||||
hover:border-[--accent-9]/50"
|
|
||||||
>
|
>
|
||||||
<div className="p-4 relative flex flex-col gap-4">
|
<div className="p-4 relative flex flex-col gap-4">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
@ -279,7 +276,11 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<ScrollArea type="hover" scrollbars="horizontal" className="flex-1">
|
<ScrollArea
|
||||||
|
type="hover"
|
||||||
|
scrollbars="horizontal"
|
||||||
|
className="scroll-container flex-1"
|
||||||
|
>
|
||||||
<Flex gap="2" className="flex-nowrap">
|
<Flex gap="2" className="flex-nowrap">
|
||||||
{article.categories?.map((category) => (
|
{article.categories?.map((category) => (
|
||||||
<Text
|
<Text
|
||||||
|
@ -18,7 +18,12 @@ import parse from 'html-react-parser';
|
|||||||
|
|
||||||
// 直接导出 Layout 实例
|
// 直接导出 Layout 实例
|
||||||
export default new Layout(({ children, args }) => {
|
export default new Layout(({ children, args }) => {
|
||||||
const [moreState, setMoreState] = useState(false);
|
const [moreState, setMoreState] = useState(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return window.innerWidth >= 1024 ? false : false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
const [loginState, setLoginState] = useState(true);
|
const [loginState, setLoginState] = useState(true);
|
||||||
const [scrollProgress, setScrollProgress] = useState(0);
|
const [scrollProgress, setScrollProgress] = useState(0);
|
||||||
|
|
||||||
@ -73,6 +78,121 @@ export default new Layout(({ children, args }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 修改移动端菜单的渲染逻辑
|
||||||
|
const mobileMenu = (
|
||||||
|
<Box className="flex lg:hidden gap-2 items-center">
|
||||||
|
{/* 添加移动端进度指示器 */}
|
||||||
|
<Box
|
||||||
|
className={`w-10 h-10 flex items-center justify-center ${
|
||||||
|
scrollProgress > 0
|
||||||
|
? 'block'
|
||||||
|
: 'hidden'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
x="50"
|
||||||
|
y="55"
|
||||||
|
className="progress-indicator font-bold transition-colors"
|
||||||
|
dominantBaseline="middle"
|
||||||
|
textAnchor="middle"
|
||||||
|
style={{
|
||||||
|
fontSize: '56px',
|
||||||
|
fill: 'currentColor'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Math.round(scrollProgress)}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-10 h-10 p-0 hover:text-[--accent-9] transition-colors flex items-center justify-center group bg-transparent border-0"
|
||||||
|
onClick={() => setMoreState(!moreState)}
|
||||||
|
>
|
||||||
|
{moreState ? (
|
||||||
|
<Cross1Icon className="h-5 w-5 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
|
||||||
|
) : (
|
||||||
|
<HamburgerMenuIcon className="h-5 w-5 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 移动端菜单内容 */}
|
||||||
|
{moreState && (
|
||||||
|
<div
|
||||||
|
className="absolute top-full right-4 w-[180px] mt-2 rounded-md bg-[--gray-1] border border-[--gray-a5] shadow-lg
|
||||||
|
animate-in fade-in-0 zoom-in-95 slide-in-from-top-2
|
||||||
|
duration-200 z-[90]"
|
||||||
|
>
|
||||||
|
<Box className="flex flex-col">
|
||||||
|
{/* 导航链接区域 */}
|
||||||
|
<Box className="flex flex-col">
|
||||||
|
<Box className="flex flex-col [&>a]:px-4 [&>a]:py-2.5 [&>a]:text-[--gray-12] [&>a]:transition-colors [&>a:hover]:bg-[--gray-a3] [&>a]:text-lg [&>a]:text-center [&>a]:border-b [&>a]:border-[--gray-a5] [&>a:first-child]:rounded-t-md [&>a:last-child]:border-b-0">
|
||||||
|
{parse(navString)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 搜索框区域 */}
|
||||||
|
<Box className="p-4 border-t border-[--gray-a5]">
|
||||||
|
<TextField.Root
|
||||||
|
size="2"
|
||||||
|
variant="surface"
|
||||||
|
placeholder="搜索..."
|
||||||
|
className="w-full [&_input]:pl-3 hover:border-[--accent-9] border transition-colors group"
|
||||||
|
>
|
||||||
|
<TextField.Slot
|
||||||
|
side="right"
|
||||||
|
className="p-2"
|
||||||
|
>
|
||||||
|
<MagnifyingGlassIcon className="h-4 w-4 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
|
||||||
|
</TextField.Slot>
|
||||||
|
</TextField.Root>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 用户操作区域 */}
|
||||||
|
<Box className="p-4 border-t border-[--gray-a5]">
|
||||||
|
<Flex gap="3" align="center">
|
||||||
|
{/* 用户信息/登录按钮 - 调整为 70% 宽度 */}
|
||||||
|
<Box className="w-[70%]">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-start gap-2 text-[--gray-12] hover:text-[--accent-9] hover:bg-[--gray-a3] transition-colors"
|
||||||
|
>
|
||||||
|
{loginState ? (
|
||||||
|
<>
|
||||||
|
<AvatarIcon className="w-5 h-5" />
|
||||||
|
<span>个人中心</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PersonIcon className="w-5 h-5" />
|
||||||
|
<span>登录/注册</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 主题切换按钮 - 调整为 30% 宽度 */}
|
||||||
|
<Box className="w-[30%] flex justify-end [&_button]:w-10 [&_button]:h-10 [&_svg]:w-5 [&_svg]:h-5 [&_button]:text-[--gray-12] [&_button:hover]:text-[--accent-9]">
|
||||||
|
<ThemeModeToggle />
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Theme
|
<Theme
|
||||||
grayColor="gray"
|
grayColor="gray"
|
||||||
@ -87,7 +207,7 @@ export default new Layout(({ children, args }) => {
|
|||||||
{/* 导航栏 */}
|
{/* 导航栏 */}
|
||||||
<Box
|
<Box
|
||||||
asChild
|
asChild
|
||||||
className="w-full backdrop-blur-sm border-b border-[--gray-a5] z-60 sticky top-0"
|
className="w-full backdrop-blur-sm border-b border-[--gray-a5] z-[100] sticky top-0"
|
||||||
>
|
>
|
||||||
<nav>
|
<nav>
|
||||||
<Container size="4">
|
<Container size="4">
|
||||||
@ -100,7 +220,7 @@ export default new Layout(({ children, args }) => {
|
|||||||
<Flex align="center">
|
<Flex align="center">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="flex items-center group transition-all"
|
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 />
|
||||||
@ -221,138 +341,7 @@ export default new Layout(({ children, args }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 移动菜单按钮 */}
|
{/* 移动菜单按钮 */}
|
||||||
<Box className="flex lg:hidden gap-2 items-center">
|
{mobileMenu}
|
||||||
{/* 添加移动端进度指示器 */}
|
|
||||||
<Box
|
|
||||||
className={`w-10 h-10 flex items-center justify-center ${
|
|
||||||
scrollProgress > 0
|
|
||||||
? 'block'
|
|
||||||
: 'hidden'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
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}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-6 h-6"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
>
|
|
||||||
<text
|
|
||||||
x="50"
|
|
||||||
y="55"
|
|
||||||
className="progress-indicator font-bold transition-colors"
|
|
||||||
dominantBaseline="middle"
|
|
||||||
textAnchor="middle"
|
|
||||||
style={{
|
|
||||||
fontSize: '56px',
|
|
||||||
fill: 'currentColor'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Math.round(scrollProgress)}
|
|
||||||
</text>
|
|
||||||
</svg>
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Root
|
|
||||||
open={moreState}
|
|
||||||
onOpenChange={setMoreState}
|
|
||||||
>
|
|
||||||
<DropdownMenuPrimitive.Trigger asChild>
|
|
||||||
<Button
|
|
||||||
className="w-10 h-10 p-0 hover:text-[--accent-9] transition-colors flex items-center justify-center group bg-transparent border-0"
|
|
||||||
>
|
|
||||||
{moreState ? (
|
|
||||||
<Cross1Icon className="h-5 w-5 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
|
|
||||||
) : (
|
|
||||||
<HamburgerMenuIcon className="h-5 w-5 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuPrimitive.Trigger>
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
align="end"
|
|
||||||
sideOffset={20}
|
|
||||||
className="min-w-[200px] rounded-md bg-[--gray-1] border border-[--gray-a5] shadow-lg
|
|
||||||
data-[state=open]:animate-in
|
|
||||||
data-[state=closed]:animate-out
|
|
||||||
data-[state=closed]:fade-out-0
|
|
||||||
data-[state=open]:fade-in-0
|
|
||||||
data-[state=closed]:zoom-out-95
|
|
||||||
data-[state=open]:zoom-in-95
|
|
||||||
data-[state=closed]:slide-in-from-top-2
|
|
||||||
data-[state=left]:slide-in-from-right-2
|
|
||||||
data-[state=right]:slide-in-from-left-2
|
|
||||||
data-[state=top]:slide-in-from-bottom-2
|
|
||||||
duration-200
|
|
||||||
max-h-[calc(100vh-6rem)]
|
|
||||||
overflow-y-auto
|
|
||||||
z-50
|
|
||||||
absolute
|
|
||||||
top-full
|
|
||||||
right-0"
|
|
||||||
>
|
|
||||||
<Box className="flex flex-col">
|
|
||||||
{/* 导航链接区域 */}
|
|
||||||
<Box className="flex flex-col">
|
|
||||||
<Box className="flex flex-col [&>a]:px-4 [&>a]:py-2.5 [&>a]:text-[--gray-12] [&>a]:transition-colors [&>a:hover]:bg-[--gray-a3] [&>a]:text-lg [&>a]:text-center [&>a]:border-b [&>a]:border-[--gray-a5] [&>a:first-child]:rounded-t-md [&>a:last-child]:border-b-0">
|
|
||||||
{parse(navString)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 搜索框区域 */}
|
|
||||||
<Box className="p-4 border-t border-[--gray-a5]">
|
|
||||||
<TextField.Root
|
|
||||||
size="2"
|
|
||||||
variant="surface"
|
|
||||||
placeholder="搜索..."
|
|
||||||
className="w-full [&_input]:pl-3 hover:border-[--accent-9] border transition-colors group"
|
|
||||||
>
|
|
||||||
<TextField.Slot
|
|
||||||
side="right"
|
|
||||||
className="p-2"
|
|
||||||
>
|
|
||||||
<MagnifyingGlassIcon className="h-4 w-4 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
|
|
||||||
</TextField.Slot>
|
|
||||||
</TextField.Root>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 用户操作区域 */}
|
|
||||||
<Box className="p-4 border-t border-[--gray-a5]">
|
|
||||||
<Flex gap="3" align="center">
|
|
||||||
{/* 用户信息/登录按钮 - 调整为 70% 宽度 */}
|
|
||||||
<Box className="w-[70%]">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="w-full justify-start gap-2 text-[--gray-12] hover:text-[--accent-9] hover:bg-[--gray-a3] transition-colors"
|
|
||||||
>
|
|
||||||
{loginState ? (
|
|
||||||
<>
|
|
||||||
<AvatarIcon className="w-5 h-5" />
|
|
||||||
<span>个人中心</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<PersonIcon className="w-5 h-5" />
|
|
||||||
<span>登录/注册</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 主题切换按钮 - 调整为 30% 宽度 */}
|
|
||||||
<Box className="w-[30%] flex justify-end [&_button]:w-10 [&_button]:h-10 [&_svg]:w-5 [&_svg]:h-5 [&_button]:text-[--gray-12] [&_button:hover]:text-[--accent-9]">
|
|
||||||
<ThemeModeToggle />
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</DropdownMenuPrimitive.Content>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
</DropdownMenuPrimitive.Root>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -20,287 +20,221 @@ import MarkdownIt from 'markdown-it';
|
|||||||
import { ComponentPropsWithoutRef } from 'react';
|
import { ComponentPropsWithoutRef } from 'react';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import type { Components } from "react-markdown";
|
import type { Components } from "react-markdown";
|
||||||
|
import { toast } from "hooks/Notification";
|
||||||
|
|
||||||
// 示例文章数据
|
// 示例文章数据
|
||||||
const mockPost: PostDisplay = {
|
const mockPost: PostDisplay = {
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "现代前端开发完全指南",
|
title: "Markdown 完全指南:从基础到高级排版",
|
||||||
content: `
|
content: `
|
||||||
# 现代前端开发完全指南
|
# Markdown 完全指南:从基础到高级排版
|
||||||
|
|
||||||
前端开发已经成为软件开发中最重要的领域之一。本全面介绍现代前端开发的各个方面。
|
这篇指南将介绍 Markdown 的基础语法和高级排版技巧。
|
||||||
|
|
||||||
![Modern Frontend Development](https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=1200&h=600)
|
## 1. 基础语法
|
||||||
|
|
||||||
## 1. 开发环境搭建
|
### 1.1 文本格式化
|
||||||
|
|
||||||
在开始前端开发之前,我们要搭建合适的开发环境。
|
普通文本不需要任何特殊标记。
|
||||||
|
|
||||||
### 1.1 必备工具安装
|
**这是粗体文本**
|
||||||
|
*这是斜体文本*
|
||||||
|
***这是粗斜体文本***
|
||||||
|
~~这是删除线文本~~
|
||||||
|
|
||||||
发环境需要安装以下工具:
|
### 1.2 列表
|
||||||
|
|
||||||
\`\`\`bash
|
#### 无序列表:
|
||||||
# 安装 Node.js
|
- 第一项
|
||||||
brew install node
|
- 子项 1
|
||||||
|
- 子项 2
|
||||||
|
- 第二项
|
||||||
|
- 第三项
|
||||||
|
|
||||||
# 安装包管理器
|
#### 有序列表:
|
||||||
npm install -g pnpm
|
1. 第一步
|
||||||
|
1. 子步骤 1
|
||||||
|
2. 子步骤 2
|
||||||
|
2. 第二步
|
||||||
|
3. 第三步
|
||||||
|
|
||||||
# 安装开发工具
|
#### 任务列表:
|
||||||
pnpm install -g typescript vite
|
- [x] 已完成任务
|
||||||
\`\`\`
|
- [ ] 未完成任务
|
||||||
|
- [x] 又一个已完成任务
|
||||||
|
|
||||||
### 1.2 编辑器配置
|
### 1.3 代码展示
|
||||||
|
|
||||||
推荐使用 VS Code 作为开发工具,需要安装以下插件:
|
行内代码:\`const greeting = "Hello World";\`
|
||||||
|
|
||||||
- ESLint
|
|
||||||
- Prettier
|
|
||||||
- TypeScript Vue Plugin
|
|
||||||
- Tailwind CSS IntelliSense
|
|
||||||
|
|
||||||
![VS Code Setup](https://images.unsplash.com/photo-1517694712202-14dd9538aa97?w=1200&h=600)
|
|
||||||
|
|
||||||
## 2. 项目架构设计
|
|
||||||
|
|
||||||
### 2.1 目录结构
|
|
||||||
### 2.1 目录结构
|
|
||||||
### 2.1 目录结构
|
|
||||||
### 2.1 目录结构
|
|
||||||
### 2.1 目录结构
|
|
||||||
### 2.1 目录结构
|
|
||||||
### 2.1 目录结构
|
|
||||||
### 2.1 目录结构
|
|
||||||
|
|
||||||
一个良好的项目结构对于项目的可维护性至关重要。
|
|
||||||
|
|
||||||
|
代码块:
|
||||||
\`\`\`typescript
|
\`\`\`typescript
|
||||||
// 推荐的项目结构
|
interface User {
|
||||||
interface ProjectStructure {
|
id: number;
|
||||||
src: {
|
name: string;
|
||||||
components: {
|
email: string;
|
||||||
common: string[]; // 通用组件
|
|
||||||
features: string[]; // 功能组件
|
|
||||||
layouts: string[]; // 布局组件
|
|
||||||
};
|
|
||||||
pages: string[]; // 页面组件
|
|
||||||
hooks: string[]; // 定 hooks
|
|
||||||
utils: string[]; // 工具函数
|
|
||||||
types: string[]; // 类型定义
|
|
||||||
styles: string[]; // 样式文件
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function greet(user: User): string {
|
||||||
|
return \`Hello, \${user.name}!\`;
|
||||||
}
|
}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
### 2.2 状态管理
|
### 1.4 表格
|
||||||
|
|
||||||
现代前端应用需要高效的状态管理方案:
|
| 功能 | 基础版 | 高级版 |
|
||||||
|
|:-----|:------:|-------:|
|
||||||
|
| 文本编辑 | ✓ | ✓ |
|
||||||
|
| 实时预览 | ✗ | ✓ |
|
||||||
|
| 导出格式 | 2种 | 5种 |
|
||||||
|
|
||||||
![State Management](https://images.unsplash.com/photo-1555949963-ff9fe0c870eb?w=1200&h=600)
|
## 2. 高级排版
|
||||||
|
|
||||||
## 3. 性能优化
|
### 2.1 图文混排布局
|
||||||
|
|
||||||
### 3.1 加载性能
|
#### 左图右文
|
||||||
|
|
||||||
关键的加载性能指标:
|
<div class="flex items-center gap-6 my-8">
|
||||||
|
<img src="https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=400&h=400"
|
||||||
|
alt="写作工具"
|
||||||
|
class="w-1/3 rounded-lg shadow-lg" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<h4 class="text-xl font-bold mb-2">高效写作工具</h4>
|
||||||
|
<p>使用合适的写作工具可以极大提升写作效率。推荐使用支持即时预览的编辑器,这样可以实时查看排版效果。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
| 指标 | 目标值 | 优化方法 |
|
#### 右图左文
|
||||||
|------|--------|----------|
|
|
||||||
| FCP | < 2s | 路由懒加载 |
|
|
||||||
| TTI | < 3.5s | 代码分割 |
|
|
||||||
| LCP | < 2.5s | 图片优化 |
|
|
||||||
|
|
||||||
### 3.2 运行时性能
|
<div class="flex items-center gap-6 my-8">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h4 class="text-xl font-bold mb-2">版面设计原则</h4>
|
||||||
|
<p>好的版面设计应该让内容清晰易读,层次分明。合理使用留白和分隔符可以让文章更有结构感。</p>
|
||||||
|
</div>
|
||||||
|
<img src="https://images.unsplash.com/photo-1499951360447-b19be8fe80f5?w=400&h=400"
|
||||||
|
alt="设计工具"
|
||||||
|
class="w-1/3 rounded-lg shadow-lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
#### 3.2.1 虚拟列表
|
### 2.2 可折叠内容
|
||||||
|
|
||||||
处理大数据列表时的示例代码:
|
<details class="my-4">
|
||||||
|
<summary class="cursor-pointer p-4 bg-gray-100 rounded-lg font-medium hover:bg-gray-200 transition-colors">
|
||||||
|
🎯 如何选择合适的写作工具?
|
||||||
|
</summary>
|
||||||
|
|
||||||
\`\`\`typescript
|
选择写作工具时需要考虑以下几点:
|
||||||
interface VirtualListProps {
|
|
||||||
items: any[];
|
|
||||||
height: number;
|
|
||||||
itemHeight: number;
|
|
||||||
renderItem: (item: any) => React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VirtualList: React.FC<VirtualListProps> = ({
|
1. **跨平台支持** - 确保在不同设备上都能访问
|
||||||
items,
|
2. **实时预览** - Markdown 实时渲染很重要
|
||||||
height,
|
3. **版本控制** - 最好能支持文章的版本管理
|
||||||
itemHeight,
|
4. **导出功能** - 支持导出为多种格式
|
||||||
renderItem
|
</details>
|
||||||
}) => {
|
|
||||||
// 现码...
|
|
||||||
};
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
#### 3.2.2 防抖与节流
|
### 2.3 并排卡片
|
||||||
|
|
||||||
\`\`\`typescript
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 my-8">
|
||||||
// 防抖函数实现
|
<div class="p-6 bg-gray-100 rounded-lg">
|
||||||
function debounce<T extends (...args: any[]) => any>(
|
<h4 class="text-lg font-bold mb-2">🚀 快速上手</h4>
|
||||||
fn: T,
|
<p>通过简单的标记语法,快速创建格式化的文档,无需复杂的排版工具。</p>
|
||||||
delay: number
|
</div>
|
||||||
): (...args: Parameters<T>) => void {
|
<div class="p-6 bg-gray-100 rounded-lg">
|
||||||
let timeoutId: NodeJS.Timeout;
|
<h4 class="text-lg font-bold mb-2">⚡ 高效输出</h4>
|
||||||
|
<p>专注于内容创作,让工具自动处理排版,提高写作效率。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
return function (...args: Parameters<T>) {
|
### 2.4 高亮提示框
|
||||||
clearTimeout(timeoutId);
|
|
||||||
timeoutId = setTimeout(() => fn(...args), delay);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 3.3 构建优化
|
<div class="p-6 bg-blue-50 border-l-4 border-blue-500 rounded-lg my-8">
|
||||||
|
<h4 class="text-lg font-bold text-blue-700 mb-2">💡 小贴士</h4>
|
||||||
|
<p class="text-blue-600">在写作时,可以先列出文章大纲,再逐步充实内容。这样可以保证文章结构清晰,内容完整。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
![Build Optimization](https://images.unsplash.com/photo-1551033406-611cf9a28f67?w=1200&h=600)
|
<div class="p-6 bg-yellow-50 border-l-4 border-yellow-500 rounded-lg my-8">
|
||||||
|
<h4 class="text-lg font-bold text-yellow-700 mb-2">⚠️ 注意事项</h4>
|
||||||
|
<p class="text-yellow-600">写作时要注意文章的受众,使用他们能理解的语言和例子。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
## 4. 测试略
|
### 2.5 时间线
|
||||||
|
|
||||||
### 4.1 单元测试
|
<div class="relative pl-8 my-8 border-l-2 border-gray-200">
|
||||||
|
<div class="mb-8 relative">
|
||||||
|
<div class="absolute -left-10 w-4 h-4 bg-blue-500 rounded-full"></div>
|
||||||
|
<div class="font-bold mb-2">1. 确定主题</div>
|
||||||
|
<p>根据目标受众和写作目的,确定文章主题。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
使用 Jest 进行单元测试:
|
<div class="mb-8 relative">
|
||||||
|
<div class="absolute -left-10 w-4 h-4 bg-blue-500 rounded-full"></div>
|
||||||
|
<div class="font-bold mb-2">2. 收集资料</div>
|
||||||
|
<p>广泛搜集相关资料,为写作做充分准备。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
\`typescript
|
<div class="relative">
|
||||||
describe('Utils', () => {
|
<div class="absolute -left-10 w-4 h-4 bg-blue-500 rounded-full"></div>
|
||||||
test('debounce should work correctly', (done) => {
|
<div class="font-bold mb-2">3. 开始写作</div>
|
||||||
let counter = 0;
|
<p>按照大纲逐步展开写作。</p>
|
||||||
const increment = () => counter++;
|
</div>
|
||||||
const debouncedIncrement = debounce(increment, 100);
|
</div>
|
||||||
|
|
||||||
debouncedIncrement();
|
### 2.6 引用样式
|
||||||
debouncedIncrement();
|
|
||||||
debouncedIncrement();
|
|
||||||
|
|
||||||
expect(counter).toBe(0);
|
> 📌 **最佳实践**
|
||||||
|
>
|
||||||
|
> 好的文章需要有清晰的结构和流畅的表达。以下是一些建议:
|
||||||
|
>
|
||||||
|
> 1. 开门见山,直入主题
|
||||||
|
> 2. 层次分明,逻辑清晰
|
||||||
|
> 3. 语言简洁,表达准确
|
||||||
|
>
|
||||||
|
> *— 写作指南*
|
||||||
|
|
||||||
setTimeout(() => {
|
## 3. 特殊语法
|
||||||
expect(counter).toBe(1);
|
|
||||||
done();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 4.2 集成测试
|
### 3.1 数学公式
|
||||||
|
|
||||||
使用 Cypress 进行端到端测试。
|
行内公式:$E = mc^2$
|
||||||
|
|
||||||
![Testing](https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=1200&h=600)
|
块级公式:
|
||||||
|
|
||||||
## 5. 部署与监控
|
$$
|
||||||
|
\\frac{n!}{k!(n-k)!} = \\binom{n}{k}
|
||||||
|
$$
|
||||||
|
|
||||||
### 5.1 CI/CD 配置
|
### 3.2 脚注
|
||||||
|
|
||||||
\`\`\`yaml
|
这里有一个脚注[^1]。
|
||||||
name: Deploy
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
jobs:
|
[^1]: 这是脚注的内容。
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Install
|
|
||||||
run: pnpm install
|
|
||||||
- name: Build
|
|
||||||
run: pnpm build
|
|
||||||
- name: Deploy
|
|
||||||
run: pnpm deploy
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 5.2 监控系统
|
### 3.3 表情符号
|
||||||
|
|
||||||
#### 5.2.1 性能监控
|
:smile: :heart: :thumbsup: :star: :rocket:
|
||||||
|
|
||||||
关键指标监控:
|
## 4. 总结
|
||||||
|
|
||||||
- 页面加载时间
|
本文展示了 Markdown 从基础到高级的各种用法:
|
||||||
- 首次内容绘制
|
|
||||||
- 首次大内容绘制
|
|
||||||
- 首次输入延迟
|
|
||||||
|
|
||||||
#### 5.2.2 错误监控
|
1. 基础语法:文本格式化、列表、代码、表格等
|
||||||
|
2. 高级排版:图文混排、折叠面板、卡片布局等
|
||||||
|
3. 特殊语法:数学公式、脚注、表情符号等
|
||||||
|
|
||||||
错误示例:
|
> 💡 **提示**:部分高级排版功能可能需要特定的 Markdown 编辑器或渲染器支持。使用前请确认你的工具是否支持这些特性。
|
||||||
|
|
||||||
\`\`\`typescript
|
|
||||||
interface ErrorReport {
|
|
||||||
type: 'error' | 'warning';
|
|
||||||
message: string;
|
|
||||||
stack?: string;
|
|
||||||
timestamp: number;
|
|
||||||
userAgent: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportError(error: Error): void {
|
|
||||||
const report: ErrorReport = {
|
|
||||||
type: 'error',
|
|
||||||
message: error.message,
|
|
||||||
stack: error.stack,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
userAgent: navigator.userAgent
|
|
||||||
};
|
|
||||||
|
|
||||||
// 发送错误报告
|
|
||||||
sendErrorReport(report);
|
|
||||||
}
|
|
||||||
\`\`
|
|
||||||
|
|
||||||
## 6. 安全最佳实践
|
|
||||||
|
|
||||||
### 6.1 XSS 防护
|
|
||||||
|
|
||||||
\`\`\`typescript
|
|
||||||
// 安全的 HTML 转义函数
|
|
||||||
function escapeHtml(unsafe: string): string {
|
|
||||||
return unsafe
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, """)
|
|
||||||
.replace(/'/g, "'");
|
|
||||||
}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 6.2 CSRF 防护
|
|
||||||
|
|
||||||
![Security](https://images.unsplash.com/photo-1555949963-aa79dcee981c?w=1200&h=600)
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
现代前端开发是一个复杂的系统工程,需要我们在以下方面不断精进:
|
|
||||||
|
|
||||||
1. 工程化能力
|
|
||||||
2. 性能优化
|
|
||||||
3. 测试覆盖
|
|
||||||
4. 全防护
|
|
||||||
5. 部署监控
|
|
||||||
|
|
||||||
> 持续学习实践是提高端开发水平的关键。
|
|
||||||
|
|
||||||
相关资源:
|
|
||||||
- [MDN Web Docs](https://developer.mozilla.org/)
|
|
||||||
- [Web.dev](https://web.dev/)
|
|
||||||
- [GitHub](https://github.com/)
|
|
||||||
`,
|
`,
|
||||||
authorName: "张三",
|
authorName: "Markdown 专家",
|
||||||
publishedAt: new Date("2024-03-15"),
|
publishedAt: new Date("2024-03-15"),
|
||||||
coverImage: "",
|
coverImage: "https://images.unsplash.com/photo-1499951360447-b19be8fe80f5?w=1200&h=600",
|
||||||
metaKeywords: "前端开发,工程,效率",
|
metaKeywords: "Markdown,基础语法,高级排版,布局设计",
|
||||||
metaDescription: "探讨如何构建高效的前端开发高开发效率",
|
metaDescription: "从基础语法到高级排版,全面了解 Markdown 的各种用法和技巧。",
|
||||||
status: "published",
|
status: "published",
|
||||||
isEditor: true,
|
isEditor: true,
|
||||||
createdAt: new Date("2024-03-15"),
|
createdAt: new Date("2024-03-15"),
|
||||||
updatedAt: new Date("2024-03-15"),
|
updatedAt: new Date("2024-03-15"),
|
||||||
categories: [{ name: "前端开发" }],
|
categories: [{ name: "教程" }],
|
||||||
tags: [{ name: "工程化" }, { name: "效率提升" }, { name: "发工具" }],
|
tags: [{ name: "Markdown" }, { name: "排版" }, { name: "写作" }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// 添 meta 函数
|
// 添 meta 函数
|
||||||
export const meta: MetaFunction = () => {
|
export const meta: MetaFunction = () => {
|
||||||
return [
|
return [
|
||||||
@ -326,23 +260,36 @@ interface CopyButtonProps {
|
|||||||
code: string;
|
code: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加 CopyButton 组件
|
// 修改 CopyButton 组件
|
||||||
const CopyButton: React.FC<CopyButtonProps> = ({ code }) => {
|
const CopyButton: React.FC<CopyButtonProps> = ({ code }) => {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
await navigator.clipboard.writeText(code);
|
await navigator.clipboard.writeText(code);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 2000);
|
toast.success("复制成功", "代码已复制到剪贴板");
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 3000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('复制失败:', err);
|
||||||
|
toast.error("复制失败", "请检查浏览器权限设置");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="h-7 px-2 text-xs hover:bg-[--gray-4]"
|
className="h-7 px-2 text-xs
|
||||||
|
transition-all duration-300 ease-in-out
|
||||||
|
[@media(hover:hover)]:hover:bg-[--gray-4]
|
||||||
|
active:bg-[--gray-4] active:transition-none"
|
||||||
>
|
>
|
||||||
|
<span className="transition-opacity duration-300">
|
||||||
{copied ? "已复制" : "复制"}
|
{copied ? "已复制" : "复制"}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -515,11 +462,11 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<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-2] dark:bg-[--gray-2]
|
bg-[--gray-3] dark:bg-[--gray-3]
|
||||||
rounded-t-lg
|
rounded-t-lg
|
||||||
mx-0"
|
mx-0"
|
||||||
>
|
>
|
||||||
<div className="text-sm text-[--gray-11] dark:text-[--gray-11] font-medium">{lang || "text"}</div>
|
<div className="text-sm text-[--gray-12] dark:text-[--gray-12] font-medium">{lang || "text"}</div>
|
||||||
<CopyButton code={String(children)} />
|
<CopyButton code={String(children)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -565,9 +512,9 @@ 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="overflow-x-auto">
|
<div className="scroll-container overflow-x-auto">
|
||||||
<div className="min-w-[640px] sm:min-w-0">
|
<div className="min-w-[640px] sm:min-w-0">
|
||||||
<div className="border-x border-t border-b sm:border-t border-[--gray-6] rounded-none sm:rounded-lg bg-white dark:bg-[--gray-1]">
|
<div className="border border-[--gray-6] rounded-lg bg-white dark:bg-[--gray-1]">
|
||||||
<table className="w-full border-collapse text-xs sm:text-sm" {...props}>
|
<table className="w-full border-collapse text-xs sm:text-sm" {...props}>
|
||||||
{children}
|
{children}
|
||||||
</table>
|
</table>
|
||||||
@ -580,7 +527,10 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
th: ({ children, ...props }: ComponentPropsWithoutRef<'th'>) => (
|
th: ({ children, ...props }: ComponentPropsWithoutRef<'th'>) => (
|
||||||
<th
|
<th
|
||||||
className="px-4 sm:px-4 md:px-6 py-2 sm:py-3 text-left text-[10px] sm:text-xs font-medium uppercase tracking-wider
|
className="px-4 sm:px-4 md:px-6 py-2 sm:py-3 text-left text-[10px] sm:text-xs font-medium uppercase tracking-wider
|
||||||
text-[--gray-12] break-words hyphens-auto"
|
text-[--gray-12] break-words hyphens-auto
|
||||||
|
bg-[--gray-3] dark:bg-[--gray-3]
|
||||||
|
first:rounded-tl-lg last:rounded-tr-lg
|
||||||
|
border-b border-[--gray-6]"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -621,7 +571,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
top: entry.boundingClientRect.top
|
top: entry.boundingClientRect.top
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 选择最靠近视口顶部的标题
|
// 选择靠近视口顶部的标题
|
||||||
const closestHeading = visibleHeadings.reduce((prev, current) => {
|
const closestHeading = visibleHeadings.reduce((prev, current) => {
|
||||||
return Math.abs(current.top) < Math.abs(prev.top) ? current : prev;
|
return Math.abs(current.top) < Math.abs(prev.top) ? current : prev;
|
||||||
});
|
});
|
||||||
@ -734,7 +684,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<ScrollArea
|
<ScrollArea
|
||||||
type="hover"
|
type="hover"
|
||||||
scrollbars="vertical"
|
scrollbars="vertical"
|
||||||
className="h-[calc(100vh-64px)] p-4"
|
className="scroll-container h-[calc(100vh-64px)] p-4"
|
||||||
>
|
>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{tocItems.map((item, index) => {
|
{tocItems.map((item, index) => {
|
||||||
@ -888,7 +838,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 封面图片 */}
|
{/* 面图片 */}
|
||||||
{mockPost.coverImage && (
|
{mockPost.coverImage && (
|
||||||
<Box className="mb-16 rounded-xl overflow-hidden aspect-[2/1] shadow-lg">
|
<Box className="mb-16 rounded-xl overflow-hidden aspect-[2/1] shadow-lg">
|
||||||
<img
|
<img
|
||||||
@ -933,7 +883,7 @@ export default new Template({}, ({ http, args }) => {
|
|||||||
<ScrollArea
|
<ScrollArea
|
||||||
type="hover"
|
type="hover"
|
||||||
scrollbars="vertical"
|
scrollbars="vertical"
|
||||||
className="max-h-[calc(100vh-180px)]"
|
className="scroll-container max-h-[calc(100vh-180px)]"
|
||||||
style={{
|
style={{
|
||||||
["--scrollbar-size" as string]: "6px",
|
["--scrollbar-size" as string]: "6px",
|
||||||
}}
|
}}
|
||||||
|
@ -53,37 +53,25 @@ body:has(:root[class~="dark"]) {
|
|||||||
.rt-Card,
|
.rt-Card,
|
||||||
[class*="rt-Card"] {
|
[class*="rt-Card"] {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid var(--gray-a3) !important;
|
||||||
|
/* 覆盖 Radix UI 的默认边框样式 */
|
||||||
|
--card-border-width: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 暗色主题卡片样式 */
|
/* 暗色主题卡片样式 */
|
||||||
html[class~="dark"] body .rt-Card,
|
html[class~="dark"] body .rt-Card,
|
||||||
html[class~="dark"] body [class*="rt-Card"] {
|
html[class~="dark"] body [class*="rt-Card"] {
|
||||||
background-color: rgb(2, 6, 16);
|
background-color: rgb(2, 6, 16);
|
||||||
border-color: rgba(148, 163, 184, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 通用交互元素样式 */
|
/* 添加卡片悬停样式 */
|
||||||
.rt-Button,
|
.rt-Card:hover,
|
||||||
.rt-DialogContent,
|
[class*="rt-Card"]:hover {
|
||||||
.rt-DropdownMenuContent,
|
border-color: var(--accent-9) !important;
|
||||||
[class*="rt-Button"],
|
|
||||||
[class*="rt-DialogContent"],
|
|
||||||
[class*="rt-DropdownMenuContent"] {
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 暗色主题交互元素 */
|
/* 导航链接样式 - 更具体的选择器 */
|
||||||
html[class~="dark"] body .rt-Button,
|
|
||||||
html[class~="dark"] body .rt-DialogContent,
|
|
||||||
html[class~="dark"] body .rt-DropdownMenuContent,
|
|
||||||
html[class~="dark"] body [class*="rt-Button"],
|
|
||||||
html[class~="dark"] body [class*="rt-DialogContent"],
|
|
||||||
html[class~="dark"] body [class*="rt-DropdownMenuContent"] {
|
|
||||||
background-color: rgb(2, 6, 16);
|
|
||||||
border-color: rgba(148, 163, 184, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 导航链接样式 */
|
|
||||||
#nav a {
|
#nav a {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
@ -110,6 +98,27 @@ html[class~="dark"] body [class*="rt-DropdownMenuContent"] {
|
|||||||
transform: scaleX(1);
|
transform: scaleX(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Markdown 内容链接样式 */
|
||||||
|
.prose a {
|
||||||
|
position: relative;
|
||||||
|
color: var(--accent-9);
|
||||||
|
text-decoration: none;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
border-bottom: 1.5px solid var(--accent-8);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose a:hover {
|
||||||
|
color: var(--accent-11);
|
||||||
|
border-bottom: 2px solid var(--accent-9);
|
||||||
|
padding-bottom: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除 Markdown 链接的导航样式 */
|
||||||
|
.prose a::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* 进度指示器动画 */
|
/* 进度指示器动画 */
|
||||||
@keyframes flow {
|
@keyframes flow {
|
||||||
0% { background-position: 0% center; }
|
0% { background-position: 0% center; }
|
||||||
@ -215,3 +224,94 @@ html[class~="dark"] body [class*="shadow"] {
|
|||||||
.zoom-out-95 {
|
.zoom-out-95 {
|
||||||
animation: zoomOut 0.2s ease-in;
|
animation: zoomOut 0.2s ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 通用交互效果 */
|
||||||
|
.hover-card {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
background-color: var(--gray-4);
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-border {
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-border:hover {
|
||||||
|
border-color: var(--accent-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-text {
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-text:hover {
|
||||||
|
color: var(--accent-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-scale {
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-scale:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-bg {
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-bg:hover {
|
||||||
|
background-color: var(--gray-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 横向滚动条容器 */
|
||||||
|
.scroll-container {
|
||||||
|
scrollbar-width: thin; /* Firefox */
|
||||||
|
scrollbar-color: var(--gray-a6) transparent; /* Firefox */
|
||||||
|
-webkit-overflow-scrolling: touch; /* iOS 滚动优化 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Webkit 滚动条样式 */
|
||||||
|
.scroll-container::-webkit-scrollbar {
|
||||||
|
height: 6px; /* 横向滚动条高度 */
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-container::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 0 4px; /* 轨道边距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-container::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--gray-a6);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--gray-a8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题滚动条 */
|
||||||
|
html[class~="dark"] .scroll-container::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--gray-a4);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[class~="dark"] .scroll-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--gray-a5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏滚动条但保持可滚动 */
|
||||||
|
.scroll-hidden {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-hidden::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari and Opera */
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user