diff --git a/backend/Cargo.toml b/backend/Cargo.toml index eec9e57..e7af1f5 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -23,4 +23,5 @@ rand = "0.8.5" chrono = "0.4" regex = "1.11.1" bcrypt = "0.16" -hex = "0.4.3" \ No newline at end of file +hex = "0.4.3" +rocket_cors = "0.6.0" diff --git a/backend/src/main.rs b/backend/src/main.rs index 47b0edc..516a6a6 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -5,10 +5,13 @@ mod storage; use crate::common::config; use common::error::{CustomErrorInto, CustomResult}; +use rocket::http::Method; use rocket::Shutdown; +use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors, CorsOptions}; use std::sync::Arc; use storage::sql; use tokio::sync::Mutex; + pub struct AppState { db: Arc>>, shutdown: Arc>>, @@ -61,6 +64,25 @@ impl AppState { } } +fn cors() -> Cors { + CorsOptions { + allowed_origins: AllowedOrigins::all(), + allowed_methods: vec![Method::Get, Method::Post, Method::Options] + .into_iter() + .map(From::from) + .collect(), + allowed_headers: AllowedHeaders::all(), + allow_credentials: true, + expose_headers: Default::default(), + max_age: None, + send_wildcard: false, + fairing_route_base: "/".to_string(), + fairing_route_rank: 0, + } + .to_cors() + .expect("CORS配置错误") +} + #[rocket::main] async fn main() -> CustomResult<()> { let config = config::Config::read().unwrap_or_else(|e| { @@ -75,7 +97,8 @@ async fn main() -> CustomResult<()> { let mut rocket_builder = rocket::build() .configure(rocket_config) - .manage(state.clone()); + .manage(state.clone()) + .attach(cors()); if !config.init.sql { rocket_builder = rocket_builder.mount("/", rocket::routes![api::setup::setup_sql]); diff --git a/backend/src/storage/sql/builder.rs b/backend/src/storage/sql/builder.rs index 69f5439..2350131 100644 --- a/backend/src/storage/sql/builder.rs +++ b/backend/src/storage/sql/builder.rs @@ -254,7 +254,7 @@ pub enum Operator { Lte, Like, In, - IsNull + IsNull, } impl Operator { @@ -268,7 +268,7 @@ impl Operator { Operator::Lte => "<=", Operator::Like => "LIKE", Operator::In => "IN", - Operator::IsNull => "IS NULL" + Operator::IsNull => "IS NULL", } } } @@ -295,7 +295,7 @@ pub enum WhereClause { And(Vec), Or(Vec), Condition(Condition), - Not(Condition) + Not(Condition), } #[derive(Debug, Clone)] diff --git a/backend/src/storage/sql/schema.rs b/backend/src/storage/sql/schema.rs index b057867..c5832bf 100644 --- a/backend/src/storage/sql/schema.rs +++ b/backend/src/storage/sql/schema.rs @@ -541,7 +541,9 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes .add_field(Field::new( "status", FieldType::VarChar(20), - FieldConstraint::new().not_null().check(content_state_check.clone()), + FieldConstraint::new() + .not_null() + .check(content_state_check.clone()), ValidationLevel::Strict, )?); @@ -587,7 +589,9 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes .add_field(Field::new( "status", FieldType::VarChar(20), - FieldConstraint::new().not_null().check(content_state_check.clone()), + FieldConstraint::new() + .not_null() + .check(content_state_check.clone()), ValidationLevel::Strict, )?) .add_field(Field::new( @@ -728,7 +732,9 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes .add_field(Field::new( "target_type", FieldType::VarChar(20), - FieldConstraint::new().not_null().check(target_type_check.clone()), + FieldConstraint::new() + .not_null() + .check(target_type_check.clone()), ValidationLevel::Strict, )?) .add_field(Field::new( @@ -782,7 +788,9 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes .add_field(Field::new( "target_type", FieldType::VarChar(20), - FieldConstraint::new().not_null().check(target_type_check.clone()), + FieldConstraint::new() + .not_null() + .check(target_type_check.clone()), ValidationLevel::Strict, )?) .add_field(Field::new( @@ -843,10 +851,7 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes WhereClause::Condition(Condition::new( "type".to_string(), Operator::Eq, - Some(SafeValue::Text( - "'tag'".to_string(), - ValidationLevel::Raw, - )), + Some(SafeValue::Text("'tag'".to_string(), ValidationLevel::Raw)), )?), WhereClause::Condition(Condition::new( "type".to_string(), diff --git a/frontend/app/dashboard/categories.tsx b/frontend/app/dashboard/categories.tsx index c0d3e10..27497a3 100644 --- a/frontend/app/dashboard/categories.tsx +++ b/frontend/app/dashboard/categories.tsx @@ -1,16 +1,16 @@ import { Template } from "interface/template"; -import { - Container, - Heading, - Text, - Box, - Flex, +import { + Container, + Heading, + Text, + Box, + Flex, Table, Button, TextField, ScrollArea, Dialog, - IconButton + IconButton, } from "@radix-ui/themes"; import { PlusIcon, @@ -28,39 +28,41 @@ const mockCategories: (Category & { id: number; count: number })[] = [ id: 1, name: "前端开发", parentId: undefined, - count: 15 + count: 15, }, { id: 2, name: "React", parentId: "1", - count: 8 + count: 8, }, { id: 3, name: "Vue", parentId: "1", - count: 5 + count: 5, }, { id: 4, name: "后端开发", parentId: undefined, - count: 12 + count: 12, }, { id: 5, name: "Node.js", parentId: "4", - count: 6 - } + 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(); + const [selectedParentId, setSelectedParentId] = useState< + string | undefined + >(); return ( @@ -74,7 +76,7 @@ export default new Template({}, ({ http, args }) => { 共 {mockCategories.length} 个分类 - - + ); -}); \ No newline at end of file +}); diff --git a/frontend/app/dashboard/comments.tsx b/frontend/app/dashboard/comments.tsx index 287d2c5..9eaac47 100644 --- a/frontend/app/dashboard/comments.tsx +++ b/frontend/app/dashboard/comments.tsx @@ -1,10 +1,10 @@ import { Template } from "interface/template"; -import { - Container, - Heading, - Text, - Box, - Flex, +import { + Container, + Heading, + Text, + Box, + Flex, Table, Button, TextField, @@ -12,7 +12,7 @@ import { ScrollArea, DataList, Avatar, - Badge + Badge, } from "@radix-ui/themes"; import { MagnifyingGlassIcon, @@ -32,7 +32,7 @@ const mockComments = [ postTitle: "构建现代化的前端开发工作流", createdAt: new Date("2024-03-15"), status: "pending", // pending, approved, rejected - avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=1" + avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=1", }, { id: 2, @@ -41,7 +41,7 @@ const mockComments = [ postTitle: "React 18 新特性详解", createdAt: new Date("2024-03-14"), status: "approved", - avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2" + avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2", }, // 可以添加更多模拟数据 ]; @@ -51,24 +51,24 @@ export default new Template({}, ({ http, args }) => { const [selectedStatus, setSelectedStatus] = useState("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]'; + 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]'; + return "bg-[--yellow-3] text-[--yellow-11]"; } }; const getStatusText = (status: string) => { - switch(status) { - case 'approved': - return '已通过'; - case 'rejected': - return '已拒绝'; + switch (status) { + case "approved": + return "已通过"; + case "rejected": + return "已拒绝"; default: - return '待审核'; + return "待审核"; } }; @@ -87,15 +87,14 @@ export default new Template({}, ({ http, args }) => { {/* 搜索和筛选栏 */} - + - ) => setSearchTerm(e.target.value)} + onChange={(e: React.ChangeEvent) => + setSearchTerm(e.target.value) + } > @@ -106,20 +105,20 @@ export default new Template({}, ({ http, args }) => { - setSelectedStatus('all')}> + setSelectedStatus("all")}> 全部 - setSelectedStatus('pending')}> + setSelectedStatus("pending")}> 待审核 - setSelectedStatus('approved')}> + setSelectedStatus("approved")}> 已通过 - setSelectedStatus('rejected')}> + setSelectedStatus("rejected")}> 已拒绝 @@ -158,20 +157,22 @@ export default new Template({}, ({ http, args }) => { - - {comment.content} - + {comment.content} - - {comment.postTitle} - + {comment.postTitle} - {comment.status === 'approved' && 已通过} - {comment.status === 'pending' && 待审核} - {comment.status === 'rejected' && 已拒绝} + {comment.status === "approved" && ( + 已通过 + )} + {comment.status === "pending" && ( + 待审核 + )} + {comment.status === "rejected" && ( + 已拒绝 + )} @@ -179,25 +180,21 @@ export default new Template({}, ({ http, args }) => { - - - @@ -211,7 +208,10 @@ export default new Template({}, ({ http, args }) => { {/* 移动端列表视图 */}
{mockComments.map((comment) => ( - + 评论者 @@ -245,9 +245,15 @@ export default new Template({}, ({ http, args }) => { 状态 - {comment.status === 'approved' && 已通过} - {comment.status === 'pending' && 待审核} - {comment.status === 'rejected' && 已拒绝} + {comment.status === "approved" && ( + 已通过 + )} + {comment.status === "pending" && ( + 待审核 + )} + {comment.status === "rejected" && ( + 已拒绝 + )} @@ -263,25 +269,21 @@ export default new Template({}, ({ http, args }) => { 操作 - - - @@ -294,4 +296,4 @@ export default new Template({}, ({ http, args }) => { ); -}); \ No newline at end of file +}); diff --git a/frontend/app/dashboard/files.tsx b/frontend/app/dashboard/files.tsx index b9a0ee3..30477e6 100644 --- a/frontend/app/dashboard/files.tsx +++ b/frontend/app/dashboard/files.tsx @@ -1,17 +1,17 @@ import { Template } from "interface/template"; -import { - Container, - Heading, - Text, - Box, - Flex, +import { + Container, + Heading, + Text, + Box, + Flex, Table, Button, TextField, DropdownMenu, ScrollArea, Dialog, - DataList + DataList, } from "@radix-ui/themes"; import { PlusIcon, @@ -22,7 +22,7 @@ import { DotsHorizontalIcon, FileTextIcon, ImageIcon, - VideoIcon + VideoIcon, } from "@radix-ui/react-icons"; import { useState } from "react"; import type { Resource } from "interface/fields"; @@ -38,7 +38,7 @@ const mockFiles: Resource[] = [ fileType: "application/pdf", category: "documents", description: "前端开发规范文档", - createdAt: new Date("2024-03-15") + createdAt: new Date("2024-03-15"), }, { id: 2, @@ -49,19 +49,19 @@ const mockFiles: Resource[] = [ fileType: "image/png", category: "images", description: "项目整体架构示意图", - createdAt: new Date("2024-03-14") + createdAt: new Date("2024-03-14"), }, { id: 3, - authorId: "1", + 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") - } + createdAt: new Date("2024-03-13"), + }, ]; export default new Template({}, ({ http, args }) => { @@ -71,18 +71,19 @@ export default new Template({}, ({ http, args }) => { // 格式化文件大小 const formatFileSize = (bytes: number) => { - if (bytes === 0) return '0 Bytes'; + if (bytes === 0) return "0 Bytes"; const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const sizes = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; }; // 获取文件图标 const getFileIcon = (fileType: string) => { - if (fileType.startsWith('image/')) return ; - if (fileType.startsWith('video/')) return ; - if (fileType.startsWith('text/')) return ; + if (fileType.startsWith("image/")) return ; + if (fileType.startsWith("video/")) return ; + if (fileType.startsWith("text/")) + return ; return ; }; @@ -94,11 +95,9 @@ export default new Template({}, ({ http, args }) => { 文件管理 - - 共 {mockFiles.length} 个文件 - + 共 {mockFiles.length} 个文件 - - setSelectedType('all')}> + setSelectedType("all")}> 全部 - setSelectedType('documents')}> + setSelectedType("documents")}> 文档 - setSelectedType('images')}> + setSelectedType("images")}> 图片 - setSelectedType('others')}> + setSelectedType("others")}> 其他 @@ -172,12 +170,10 @@ export default new Template({}, ({ http, args }) => { {file.name} - - {formatFileSize(file.sizeBytes)} - + {formatFileSize(file.sizeBytes)} - {file.fileType.split('/')[1].toUpperCase()} + {file.fileType.split("/")[1].toUpperCase()} @@ -202,7 +198,10 @@ export default new Template({}, ({ http, args }) => { {/* 移动端列表视图 */}
{mockFiles.map((file) => ( - + 文件名 @@ -223,7 +222,7 @@ export default new Template({}, ({ http, args }) => { 类型 - {file.fileType.split('/')[1].toUpperCase()} + {file.fileType.split("/")[1].toUpperCase()} @@ -254,7 +253,10 @@ export default new Template({}, ({ http, args }) => { {/* 上传对话框 */} - + 上传文件 @@ -272,10 +274,7 @@ export default new Template({}, ({ http, args }) => { console.log(e.target.files); }} /> - ); -}); \ No newline at end of file +}); diff --git a/frontend/app/dashboard/index.tsx b/frontend/app/dashboard/index.tsx index 4447dd1..2a25a63 100644 --- a/frontend/app/dashboard/index.tsx +++ b/frontend/app/dashboard/index.tsx @@ -100,13 +100,13 @@ export default new Template({}, ({ http, args }) => { {stat.trend} - - - {stat.icon} - + {stat.icon} @@ -120,7 +120,7 @@ export default new Template({}, ({ http, args }) => { {recentPosts.map((post, index) => ( - @@ -150,14 +150,15 @@ export default new Template({}, ({ http, args }) => { - - {post.status === 'published' ? '已发布' : '草稿'} + {post.status === "published" ? "已发布" : "草稿"} @@ -166,4 +167,4 @@ export default new Template({}, ({ http, args }) => { ); -}); \ No newline at end of file +}); diff --git a/frontend/app/dashboard/layout.tsx b/frontend/app/dashboard/layout.tsx index ff4474b..d5db84b 100644 --- a/frontend/app/dashboard/layout.tsx +++ b/frontend/app/dashboard/layout.tsx @@ -159,7 +159,7 @@ export default new Layout(({ children }) => { {/* 主内容区域 */} {/* 顶部导航栏 */} - { )} - + {/* 退出登录按钮 */} -
); -}); \ No newline at end of file +}); diff --git a/frontend/app/dashboard/plugins.tsx b/frontend/app/dashboard/plugins.tsx index f5f9464..0cfeacb 100644 --- a/frontend/app/dashboard/plugins.tsx +++ b/frontend/app/dashboard/plugins.tsx @@ -1,10 +1,10 @@ import { Template } from "interface/template"; -import { - Container, - Heading, - Text, - Box, - Flex, +import { + Container, + Heading, + Text, + Box, + Flex, Card, Button, TextField, @@ -13,7 +13,7 @@ import { Dialog, Tabs, Switch, - IconButton + IconButton, } from "@radix-ui/themes"; import { PlusIcon, @@ -25,13 +25,17 @@ import { CheckIcon, UpdateIcon, TrashIcon, - ExclamationTriangleIcon + 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 })[] = [ +const mockPlugins: (PluginConfig & { + id: number; + preview?: string; + installed?: boolean; +})[] = [ { id: 1, name: "comment-system", @@ -41,7 +45,8 @@ const mockPlugins: (PluginConfig & { id: number; preview?: string; installed?: b 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", + preview: + "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format", managePath: "/dashboard/plugins/comment-system", installed: true, configuration: { @@ -51,11 +56,11 @@ const mockPlugins: (PluginConfig & { id: number; preview?: string; installed?: b data: { provider: "gitalk", clientId: "", - clientSecret: "" - } - } + clientSecret: "", + }, + }, }, - routes: new Set() + routes: new Set(), }, { id: 2, @@ -66,7 +71,8 @@ const mockPlugins: (PluginConfig & { id: number; preview?: string; installed?: b 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", + preview: + "https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?w=500&auto=format", installed: true, configuration: { system: { @@ -75,12 +81,12 @@ const mockPlugins: (PluginConfig & { id: number; preview?: string; installed?: b data: { quality: 80, maxWidth: 1920, - watermark: false - } - } + watermark: false, + }, + }, }, - routes: new Set() - } + routes: new Set(), + }, ]; // 模拟市场插件数据 @@ -104,9 +110,10 @@ const marketPlugins: MarketPlugin[] = [ version: "1.0.0", description: "自动优化上传的图片,支持压缩、裁剪、水印等功能", author: "ThirdParty", - preview: "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format", + preview: + "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format", downloads: 1200, - rating: 4.5 + rating: 4.5, }, { id: 5, @@ -115,22 +122,25 @@ const marketPlugins: MarketPlugin[] = [ version: "2.0.0", description: "增强的Markdown编辑器,支持更多扩展语法和实时预览", author: "ThirdParty", - preview: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=500&auto=format", + preview: + "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=500&auto=format", downloads: 3500, - rating: 4.8 - } + 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(null); + const [selectedPlugin, setSelectedPlugin] = useState< + (typeof mockPlugins)[0] | null + >(null); // 处理插件启用/禁用 const handleTogglePlugin = (pluginId: number) => { // 这里添加启用/禁用插件的逻辑 - console.log('Toggle plugin:', pluginId); + console.log("Toggle plugin:", pluginId); }; return ( @@ -145,7 +155,7 @@ export default new Template({}, ({ http, args }) => { 共 {mockPlugins.length} 个插件 - )} - @@ -249,10 +260,7 @@ export default new Template({}, ({ http, args }) => { console.log(e.target.files); }} /> -