mirror of
https://github.com/ant-design/ant-design.git
synced 2026-02-14 21:39:20 +08:00
Compare commits
10 Commits
copilot/fi
...
docs/theme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4aadae9bf6 | ||
|
|
7868764b90 | ||
|
|
276a8921cf | ||
|
|
fde6fa9795 | ||
|
|
3dbba0e639 | ||
|
|
23e2c4a472 | ||
|
|
73c6168e3f | ||
|
|
87f59266e5 | ||
|
|
caf62f4e56 | ||
|
|
8f95529432 |
@@ -46,6 +46,8 @@ export interface GroupProps {
|
||||
decoration?: React.ReactNode;
|
||||
/** 预加载的背景图片列表 */
|
||||
backgroundPrefetchList?: string[];
|
||||
/** 标题右侧的操作按钮 */
|
||||
extra?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Group: React.FC<React.PropsWithChildren<GroupProps>> = (props) => {
|
||||
@@ -59,6 +61,7 @@ const Group: React.FC<React.PropsWithChildren<GroupProps>> = (props) => {
|
||||
background,
|
||||
collapse,
|
||||
backgroundPrefetchList,
|
||||
extra,
|
||||
} = props;
|
||||
|
||||
// 预加载背景图片
|
||||
@@ -87,18 +90,29 @@ const Group: React.FC<React.PropsWithChildren<GroupProps>> = (props) => {
|
||||
<div className={styles.container}>{decoration}</div>
|
||||
<GroupMaskLayer style={{ paddingBlock: token.marginFarSM }}>
|
||||
<div className={styles.typographyWrapper}>
|
||||
<Typography.Title
|
||||
id={id}
|
||||
level={1}
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 900,
|
||||
color: titleColor,
|
||||
// Special for the title
|
||||
fontSize: isMobile ? token.fontSizeHeading2 : token.fontSizeHeading1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: token.paddingXS,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography.Title>
|
||||
<Typography.Title
|
||||
id={id}
|
||||
level={1}
|
||||
style={{
|
||||
fontWeight: 900,
|
||||
color: titleColor,
|
||||
margin: 0,
|
||||
// Special for the title
|
||||
fontSize: isMobile ? token.fontSizeHeading2 : token.fontSizeHeading1,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography.Title>
|
||||
{extra}
|
||||
</div>
|
||||
<Typography.Paragraph
|
||||
style={{
|
||||
color: titleColor,
|
||||
|
||||
@@ -4,15 +4,16 @@ import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
ColorPicker,
|
||||
ConfigProvider,
|
||||
DatePicker,
|
||||
Dropdown,
|
||||
Flex,
|
||||
Modal,
|
||||
Progress,
|
||||
Radio,
|
||||
Segmented,
|
||||
Select,
|
||||
Slider,
|
||||
Space,
|
||||
@@ -26,6 +27,7 @@ import clsx from 'clsx';
|
||||
import useLocale from '../../../../hooks/useLocale';
|
||||
|
||||
const { _InternalPanelDoNotUseOrYouWillBeFired: ModalPanel } = Modal;
|
||||
const { Group: RadioButtonGroup, Button: RadioButton } = Radio;
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
@@ -48,6 +50,9 @@ const locales = {
|
||||
icon: '图标按钮',
|
||||
hello: '你好,Ant Design!',
|
||||
release: 'Ant Design 6.0 正式发布!',
|
||||
segmentedDaily: '每日',
|
||||
segmentedWeekly: '每周',
|
||||
segmentedMonthly: '每月',
|
||||
},
|
||||
en: {
|
||||
range: 'Set Range',
|
||||
@@ -69,6 +74,9 @@ const locales = {
|
||||
icon: 'Icon',
|
||||
hello: 'Hello, Ant Design!',
|
||||
release: 'Ant Design 6.0 is released!',
|
||||
segmentedDaily: 'Daily',
|
||||
segmentedWeekly: 'Weekly',
|
||||
segmentedMonthly: 'Monthly',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -95,7 +103,7 @@ const ComponentsBlock: React.FC<ComponentsBlockProps> = (props) => {
|
||||
|
||||
return (
|
||||
<ConfigProvider {...config}>
|
||||
<Card className={clsx(containerClassName, styles.container)}>
|
||||
<div className={clsx(containerClassName, styles.container)}>
|
||||
<App>
|
||||
<Flex vertical gap="middle" style={style} className={className}>
|
||||
<ModalPanel title="Ant Design" width="100%">
|
||||
@@ -120,13 +128,32 @@ const ComponentsBlock: React.FC<ComponentsBlockProps> = (props) => {
|
||||
</Space.Compact>
|
||||
</div>
|
||||
|
||||
<ColorPicker style={{ flex: 'none' }} />
|
||||
<ColorPicker showText defaultValue="#1677ff" style={{ flex: 'none' }} />
|
||||
|
||||
<Select
|
||||
style={{ flex: 'auto' }}
|
||||
mode="multiple"
|
||||
maxTagCount="responsive"
|
||||
defaultValue={[{ value: 'apple' }, { value: 'banana' }]}
|
||||
defaultValue={['apple', 'banana']}
|
||||
options={[
|
||||
{ value: 'apple', label: locale.apple },
|
||||
{ value: 'banana', label: locale.banana },
|
||||
{ value: 'orange', label: locale.orange },
|
||||
{ value: 'watermelon', label: locale.watermelon },
|
||||
]}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{/* Filled variants */}
|
||||
<Flex gap="middle">
|
||||
<DatePicker variant="filled" />
|
||||
|
||||
<Select
|
||||
variant="filled"
|
||||
style={{ flex: 'auto' }}
|
||||
mode="multiple"
|
||||
maxTagCount="responsive"
|
||||
defaultValue={['apple', 'banana']}
|
||||
options={[
|
||||
{ value: 'apple', label: locale.apple },
|
||||
{ value: 'banana', label: locale.banana },
|
||||
@@ -147,20 +174,7 @@ const ComponentsBlock: React.FC<ComponentsBlockProps> = (props) => {
|
||||
]}
|
||||
/>
|
||||
{/* Line */}
|
||||
<Slider
|
||||
style={{ marginInline: 20 }}
|
||||
range
|
||||
marks={{
|
||||
0: '0°C',
|
||||
26: '26°C',
|
||||
37: '37°C',
|
||||
100: {
|
||||
style: { color: '#f50' },
|
||||
label: <strong>100°C</strong>,
|
||||
},
|
||||
}}
|
||||
defaultValue={[26, 37]}
|
||||
/>
|
||||
<Slider defaultValue={50} />
|
||||
{/* Line */}
|
||||
<Flex gap="middle">
|
||||
<Button type="primary" className={styles.flexAuto}>
|
||||
@@ -188,9 +202,20 @@ const ComponentsBlock: React.FC<ComponentsBlockProps> = (props) => {
|
||||
/>
|
||||
<Radio.Group defaultValue={locale.apple} options={[locale.apple, locale.banana]} />
|
||||
</Flex>
|
||||
<Flex gap="middle" align="center">
|
||||
<RadioButtonGroup defaultValue="a">
|
||||
<RadioButton value="a">A</RadioButton>
|
||||
<RadioButton value="b">B</RadioButton>
|
||||
<RadioButton value="c">C</RadioButton>
|
||||
</RadioButtonGroup>
|
||||
<Segmented
|
||||
defaultValue={locale.segmentedDaily}
|
||||
options={[locale.segmentedDaily, locale.segmentedWeekly, locale.segmentedMonthly]}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</App>
|
||||
</Card>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,11 +13,15 @@ const locales = {
|
||||
cn: {
|
||||
themeTitle: '定制主题,随心所欲',
|
||||
themeDesc: '开放样式算法与语义化结构,让你与 AI 一起轻松定制主题',
|
||||
aiGenerate: 'AI 生成主题',
|
||||
aiGenerateDesc: '用一句话描述你想要的风格',
|
||||
},
|
||||
en: {
|
||||
themeTitle: 'Flexible theme customization',
|
||||
themeDesc:
|
||||
'Open style algorithms and semantic structures make it easy for you and AI to customize themes',
|
||||
aiGenerate: 'AI Generate Theme',
|
||||
aiGenerateDesc: 'Describe your desired style',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -40,7 +44,7 @@ const useStyles = createStyles(({ css, cssVar }) => ({
|
||||
listStyleType: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: cssVar.paddingMD,
|
||||
gap: cssVar.paddingSM,
|
||||
}),
|
||||
listItem: css({
|
||||
margin: 0,
|
||||
@@ -53,7 +57,7 @@ const useStyles = createStyles(({ css, cssVar }) => ({
|
||||
borderColor: 'transparent',
|
||||
transition: `all ${cssVar.motionDurationMid} ${cssVar.motionEaseInOut}`,
|
||||
|
||||
'&:hover:not(.active)': {
|
||||
'&:hover:not(.active):not(.ai-generate-item)': {
|
||||
borderColor: cssVar.colorPrimaryBorder,
|
||||
backgroundColor: cssVar.colorPrimaryBg,
|
||||
cursor: 'pointer',
|
||||
@@ -82,6 +86,39 @@ const useStyles = createStyles(({ css, cssVar }) => ({
|
||||
},
|
||||
}),
|
||||
|
||||
// AI Generate Item
|
||||
aiGenerateItem: css({
|
||||
borderStyle: 'dashed',
|
||||
opacity: 0.7,
|
||||
cursor: 'pointer',
|
||||
color: cssVar.colorTextSecondary,
|
||||
paddingInline: cssVar.padding,
|
||||
|
||||
'&:hover': {
|
||||
borderColor: cssVar.colorPrimary,
|
||||
color: cssVar.colorPrimary,
|
||||
opacity: 1,
|
||||
},
|
||||
}),
|
||||
|
||||
aiGenerateContent: css({
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
}),
|
||||
|
||||
aiGenerateIcon: css({
|
||||
fontSize: 14,
|
||||
marginRight: 6,
|
||||
opacity: 0.6,
|
||||
}),
|
||||
|
||||
aiGenerateDesc: css({
|
||||
fontSize: cssVar.fontSizeSM,
|
||||
opacity: 0.5,
|
||||
marginTop: 2,
|
||||
fontWeight: 400,
|
||||
}),
|
||||
|
||||
// Components
|
||||
componentsBlockContainer: css({
|
||||
flex: 'auto',
|
||||
@@ -98,7 +135,12 @@ const useStyles = createStyles(({ css, cssVar }) => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
export default function ThemePreview() {
|
||||
export interface ThemePreviewProps {
|
||||
onOpenPromptDrawer?: () => void;
|
||||
}
|
||||
|
||||
export default function ThemePreview(props: ThemePreviewProps = {}) {
|
||||
const { onOpenPromptDrawer } = props;
|
||||
const [locale] = useLocale(locales);
|
||||
const { styles } = useStyles();
|
||||
const isDark = React.use(DarkContext);
|
||||
@@ -111,10 +153,7 @@ export default function ThemePreview() {
|
||||
const defaultThemeName = isDark ? 'dark' : 'light';
|
||||
|
||||
const targetTheme =
|
||||
process.env.NODE_ENV !== 'production'
|
||||
? previewThemes[previewThemes.length - 1].name
|
||||
: previewThemes.find((theme) => theme.key === defaultThemeName)?.name ||
|
||||
previewThemes[0].name;
|
||||
previewThemes.find((theme) => theme.key === defaultThemeName)?.name || previewThemes[0].name;
|
||||
|
||||
setActiveName(targetTheme);
|
||||
}, [isDark]);
|
||||
@@ -148,24 +187,50 @@ export default function ThemePreview() {
|
||||
backgroundPrefetchList={backgroundPrefetchList}
|
||||
>
|
||||
<Flex className={styles.container} gap="large">
|
||||
<div className={styles.list} role="tablist" aria-label="Theme selection">
|
||||
{previewThemes.map((theme) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<div className={styles.list} role="tablist" aria-label="Theme selection">
|
||||
{previewThemes.map((theme) => (
|
||||
<div
|
||||
className={clsx(
|
||||
styles.listItem,
|
||||
activeName === theme.name && 'active',
|
||||
activeTheme?.bgImgDark && 'dark',
|
||||
)}
|
||||
key={theme.name}
|
||||
role="tab"
|
||||
tabIndex={activeName === theme.name ? 0 : -1}
|
||||
aria-selected={activeName === theme.name}
|
||||
onClick={() => handleThemeClick(theme.name)}
|
||||
onKeyDown={(event) => handleKeyDown(event, theme.name)}
|
||||
style={{ marginBottom: 8 }}
|
||||
>
|
||||
{theme.name}
|
||||
</div>
|
||||
))}
|
||||
{/* AI 生成主题 - 最后一个选项 */}
|
||||
<div
|
||||
className={clsx(
|
||||
styles.listItem,
|
||||
activeName === theme.name && 'active',
|
||||
activeTheme?.bgImgDark && 'dark',
|
||||
)}
|
||||
key={theme.name}
|
||||
className={clsx(styles.listItem, styles.aiGenerateItem, 'ai-generate-item')}
|
||||
role="tab"
|
||||
tabIndex={activeName === theme.name ? 0 : -1}
|
||||
aria-selected={activeName === theme.name}
|
||||
onClick={() => handleThemeClick(theme.name)}
|
||||
onKeyDown={(event) => handleKeyDown(event, theme.name)}
|
||||
tabIndex={0}
|
||||
onClick={onOpenPromptDrawer}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
onOpenPromptDrawer?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{theme.name}
|
||||
<div className={styles.aiGenerateContent}>
|
||||
<span className={styles.aiGenerateIcon}>🎨</span>
|
||||
<span>{locale.aiGenerate}</span>
|
||||
</div>
|
||||
<div className={styles.aiGenerateDesc}>{locale.aiGenerateDesc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ComponentsBlock
|
||||
key={activeName}
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { UseTheme } from '.';
|
||||
|
||||
const useStyles = createStyles(({ css, cssVar }) => {
|
||||
const glassBorder = {
|
||||
border: `${cssVar.lineWidth} solid rgba(255,255,255,0.3)`,
|
||||
boxShadow: [
|
||||
`${cssVar.boxShadowSecondary}`,
|
||||
`inset 0 0 5px 2px rgba(255, 255, 255, 0.3)`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import React, { Suspense, useState } from 'react';
|
||||
import { theme } from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
|
||||
@@ -8,6 +8,9 @@ import BannerRecommends from './components/BannerRecommends';
|
||||
import Group from './components/Group';
|
||||
import PreviewBanner from './components/PreviewBanner';
|
||||
import ThemePreview from './components/ThemePreview';
|
||||
import PromptDrawer from '../../theme/common/ThemeSwitch/PromptDrawer';
|
||||
import SiteContext from '../../theme/slots/SiteContext';
|
||||
import type { SiteContextProps } from '../../theme/slots/SiteContext';
|
||||
|
||||
const ComponentsList = React.lazy(() => import('./components/ComponentsList'));
|
||||
const DesignFramework = React.lazy(() => import('./components/DesignFramework'));
|
||||
@@ -42,6 +45,16 @@ const Homepage: React.FC = () => {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const isDark = React.use(DarkContext);
|
||||
const [promptDrawerOpen, setPromptDrawerOpen] = useState(false);
|
||||
const siteContext = React.use(SiteContext);
|
||||
|
||||
const handlePromptDrawerOpen = () => setPromptDrawerOpen(true);
|
||||
const handlePromptDrawerClose = () => setPromptDrawerOpen(false);
|
||||
const handleThemeChange = (themeConfig: SiteContextProps['dynamicTheme']) => {
|
||||
if (siteContext?.updateSiteConfig) {
|
||||
siteContext.updateSiteConfig({ dynamicTheme: themeConfig });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
@@ -49,13 +62,14 @@ const Homepage: React.FC = () => {
|
||||
<BannerRecommends />
|
||||
</PreviewBanner>
|
||||
|
||||
{/* 定制主题 */}
|
||||
{/* <ConfigProvider theme={{ algorithm: theme.defaultAlgorithm }}>
|
||||
<Suspense fallback={null}>
|
||||
<Theme />
|
||||
</Suspense>
|
||||
</ConfigProvider> */}
|
||||
<ThemePreview />
|
||||
<ThemePreview onOpenPromptDrawer={handlePromptDrawerOpen} />
|
||||
|
||||
{/* AI 生成主题抽屉 */}
|
||||
<PromptDrawer
|
||||
open={promptDrawerOpen}
|
||||
onClose={handlePromptDrawerClose}
|
||||
onThemeChange={handleThemeChange}
|
||||
/>
|
||||
|
||||
{/* 组件列表 */}
|
||||
<Group
|
||||
@@ -90,55 +104,6 @@ const Homepage: React.FC = () => {
|
||||
</Group>
|
||||
</section>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <section>
|
||||
// <PreviewBanner>
|
||||
// <BannerRecommends />
|
||||
// </PreviewBanner>
|
||||
|
||||
// <div>
|
||||
// {/* 定制主题 */}
|
||||
// <ConfigProvider theme={{ algorithm: theme.defaultAlgorithm }}>
|
||||
// <Suspense fallback={null}>
|
||||
// <Theme />
|
||||
// </Suspense>
|
||||
// </ConfigProvider>
|
||||
|
||||
// {/* 组件列表 */}
|
||||
// <Group
|
||||
// background={token.colorBgElevated}
|
||||
// collapse
|
||||
// title={locale.assetsTitle}
|
||||
// description={locale.assetsDesc}
|
||||
// id="design"
|
||||
// >
|
||||
// <Suspense fallback={null}>
|
||||
// <ComponentsList />
|
||||
// </Suspense>
|
||||
// </Group>
|
||||
|
||||
// {/* 设计语言 */}
|
||||
// <Group
|
||||
// title={locale.designTitle}
|
||||
// description={locale.designDesc}
|
||||
// background={isDark ? '#393F4A' : '#F5F8FF'}
|
||||
// decoration={
|
||||
// <img
|
||||
// draggable={false}
|
||||
// className={classNames.image}
|
||||
// src="https://gw.alipayobjects.com/zos/bmw-prod/ba37a413-28e6-4be4-b1c5-01be1a0ebb1c.svg"
|
||||
// alt="bg"
|
||||
// />
|
||||
// }
|
||||
// >
|
||||
// <Suspense fallback={null}>
|
||||
// <DesignFramework />
|
||||
// </Suspense>
|
||||
// </Group>
|
||||
// </div>
|
||||
// </section>
|
||||
// );
|
||||
};
|
||||
|
||||
export default Homepage;
|
||||
|
||||
@@ -1,22 +1,46 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { AntDesignOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Bubble, Sender } from '@ant-design/x';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import { Bubble, Prompts, Sender, Welcome } from '@ant-design/x';
|
||||
import type { BubbleItemType } from '@ant-design/x/es/bubble/interface';
|
||||
import type { PromptsItemType } from '@ant-design/x';
|
||||
import type { SenderRef } from '@ant-design/x/es/sender';
|
||||
import { Drawer, Flex, Typography } from 'antd';
|
||||
import type { GetProp } from 'antd';
|
||||
import { Button, Divider, Drawer, Flex, Skeleton, Splitter, Typography } from 'antd';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import type { SiteContextProps } from '../../../theme/slots/SiteContext';
|
||||
import SiteContext from '../../../theme/slots/SiteContext';
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import ComponentsBlock from '../../../pages/index/components/ThemePreview/ComponentsBlock';
|
||||
import usePromptTheme from './usePromptTheme';
|
||||
import usePromptRecommend from './usePromptRecommend';
|
||||
|
||||
const antdLogoSrc = 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg';
|
||||
|
||||
const THEME_EMOJIS = ['🌅', '🌊', '🌿', '🍂', '🌸', '🌌', '🎨', '⚡', '🔮', '🪐'];
|
||||
|
||||
const getEmojiForTheme = (index: number) => THEME_EMOJIS[index % THEME_EMOJIS.length];
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
title: 'AI 生成主题',
|
||||
finishTips: '生成完成,对话以重新生成。',
|
||||
title: '🎨 AI 生成主题',
|
||||
finishTips: '生成主题完成,已应用',
|
||||
placeholder: '描述你想要的主题风格,如:温暖阳光、清新自然、科技感...',
|
||||
welcomeTitle: 'AI 主题生成器',
|
||||
welcomeDescription: '描述你想要的风格,我会为你生成专属主题',
|
||||
recommendTitle: '推荐主题',
|
||||
loading: '加载中...',
|
||||
refresh: '换一换',
|
||||
resetToDefault: '恢复默认主题',
|
||||
},
|
||||
en: {
|
||||
title: 'AI Theme Generator',
|
||||
finishTips: 'Completed. Regenerate by start a new conversation.',
|
||||
title: '🎨 AI Theme Generator',
|
||||
finishTips: 'Theme generated and applied',
|
||||
placeholder: 'Describe your desired theme style, e.g., warm sunny, fresh natural, tech feel...',
|
||||
welcomeTitle: 'AI Theme Generator',
|
||||
welcomeDescription: 'Describe your desired style and I will generate a custom theme for you',
|
||||
recommendTitle: 'Recommended Themes',
|
||||
loading: 'Loading...',
|
||||
refresh: 'Refresh',
|
||||
resetToDefault: 'Reset to default theme',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -26,17 +50,40 @@ export interface PromptDrawerProps {
|
||||
onThemeChange?: (themeConfig: SiteContextProps['dynamicTheme']) => void;
|
||||
}
|
||||
|
||||
// Extended type for Prompts items with additional properties
|
||||
interface ExtendedPromptsItemType extends PromptsItemType {
|
||||
originalDescription?: string;
|
||||
isRefresh?: boolean;
|
||||
}
|
||||
|
||||
const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChange }) => {
|
||||
const [locale] = useLocale(locales);
|
||||
const { updateSiteConfig, isDark } = React.use(SiteContext) as SiteContextProps;
|
||||
const [locale, localeKey] = useLocale(locales);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const senderRef = useRef<SenderRef>(null);
|
||||
|
||||
const [submitPrompt, loading, prompt, resText, cancelRequest] = usePromptTheme(onThemeChange);
|
||||
const {
|
||||
recommendations,
|
||||
loading: recommendLoading,
|
||||
fetch: fetchRecommendations,
|
||||
} = usePromptRecommend(localeKey);
|
||||
|
||||
const handleSubmit = (value: string) => {
|
||||
submitPrompt(value);
|
||||
setInputValue('');
|
||||
const handleSubmit = React.useCallback(
|
||||
(value: string) => {
|
||||
submitPrompt(value);
|
||||
setInputValue('');
|
||||
},
|
||||
[submitPrompt],
|
||||
);
|
||||
|
||||
const handleRefreshRecommendations = React.useCallback(() => {
|
||||
fetchRecommendations(`prompt-drawer-refresh-${Date.now()}`);
|
||||
}, [fetchRecommendations]);
|
||||
|
||||
const handleResetToDefaultTheme = () => {
|
||||
updateSiteConfig({ dynamicTheme: undefined });
|
||||
};
|
||||
|
||||
const handleAfterOpenChange = (isOpen: boolean) => {
|
||||
@@ -44,14 +91,18 @@ const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChang
|
||||
// Focus the Sender component when drawer opens
|
||||
senderRef.current.focus?.();
|
||||
}
|
||||
// Fetch AI recommendations when drawer opens
|
||||
if (isOpen) {
|
||||
fetchRecommendations('prompt-drawer-init');
|
||||
}
|
||||
};
|
||||
|
||||
const items = React.useMemo<GetProp<typeof Bubble.List, 'items'>>(() => {
|
||||
const items = React.useMemo<BubbleItemType[]>(() => {
|
||||
if (!prompt) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const nextItems: GetProp<typeof Bubble.List, 'items'> = [
|
||||
const nextItems: BubbleItemType[] = [
|
||||
{
|
||||
key: 1,
|
||||
role: 'user',
|
||||
@@ -62,54 +113,274 @@ const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChang
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
role: 'system',
|
||||
role: 'ai',
|
||||
placement: 'start',
|
||||
content: resText,
|
||||
avatar: <AntDesignOutlined />,
|
||||
avatar: <img src={antdLogoSrc} alt="Ant Design" style={{ width: 28, height: 28 }} />,
|
||||
loading: !resText,
|
||||
contentRender: (content: string) => (
|
||||
<Typography>
|
||||
<pre style={{ margin: 0 }}>{content}</pre>
|
||||
<pre
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '16px',
|
||||
borderRadius: 8,
|
||||
background: isDark
|
||||
? 'linear-gradient(135deg, rgba(90,196,255,0.08) 0%, rgba(174,136,255,0.08) 100%)'
|
||||
: 'linear-gradient(135deg, #f2f9fe 0%, #f7f3ff 100%)',
|
||||
fontSize: 13,
|
||||
lineHeight: 1.6,
|
||||
border: 'none',
|
||||
color: isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)',
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</pre>
|
||||
</Typography>
|
||||
),
|
||||
styles: {
|
||||
content: {
|
||||
background: 'transparent',
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (!loading) {
|
||||
nextItems.push({
|
||||
key: 3,
|
||||
role: 'divider',
|
||||
role: 'system',
|
||||
placement: 'start',
|
||||
shape: 'round',
|
||||
content: locale.finishTips,
|
||||
avatar: <AntDesignOutlined />,
|
||||
shape: 'corner',
|
||||
});
|
||||
|
||||
// Add recommended themes prompts
|
||||
const recommendedPrompts: ExtendedPromptsItemType[] = recommendations
|
||||
.slice(0, 4)
|
||||
.map((text, index) => ({
|
||||
key: `rec-${text}`,
|
||||
description: `${getEmojiForTheme(index)} ${text}`,
|
||||
originalDescription: text,
|
||||
}));
|
||||
|
||||
// Add refresh button
|
||||
recommendedPrompts.push({
|
||||
key: 'refresh',
|
||||
description: `🔄 ${locale.refresh}`,
|
||||
isRefresh: true,
|
||||
});
|
||||
|
||||
nextItems.push({
|
||||
key: 4,
|
||||
role: 'ai',
|
||||
placement: 'start',
|
||||
content: '',
|
||||
avatar: <img src={antdLogoSrc} alt="Ant Design" style={{ width: 28, height: 28 }} />,
|
||||
contentRender: () =>
|
||||
recommendLoading ? (
|
||||
<Flex gap={8} wrap style={{ justifyContent: 'center' }}>
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<Skeleton.Input
|
||||
key={index}
|
||||
active
|
||||
size="small"
|
||||
style={{ width: 140, borderRadius: 8 }}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
) : (
|
||||
<Prompts
|
||||
wrap
|
||||
items={recommendedPrompts}
|
||||
onItemClick={({ data }) => {
|
||||
if ('isRefresh' in data && data.isRefresh) {
|
||||
handleRefreshRecommendations();
|
||||
} else {
|
||||
handleSubmit(
|
||||
String(
|
||||
(data as ExtendedPromptsItemType).originalDescription ?? data.description,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
styles={{ root: { marginTop: 8 } }}
|
||||
/>
|
||||
),
|
||||
styles: { content: { padding: 0, background: 'transparent' } },
|
||||
});
|
||||
}
|
||||
|
||||
return nextItems;
|
||||
}, [prompt, resText, loading, locale.finishTips]);
|
||||
}, [
|
||||
prompt,
|
||||
resText,
|
||||
loading,
|
||||
recommendLoading,
|
||||
locale.finishTips,
|
||||
isDark,
|
||||
recommendations,
|
||||
handleSubmit,
|
||||
handleRefreshRecommendations,
|
||||
locale.refresh,
|
||||
]);
|
||||
|
||||
// Limit to 3 recommendations for Prompts component + refresh button
|
||||
const prompts: ExtendedPromptsItemType[] = React.useMemo(() => {
|
||||
const themePrompts: ExtendedPromptsItemType[] = recommendations
|
||||
.slice(0, 3)
|
||||
.map((text, index) => ({
|
||||
key: text,
|
||||
description: `${getEmojiForTheme(index)} ${text}`,
|
||||
originalDescription: text,
|
||||
}));
|
||||
|
||||
// Add refresh button as last item only when we have recommendations
|
||||
if (themePrompts.length > 0) {
|
||||
themePrompts.push({
|
||||
key: 'refresh',
|
||||
description: `🔄 ${locale.refresh}`,
|
||||
isRefresh: true,
|
||||
});
|
||||
}
|
||||
|
||||
return themePrompts;
|
||||
}, [recommendations, locale.refresh]);
|
||||
|
||||
const renderedWelcome = React.useMemo(
|
||||
() => (
|
||||
<div style={{ padding: '0 0 16px' }}>
|
||||
<Welcome
|
||||
icon={<img src={antdLogoSrc} alt="Ant Design" style={{ width: 48, height: 48 }} />}
|
||||
title={locale.welcomeTitle}
|
||||
description={locale.welcomeDescription}
|
||||
styles={{
|
||||
root: {
|
||||
background: isDark
|
||||
? 'linear-gradient(97deg, rgba(90,196,255,0.12) 0%, rgba(174,136,255,0.12) 100%)'
|
||||
: 'linear-gradient(97deg, #f2f9fe 0%, #f7f3ff 100%)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
[locale.welcomeTitle, locale.welcomeDescription, isDark],
|
||||
);
|
||||
|
||||
const renderedPrompts = React.useMemo(
|
||||
() => (
|
||||
<div style={{ padding: '0 0 32px' }}>
|
||||
<Flex vertical gap={12} align="center">
|
||||
<Divider titlePlacement="center" style={{ margin: 0, fontSize: 12 }}>
|
||||
{locale.recommendTitle}
|
||||
</Divider>
|
||||
{recommendLoading ? (
|
||||
<Flex gap={8} wrap style={{ justifyContent: 'center' }}>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Skeleton.Input
|
||||
key={index}
|
||||
active
|
||||
size="small"
|
||||
style={{ width: 140, borderRadius: 8 }}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
) : (
|
||||
prompts.length > 0 && (
|
||||
<Prompts
|
||||
wrap
|
||||
items={prompts}
|
||||
onItemClick={({ data }) => {
|
||||
if ('isRefresh' in data && data.isRefresh) {
|
||||
handleRefreshRecommendations();
|
||||
} else {
|
||||
handleSubmit(
|
||||
String(
|
||||
(data as ExtendedPromptsItemType).originalDescription ?? data.description,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
styles={{
|
||||
root: {
|
||||
marginTop: 4,
|
||||
},
|
||||
item: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
),
|
||||
[locale.recommendTitle, recommendLoading, prompts, handleSubmit, handleRefreshRecommendations],
|
||||
);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={locale.title}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
size={480}
|
||||
width="80vw"
|
||||
placement="right"
|
||||
afterOpenChange={handleAfterOpenChange}
|
||||
extra={
|
||||
<Button type="text" size="small" onClick={handleResetToDefaultTheme}>
|
||||
{locale.resetToDefault}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Flex vertical style={{ height: '100%' }}>
|
||||
<Bubble.List style={{ flex: 1, overflow: 'auto' }} items={items} />
|
||||
<Sender
|
||||
ref={senderRef}
|
||||
style={{ flex: 0 }}
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
onSubmit={handleSubmit}
|
||||
loading={loading}
|
||||
onCancel={cancelRequest}
|
||||
/>
|
||||
</Flex>
|
||||
<Splitter style={{ height: '100%' }}>
|
||||
{/* 左侧预览区域 */}
|
||||
<Splitter.Panel defaultSize="50%" min="30%" max="70%">
|
||||
<Flex vertical style={{ height: '100%', padding: '0 8px' }}>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '16px 8px',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<ComponentsBlock className="prompt-drawer-preview" />
|
||||
</div>
|
||||
</Flex>
|
||||
</Splitter.Panel>
|
||||
|
||||
{/* 右侧对话区域 */}
|
||||
<Splitter.Panel defaultSize="50%" min="30%" max="70%">
|
||||
<Flex vertical gap={0} style={{ height: '100%', padding: '0 8px' }}>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 0,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{!prompt ? (
|
||||
<>
|
||||
{renderedWelcome}
|
||||
{renderedPrompts}
|
||||
</>
|
||||
) : (
|
||||
<Bubble.List items={items} />
|
||||
)}
|
||||
</div>
|
||||
<Sender
|
||||
ref={senderRef}
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
onSubmit={handleSubmit}
|
||||
loading={loading}
|
||||
onCancel={cancelRequest}
|
||||
placeholder={locale.placeholder}
|
||||
/>
|
||||
</Flex>
|
||||
</Splitter.Panel>
|
||||
</Splitter>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
158
.dumi/theme/common/ThemeSwitch/usePromptRecommend.ts
Normal file
158
.dumi/theme/common/ThemeSwitch/usePromptRecommend.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { XStream } from '@ant-design/x-sdk';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
recommendPrompt:
|
||||
'请生成 4 个 Tailwindcss 主题描述词,需要多样化的 UI 设计风格,用于 Ant Design 主题生成器推荐。参考官方内建风格:暗黑风格、类 MUI 风格、类 shadcn 风格、卡通风格、插画风格、类 Bootstrap 拟物化风格、玻璃风格、极客风格。回复格式:用逗号分隔。',
|
||||
},
|
||||
en: {
|
||||
recommendPrompt:
|
||||
'Generate 4 Tailwind CSS theme names featuring diverse UI design styles for an Ant Design theme generator recommendation. Reference the official built-in styles: Dark, MUI-like, shadcn-like, Cartoon, Illustration, Bootstrap-like Skeuomorphic, Glassmorphism, and Geek. Output format: comma-separated.',
|
||||
},
|
||||
};
|
||||
|
||||
const FALLBACK_THEMES = {
|
||||
cn: [
|
||||
'温暖阳光的橙色调,营造活力积极的氛围',
|
||||
'专业稳重的深海蓝商务风格',
|
||||
'清新自然的森林绿环保主题',
|
||||
'极客紫霓虹感的科技前沿风格',
|
||||
'柔和粉紫的樱花春日浪漫主题',
|
||||
'高对比度的赛博朋克深色科技风',
|
||||
'莫兰迪灰色调,简约优雅的现代感',
|
||||
'青花瓷蓝白配色,东方雅韵',
|
||||
'马卡龙多彩配色,活泼童趣',
|
||||
'水墨黑白灰,传统韵味',
|
||||
],
|
||||
en: [
|
||||
'Warm sunny orange tones for energetic positive vibes',
|
||||
'Professional deep ocean blue business style',
|
||||
'Fresh natural forest green eco-friendly theme',
|
||||
'Geek purple neon tech cutting-edge style',
|
||||
'Soft pink-purple cherry blossom spring romantic theme',
|
||||
'High contrast cyberpunk dark tech style',
|
||||
'Morandi gray tones, minimalist elegant modern feel',
|
||||
'Blue and white porcelain colors, Eastern elegance',
|
||||
'Colorful macaron, lively and playful',
|
||||
'Ink black white gray, traditional charm',
|
||||
],
|
||||
};
|
||||
|
||||
const fetchRecommendations = async (
|
||||
localeKey: keyof typeof locales,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<string[]> => {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: locales[localeKey].recommendPrompt,
|
||||
userId: 'AntDesignSite',
|
||||
}),
|
||||
signal: abortSignal,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api.x.ant.design/api/agent_tbox_antd', options);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('Response body is null');
|
||||
}
|
||||
|
||||
let fullContent = '';
|
||||
|
||||
for await (const chunk of XStream({
|
||||
readableStream: response.body,
|
||||
})) {
|
||||
if (chunk.event === 'message') {
|
||||
try {
|
||||
const data = JSON.parse(chunk.data) as {
|
||||
lane: string;
|
||||
payload: string;
|
||||
};
|
||||
|
||||
const payload = JSON.parse(data.payload) as {
|
||||
text: string;
|
||||
};
|
||||
|
||||
fullContent += payload.text || '';
|
||||
} catch {
|
||||
// Skip malformed chunks and continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse theme names from response - separated by commas
|
||||
const text = fullContent.trim();
|
||||
const items = text
|
||||
.split(/[,,\n]/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
const result = items.slice(0, 4);
|
||||
|
||||
// If parsing failed or got no results, use fallback
|
||||
if (result.length === 0) {
|
||||
return FALLBACK_THEMES[localeKey];
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Don't log AbortError - it's expected when cancelling requests
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw error; // Re-throw AbortError to be handled by caller
|
||||
}
|
||||
console.error('Error in fetchRecommendations:', error);
|
||||
// Return fallback themes
|
||||
return FALLBACK_THEMES[localeKey];
|
||||
}
|
||||
};
|
||||
|
||||
export default function usePromptRecommend(localeKey: keyof typeof locales = 'cn') {
|
||||
const [recommendations, setRecommendations] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const fetchedKeyRef = useRef<string>('');
|
||||
|
||||
const fetch = async (key: string) => {
|
||||
if (fetchedKeyRef.current === key) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel previous request if it exists
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const items = await fetchRecommendations(localeKey, abortController.signal);
|
||||
setRecommendations(items);
|
||||
fetchedKeyRef.current = key;
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error && error.name === 'AbortError')) {
|
||||
console.error('Failed to fetch recommendations:', error);
|
||||
// Use fallback on error
|
||||
setRecommendations(FALLBACK_THEMES[localeKey]);
|
||||
fetchedKeyRef.current = key;
|
||||
}
|
||||
} finally {
|
||||
// Only clear loading and controller if this is still the active request
|
||||
if (abortControllerRef.current === abortController) {
|
||||
setLoading(false);
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { recommendations, loading, fetch };
|
||||
}
|
||||
@@ -110,12 +110,10 @@ const ColorTrigger = forwardRef<HTMLDivElement, ColorTriggerProps>((props, ref)
|
||||
prefixCls={prefixCls}
|
||||
color={color.toCssString()}
|
||||
className={classNames.body}
|
||||
innerClassName={classNames.content}
|
||||
style={styles.body}
|
||||
innerStyle={styles.content}
|
||||
/>
|
||||
),
|
||||
[color, prefixCls, classNames.body, classNames.content, styles.body, styles.content],
|
||||
[color, prefixCls, classNames.body, styles.body],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user