echoes/frontend/app/dashboard/plugins.tsx

290 lines
8.2 KiB
TypeScript
Raw Normal View History

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>
);
});