Skip to content

Commit a66bb7f

Browse files
committed
feat: 集成全局侧边栏配置
1 parent 6494bb0 commit a66bb7f

File tree

10 files changed

+822
-0
lines changed

10 files changed

+822
-0
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { TooltipProps } from 'antd';
2+
import { Tooltip } from 'antd';
3+
import clsx from 'clsx';
4+
5+
import { themeLayoutModeRecord } from '@/constants/app';
6+
import { getIsMobile } from '@/store/slice/app';
7+
import { setLayoutMode } from '@/store/slice/theme';
8+
9+
type LayoutConfig = Record<
10+
UnionKey.ThemeLayoutMode,
11+
{
12+
headerClass: string;
13+
mainClass: string;
14+
menuClass: string;
15+
placement: TooltipProps['placement'];
16+
}
17+
>;
18+
19+
const LAYOUT_CONFIG: LayoutConfig = {
20+
horizontal: {
21+
headerClass: '',
22+
mainClass: 'w-full h-3/4',
23+
menuClass: 'w-full h-1/4',
24+
placement: 'bottom'
25+
},
26+
'horizontal-mix': {
27+
headerClass: '',
28+
mainClass: 'w-2/3 h-3/4',
29+
menuClass: 'w-full h-1/4',
30+
placement: 'bottom'
31+
},
32+
vertical: {
33+
headerClass: '',
34+
mainClass: 'w-2/3 h-3/4',
35+
menuClass: 'w-1/3 h-full',
36+
placement: 'bottom'
37+
},
38+
'vertical-mix': {
39+
headerClass: '',
40+
mainClass: 'w-2/3 h-3/4',
41+
menuClass: 'w-1/4 h-full',
42+
placement: 'bottom'
43+
}
44+
};
45+
46+
interface Props extends Record<UnionKey.ThemeLayoutMode, React.ReactNode> {
47+
mode: UnionKey.ThemeLayoutMode;
48+
}
49+
50+
const LayoutModeCard: FC<Props> = ({ mode, ...rest }: Props) => {
51+
const isMobile = useAppSelector(getIsMobile);
52+
53+
const dispatch = useAppDispatch();
54+
55+
const { t } = useTranslation();
56+
function handleChangeMode(modeType: UnionKey.ThemeLayoutMode) {
57+
if (isMobile) return;
58+
59+
dispatch(setLayoutMode(modeType));
60+
}
61+
62+
return (
63+
<div className="flex-center flex-wrap gap-x-32px gap-y-16px">
64+
{Object.entries(LAYOUT_CONFIG).map(([key, item]) => (
65+
<div
66+
key={key}
67+
className={clsx(
68+
'flex cursor-pointer border-2px rounded-6px hover:border-primary',
69+
mode === key ? 'border-primary' : 'border-transparent'
70+
)}
71+
onClick={() => handleChangeMode(key as UnionKey.ThemeLayoutMode)}
72+
>
73+
<Tooltip
74+
placement={item.placement}
75+
title={t(themeLayoutModeRecord[key as UnionKey.ThemeLayoutMode])}
76+
>
77+
<div
78+
className={clsx(
79+
'h-64px w-96px gap-6px rd-4px p-6px shadow dark:shadow-coolGray-5',
80+
key.includes('vertical') ? 'flex' : 'flex-col'
81+
)}
82+
>
83+
{rest[key as UnionKey.ThemeLayoutMode]}
84+
</div>
85+
</Tooltip>
86+
</div>
87+
))}
88+
</div>
89+
);
90+
};
91+
92+
export default LayoutModeCard;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import clsx from 'clsx';
2+
import type { PropsWithChildren } from 'react';
3+
4+
type Props = PropsWithChildren<{
5+
className?: string;
6+
/** Label */
7+
label: React.ReactNode;
8+
show?: boolean;
9+
suffix?: React.ReactNode;
10+
}>;
11+
12+
const SettingItem: FC<Props> = ({ children, className, label, show = true, suffix }: Props) => {
13+
if (!show) return null;
14+
15+
return (
16+
<div className={clsx('w-full flex-y-center justify-between', className)}>
17+
<div>
18+
<span className="pr-8px text-base-text">{label}</span>
19+
{suffix}
20+
</div>
21+
{children}
22+
</div>
23+
);
24+
};
25+
26+
export default SettingItem;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { SimpleScrollbar } from '@sa/materials';
2+
3+
import { closeThemeDrawer, getThemeDrawerVisible } from '@/store/slice/app';
4+
5+
import ConfigOperation from './modules/ConfigOperation';
6+
import DarkMode from './modules/DarkMode';
7+
import LayoutMode from './modules/LayoutMode';
8+
import PageFun from './modules/PageFun';
9+
import ThemeColor from './modules/ThemeColor';
10+
11+
const ThemeDrawer = memo(() => {
12+
const { t } = useTranslation();
13+
14+
const dispatch = useAppDispatch();
15+
16+
const themeDrawerVisible = useAppSelector(getThemeDrawerVisible);
17+
18+
function close() {
19+
dispatch(closeThemeDrawer());
20+
}
21+
22+
return (
23+
<ADrawer
24+
closeIcon={false}
25+
footer={<ConfigOperation />}
26+
open={themeDrawerVisible}
27+
styles={{ body: { padding: 0 } }}
28+
title={t('theme.themeDrawerTitle')}
29+
extra={
30+
<ButtonIcon
31+
className="h-28px"
32+
icon="ant-design:close-outlined"
33+
onClick={close}
34+
/>
35+
}
36+
onClose={close}
37+
>
38+
<SimpleScrollbar>
39+
<div className="overflow-x-hidden px-24px pb-24px pt-8px">
40+
<ADivider>{t('theme.themeSchema.title')}</ADivider>
41+
<DarkMode />
42+
<ADivider>{t('theme.layoutMode.title')}</ADivider>
43+
<LayoutMode />
44+
<ADivider>{t('theme.themeColor.title')}</ADivider>
45+
<ThemeColor />
46+
<ADivider>{t('theme.pageFunTitle')}</ADivider>
47+
<PageFun />
48+
</div>
49+
</SimpleScrollbar>
50+
</ADrawer>
51+
);
52+
});
53+
54+
export default ThemeDrawer;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import Clipboard from 'clipboard';
2+
3+
import { resetTheme, settingsJson } from '@/store/slice/theme';
4+
5+
const ConfigOperation = () => {
6+
const { t } = useTranslation();
7+
8+
const domRef = useRef<HTMLDivElement | null>(null);
9+
10+
const themeSettingsJson = useAppSelector(settingsJson);
11+
12+
const dispatch = useAppDispatch();
13+
14+
function getClipboardText() {
15+
const reg = /"\w+":/g;
16+
17+
return themeSettingsJson.replace(reg, match => match.replace(/"/g, ''));
18+
}
19+
20+
function initClipboard() {
21+
if (!domRef.current) return;
22+
23+
const clipboard = new Clipboard(domRef.current, {
24+
text: () => getClipboardText()
25+
});
26+
27+
clipboard.on('success', () => {
28+
window.$message?.success(t('theme.configOperation.copySuccessMsg'));
29+
});
30+
}
31+
32+
function handleReset() {
33+
dispatch(resetTheme());
34+
35+
setTimeout(() => {
36+
window.$message?.success(t('theme.configOperation.resetSuccessMsg'));
37+
}, 50);
38+
}
39+
40+
useMount(() => {
41+
initClipboard();
42+
});
43+
44+
return (
45+
<div className="flex justify-between">
46+
<AButton
47+
danger
48+
onClick={handleReset}
49+
>
50+
{t('theme.configOperation.resetConfig')}
51+
</AButton>
52+
<div ref={domRef}>
53+
<AButton type="primary">{t('theme.configOperation.copyConfig')}</AButton>
54+
</div>
55+
</div>
56+
);
57+
};
58+
59+
export default ConfigOperation;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { CheckboxProps, ColorPickerProps } from 'antd';
2+
3+
import { setIsInfoFollowPrimary, updateThemeColors } from '@/store/slice/theme';
4+
5+
import SettingItem from '../components/SettingItem';
6+
7+
const swatches: { color: string; name: string }[] = [
8+
{ color: '#3b82f6', name: '海洋蓝' },
9+
{ color: '#6366f1', name: '紫罗兰' },
10+
{ color: '#8b5cf6', name: '梦幻紫' },
11+
{ color: '#a855f7', name: '迷人紫' },
12+
{ color: '#0ea5e9', name: '清澈海洋' },
13+
{ color: '#06b6d4', name: '天空蓝' },
14+
{ color: '#f43f5e', name: '樱桃红' },
15+
{ color: '#ef4444', name: '火焰红' },
16+
{ color: '#ec4899', name: '玫瑰粉' },
17+
{ color: '#d946ef', name: '紫色魅影' },
18+
{ color: '#f97316', name: '橙色阳光' },
19+
{ color: '#f59e0b', name: '金色晨曦' },
20+
{ color: '#eab308', name: '柠檬黄' },
21+
{ color: '#84cc16', name: '草地绿' },
22+
{ color: '#22c55e', name: '清新绿' },
23+
{ color: '#10b981', name: '热带绿' }
24+
];
25+
26+
interface Props {
27+
index: number;
28+
isInfoFollowPrimary: boolean;
29+
label: string;
30+
theme: string;
31+
value: string;
32+
}
33+
34+
const CustomPicker: FC<Props> = memo(({ isInfoFollowPrimary, label, theme, value }) => {
35+
const { t } = useTranslation();
36+
37+
const dispatch = useAppDispatch();
38+
39+
function handleUpdateColor(color: string, name: App.Theme.ThemeColorKey) {
40+
dispatch(updateThemeColors({ color, key: name }));
41+
}
42+
43+
const [selectTheme, setSelectTheme] = useState<string>(theme);
44+
45+
const onChange: CheckboxProps['onChange'] = e => {
46+
dispatch(setIsInfoFollowPrimary(e.target.checked));
47+
};
48+
49+
const customPanelRender: ColorPickerProps['panelRender'] = (_, { components: { Picker } }) => (
50+
<ASpace
51+
className="w-250px"
52+
direction="vertical"
53+
>
54+
<>
55+
<Picker />
56+
<AFlex
57+
wrap
58+
gap="small"
59+
>
60+
{swatches.map(item => (
61+
<ATooltip
62+
key={item.name}
63+
title={item.name}
64+
>
65+
<span
66+
onClick={() => {
67+
handleUpdateColor(item.color, selectTheme as App.Theme.ThemeColorKey);
68+
}}
69+
>
70+
<AColorPicker
71+
defaultValue={item.color}
72+
open={false}
73+
size="small"
74+
/>
75+
</span>
76+
</ATooltip>
77+
))}
78+
</AFlex>
79+
</>
80+
</ASpace>
81+
);
82+
83+
return (
84+
<SettingItem
85+
label={t(`theme.themeColor.${label}`)}
86+
suffix={
87+
label === 'info' && (
88+
<ACheckbox
89+
defaultChecked={isInfoFollowPrimary}
90+
onChange={onChange}
91+
>
92+
{t('theme.themeColor.followPrimary')}
93+
</ACheckbox>
94+
)
95+
}
96+
>
97+
<AColorPicker
98+
disabled={label === 'info' && isInfoFollowPrimary}
99+
panelRender={customPanelRender}
100+
value={value}
101+
onChange={(_, hex) => handleUpdateColor(hex, label as App.Theme.ThemeColorKey)}
102+
onOpenChange={() => {
103+
setSelectTheme(label);
104+
}}
105+
/>
106+
</SettingItem>
107+
);
108+
});
109+
110+
export default CustomPicker;

0 commit comments

Comments
 (0)