diff --git a/.dumi/theme/common/Loading.tsx b/.dumi/theme/common/Loading.tsx index 337db990f9..1d0eeabd9d 100644 --- a/.dumi/theme/common/Loading.tsx +++ b/.dumi/theme/common/Loading.tsx @@ -1,15 +1,21 @@ import React from 'react'; +import { StyleProvider } from '@ant-design/cssinjs'; import { Flex, Skeleton, Spin } from 'antd'; import { useLocation } from 'dumi'; +import { Common } from './styles'; + const Loading: React.FC = () => { const { pathname } = useLocation(); + + let loadingNode: React.ReactNode = null; + if ( pathname.startsWith('/components') || pathname.startsWith('/docs') || pathname.startsWith('/changelog') ) { - return ( + loadingNode = (
{
); + } else { + loadingNode = ( + + + + ); } + // Loading 组件独立于 GlobalLayout,而它也会影响站点样式。 + // 所以我们这边需要 hardcode 一下启动 layer。 return ( - - - + + + {loadingNode} + ); }; diff --git a/.dumi/theme/common/ThemeSwitch/PromptDrawer.tsx b/.dumi/theme/common/ThemeSwitch/PromptDrawer.tsx new file mode 100644 index 0000000000..9a42827468 --- /dev/null +++ b/.dumi/theme/common/ThemeSwitch/PromptDrawer.tsx @@ -0,0 +1,108 @@ +import React, { useRef, useState } from 'react'; +import { AntDesignOutlined, UserOutlined } from '@ant-design/icons'; +import { Bubble, Sender } from '@ant-design/x'; +import { Drawer, Flex, GetProp, Typography } from 'antd'; + +import useLocale from '../../../hooks/useLocale'; +import type { SiteContextProps } from '../../../theme/slots/SiteContext'; +import usePromptTheme from './usePromptTheme'; + +const locales = { + cn: { + title: 'AI 生成主题', + finishTips: '生成完成,对话以重新生成。', + }, + en: { + title: 'AI Theme Generator', + finishTips: 'Completed. Regenerate by start a new conversation.', + }, +}; + +export interface PromptDrawerProps { + open: boolean; + onClose: () => void; + onThemeChange?: (themeConfig: SiteContextProps['dynamicTheme']) => void; +} + +const PromptDrawer: React.FC = ({ open, onClose, onThemeChange }) => { + const [locale] = useLocale(locales); + const [inputValue, setInputValue] = useState(''); + const senderRef = useRef(null); + + const [submitPrompt, loading, prompt, resText, cancelRequest] = usePromptTheme(onThemeChange); + + const handleSubmit = (value: string) => { + submitPrompt(value); + setInputValue(''); + }; + + const handleAfterOpenChange = (isOpen: boolean) => { + if (isOpen && senderRef.current) { + // Focus the Sender component when drawer opens + senderRef.current.focus?.(); + } + }; + + const items = React.useMemo>(() => { + if (!prompt) { + return []; + } + + const nextItems: GetProp = [ + { + placement: 'end', + content: prompt, + avatar: { icon: }, + shape: 'corner', + }, + { + placement: 'start', + content: resText, + avatar: { icon: }, + loading: !resText, + messageRender: (content: string) => ( + +
{content}
+
+ ), + }, + ]; + + if (!loading) { + nextItems.push({ + placement: 'start', + content: locale.finishTips, + avatar: { icon: }, + shape: 'corner', + }); + } + + return nextItems; + }, [prompt, resText, loading]); + + return ( + + + + + + + ); +}; + +export default PromptDrawer; diff --git a/.dumi/theme/common/ThemeSwitch/index.tsx b/.dumi/theme/common/ThemeSwitch/index.tsx index cce622ed76..d93afe5d29 100644 --- a/.dumi/theme/common/ThemeSwitch/index.tsx +++ b/.dumi/theme/common/ThemeSwitch/index.tsx @@ -1,5 +1,11 @@ -import React, { use, useRef } from 'react'; -import { BgColorsOutlined, LinkOutlined, SmileOutlined, SunOutlined } from '@ant-design/icons'; +import React, { use, useRef, useState } from 'react'; +import { + BgColorsOutlined, + LinkOutlined, + ShopOutlined, + SmileOutlined, + SunOutlined, +} from '@ant-design/icons'; import { Badge, Button, Dropdown } from 'antd'; import type { MenuProps } from 'antd'; import { CompactTheme, DarkTheme } from 'antd-token-previewer/es/icons'; @@ -10,6 +16,7 @@ import type { SiteContextProps } from '../../slots/SiteContext'; import SiteContext from '../../slots/SiteContext'; import { getLocalizedPathname, isZhCN } from '../../utils'; import Link from '../Link'; +import PromptDrawer from './PromptDrawer'; import ThemeIcon from './ThemeIcon'; export type ThemeName = 'light' | 'dark' | 'compact' | 'motion-off' | 'happy-work'; @@ -20,9 +27,10 @@ export interface ThemeSwitchProps { const ThemeSwitch: React.FC = () => { const { pathname, search } = useLocation(); - const { theme, updateSiteConfig } = use(SiteContext); + const { theme, updateSiteConfig, dynamicTheme } = use(SiteContext); const toggleAnimationTheme = useThemeAnimation(); const lastThemeKey = useRef(theme.includes('dark') ? 'dark' : 'light'); + const [isMarketDrawerOpen, setIsMarketDrawerOpen] = useState(false); const badge = ; @@ -61,6 +69,12 @@ const ThemeSwitch: React.FC = () => { { type: 'divider', }, + { + id: 'app.theme.switch.market', + icon: , + key: 'market', + showBadge: () => !!dynamicTheme, + }, { id: 'app.footer.theme', icon: , @@ -95,8 +109,25 @@ const ThemeSwitch: React.FC = () => { // 处理主题切换 const handleThemeChange = (key: string, domEvent: React.MouseEvent) => { - // 主题编辑器特殊处理 - if (key === 'theme-editor') { + // 查找对应的选项配置 + const option = themeOptions.find((opt) => opt.key === key); + + // 链接类型的菜单项特殊处理,不执行主题切换逻辑 + if (option?.isLink) { + return; + } + + // Market 选项特殊处理 + if (key === 'market') { + // 如果已经有动态主题,点击时清除动态主题 + if (dynamicTheme) { + updateSiteConfig({ + dynamicTheme: undefined, + }); + } else { + // 否则打开 Drawer 生成新主题 + setIsMarketDrawerOpen(true); + } return; } @@ -131,9 +162,21 @@ const ThemeSwitch: React.FC = () => { }; return ( - -