diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..dfaac45 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,8 @@ +# Pika 开发约定 + +## Web 样式 + +- 不要直接使用 Tailwind CSS 工具类控制 Ant Design 组件的布局、尺寸、间距、溢出、颜色或内部子节点。Ant Design 组件的 `className` 不保证作用在期望的 DOM 节点,且组件样式优先级可能覆盖 Tailwind。 +- 需要 Tailwind 布局或视觉样式时,在 Ant Design 组件外层包裹原生 `div`、`span` 等元素,并将 Tailwind `className` 放在这些原生元素上。 +- 需要调整 Ant Design 组件本身时,优先使用组件自身的 props、`style`、`styles`、`classNames`、设计 Token,或 Ant Design 的 `Flex`、`Space`、`Row`、`Col` 等布局组件。 +- 如果一个样式同时涉及 Ant Design 组件外部布局和组件内部外观,将两者分开:外部布局由包裹元素承担,组件内部外观由 Ant Design API 承担。 diff --git a/web/src/pages/Settings/ThemeSettings.tsx b/web/src/pages/Settings/ThemeSettings.tsx index 8b7f1d0..a94e9e1 100644 --- a/web/src/pages/Settings/ThemeSettings.tsx +++ b/web/src/pages/Settings/ThemeSettings.tsx @@ -1,18 +1,20 @@ import {useEffect, useState} from 'react'; -import {Alert, App, Button, Card, Col, Image, Popconfirm, Row, Space, Spin, Tag, Typography, Upload} from 'antd'; +import {Alert, App, Button, Card, Empty, Popconfirm, Space, Spin, Tag, Typography, Upload} from 'antd'; import {Palette, ShieldAlert, Trash2, UploadCloud} from 'lucide-react'; import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; import {activateTheme, deleteTheme, listThemes, type ThemeInfo, uploadTheme} from '@/api/theme'; import {getErrorMessage} from '@/lib/utils'; -const {Text, Paragraph} = Typography; +const {Text, Paragraph, Title} = Typography; const ThemePreview = ({theme}: {theme: ThemeInfo}) => { const [source, setSource] = useState(); + useEffect(() => { const controller = new AbortController(); let objectURL = ''; const token = localStorage.getItem('token'); + fetch(theme.previewUrl, { signal: controller.signal, headers: token ? {Authorization: 'Bearer ' + token} : {}, @@ -23,36 +25,60 @@ const ThemePreview = ({theme}: {theme: ThemeInfo}) => { setSource(objectURL); }) .catch(() => setSource(undefined)); + return () => { controller.abort(); if (objectURL) URL.revokeObjectURL(objectURL); }; }, [theme.id, theme.previewUrl]); - return source - ? {theme.name}/ - :
; + + return ( +
+ {source ? ( + {`${theme.name} + ) : ( +
+ + 暂无预览图 +
+ )} +
+ ); }; const ThemeSettings = () => { const {message, modal} = App.useApp(); const queryClient = useQueryClient(); const themesQuery = useQuery({queryKey: ['themes'], queryFn: listThemes}); + const themes = themesQuery.data || []; const refresh = () => queryClient.invalidateQueries({queryKey: ['themes']}); const operation = useMutation({ mutationFn: async (fn: () => Promise) => fn(), - onSuccess: async () => { message.success('操作成功'); await refresh(); }, + onSuccess: async () => { + message.success('操作成功'); + await refresh(); + }, onError: (error) => message.error(getErrorMessage(error, '主题操作失败')), }); - const confirmUntrusted = (title: string, action: () => Promise) => { + const confirmTrustedTheme = (title: string, action: () => Promise) => { modal.confirm({ title, - icon: , + icon: , width: 560, content: ( - +
+ +
), okText: '我信任此主题,继续', okButtonProps: {danger: true}, @@ -60,51 +86,131 @@ const ThemeSettings = () => { }); }; - const installedCards = (themesQuery.data || []).map((theme) => ( - - } - actions={[ - theme.active ? 当前主题 : - , - theme.official ? 内置 : - operation.mutateAsync(() => deleteTheme(theme.id))}> - - , - ]}> - {theme.name}{theme.version}{theme.official && 官方}} - description={<> - {theme.description || '暂无说明'} - 作者:{theme.author} - {!theme.compatible && } - }/> - - - )); + const renderThemeActions = (theme: ThemeInfo) => [ + theme.active ? ( + 当前主题 + ) : ( + + ), + theme.official ? ( + 内置 + ) : ( + operation.mutateAsync(() => deleteTheme(theme.id))} + > + + + ), + ]; return ( -
-
-
-

主题管理

-

安装和切换完整的公开页面主题;管理后台始终使用官方界面。

+
+
+
+ 主题管理 + + 安装和切换完整的公开页面主题;管理后台始终使用官方界面。 +
- + { - confirmUntrusted('上传并安装 ' + file.name, () => uploadTheme(file as File)); + confirmTrustedTheme('上传并安装 ' + file.name, () => uploadTheme(file as File)); return false; - }}> - - + }} + > + + +
- - {themesQuery.isLoading - ?
- : {installedCards}} + +
+ +
+ + {themesQuery.isLoading ? ( +
+ +
+ +
+ ) : themesQuery.isError ? ( + themesQuery.refetch()}>重试} + /> + ) : themes.length === 0 ? ( +
+ +
+ ) : ( +
+ {themes.map((theme) => ( +
+ } + actions={renderThemeActions(theme)} + styles={{ + root: {height: '100%', overflow: 'hidden'}, + body: {padding: 16}, + actions: {marginTop: 'auto'}, + cover: {overflow: 'hidden'}, + }} + > + + {theme.name} + {theme.version} + {theme.official && 官方} + + )} + description={( + + + {theme.description || '暂无说明'} + + 作者:{theme.author} + {!theme.compatible && ( + + )} + + )} + /> + +
+ ))} +
+ )}
); };