feat: add built-in themes (#2284)
* feat: add built-in themes * chore: add translation * fix: should be optional in initial
This commit is contained in:
parent
a4d8ef4b71
commit
eb9ee38a2b
@ -71,8 +71,15 @@ const ThemeCard = (props: Props) => {
|
|||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
const currentThemeId = useCurrentThemeId();
|
const currentThemeId = useCurrentThemeId();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { token } = useToken();
|
||||||
|
|
||||||
|
const isDefault = item.id === systemSettings?.data?.data?.options?.themeId;
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
|
if (isDefault) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
title: t('Delete theme'),
|
title: t('Delete theme'),
|
||||||
content: t('Deletion is unrecoverable. Confirm deletion?'),
|
content: t('Deletion is unrecoverable. Confirm deletion?'),
|
||||||
@ -94,6 +101,7 @@ const ThemeCard = (props: Props) => {
|
|||||||
}, [
|
}, [
|
||||||
api,
|
api,
|
||||||
currentUser?.data?.data?.systemSettings?.themeId,
|
currentUser?.data?.data?.systemSettings?.themeId,
|
||||||
|
isDefault,
|
||||||
item,
|
item,
|
||||||
modal,
|
modal,
|
||||||
onChange,
|
onChange,
|
||||||
@ -197,6 +205,7 @@ const ThemeCard = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<span>{t('User selectable')}</span>
|
<span>{t('User selectable')}</span>
|
||||||
<Switch
|
<Switch
|
||||||
|
disabled={isDefault}
|
||||||
style={{ transform: 'translateY(-2px)' }}
|
style={{ transform: 'translateY(-2px)' }}
|
||||||
checked={item.optional}
|
checked={item.optional}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
@ -217,8 +226,9 @@ const ThemeCard = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<span>{t('Default theme')}</span>
|
<span>{t('Default theme')}</span>
|
||||||
<Switch
|
<Switch
|
||||||
|
disabled={isDefault}
|
||||||
style={{ transform: 'translateY(-2px)' }}
|
style={{ transform: 'translateY(-2px)' }}
|
||||||
checked={item.id === systemSettings?.data?.data?.options?.themeId}
|
checked={isDefault}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onChange={handleSwitchDefault}
|
onChange={handleSwitchDefault}
|
||||||
@ -228,25 +238,29 @@ const ThemeCard = (props: Props) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}, [
|
}, [handleSwitchDefault, handleSwitchOptional, isDefault, item.optional, loading, t]);
|
||||||
handleSwitchDefault,
|
|
||||||
handleSwitchOptional,
|
|
||||||
item.id,
|
|
||||||
item.optional,
|
|
||||||
loading,
|
|
||||||
systemSettings?.data?.data?.options?.themeId,
|
|
||||||
t,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const actions = useMemo(() => {
|
const actions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
<EditOutlined key="edit" onClick={handleEdit} />,
|
<EditOutlined key="edit" onClick={handleEdit} />,
|
||||||
<DeleteOutlined key="delete" onClick={handleDelete} />,
|
<DeleteOutlined
|
||||||
|
key="delete"
|
||||||
|
style={
|
||||||
|
isDefault
|
||||||
|
? {
|
||||||
|
color: token.colorTextDisabled,
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
disabled={isDefault}
|
||||||
|
onClick={handleDelete}
|
||||||
|
/>,
|
||||||
<Dropdown key="ellipsis" menu={menu}>
|
<Dropdown key="ellipsis" menu={menu}>
|
||||||
<EllipsisOutlined />
|
<EllipsisOutlined />
|
||||||
</Dropdown>,
|
</Dropdown>,
|
||||||
];
|
];
|
||||||
}, [handleDelete, handleEdit, menu]);
|
}, [handleDelete, handleEdit, isDefault, menu, token.colorTextDisabled]);
|
||||||
|
|
||||||
const extra = useMemo(() => {
|
const extra = useMemo(() => {
|
||||||
if (item.id !== systemSettings?.data?.data?.options?.themeId && !item.optional) {
|
if (item.id !== systemSettings?.data?.data?.options?.themeId && !item.optional) {
|
||||||
@ -294,7 +308,7 @@ const ThemeCard = (props: Props) => {
|
|||||||
<Card
|
<Card
|
||||||
hoverable
|
hoverable
|
||||||
extra={extra}
|
extra={extra}
|
||||||
title={item.config.name}
|
title={t(item.config.name)}
|
||||||
size="small"
|
size="small"
|
||||||
style={cardStyle}
|
style={cardStyle}
|
||||||
headStyle={{ minHeight: 38 }}
|
headStyle={{ minHeight: 38 }}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useCurrentUserContext, useSystemSettings } from '@nocobase/client';
|
import { useAPIClient, useCurrentUserContext, useSystemSettings } from '@nocobase/client';
|
||||||
import { error } from '@nocobase/utils/client';
|
import { error } from '@nocobase/utils/client';
|
||||||
import { MenuProps, Select } from 'antd';
|
import { MenuProps, Select } from 'antd';
|
||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
@ -21,9 +21,11 @@ function Label() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const currentUser = useCurrentUserContext();
|
const currentUser = useCurrentUserContext();
|
||||||
const systemSettings = useSystemSettings();
|
const systemSettings = useSystemSettings();
|
||||||
const { run, error: err, data } = useThemeListContext();
|
const { run, error: err, data, refresh } = useThemeListContext();
|
||||||
const { updateUserThemeSettings } = useUpdateThemeSettings();
|
const { updateUserThemeSettings, updateSystemThemeSettings } = useUpdateThemeSettings();
|
||||||
const currentThemeId = useCurrentThemeId();
|
const currentThemeId = useCurrentThemeId();
|
||||||
|
const themeId = useCurrentThemeId();
|
||||||
|
const api = useAPIClient();
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
return data
|
return data
|
||||||
@ -34,7 +36,7 @@ function Label() {
|
|||||||
value: item.id,
|
value: item.id,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [data]);
|
}, [data, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@ -42,6 +44,25 @@ function Label() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const init = async () => {
|
||||||
|
// 当 themeId 为空时表示插件是第一次被启用
|
||||||
|
if (themeId == null && data) {
|
||||||
|
const firstTheme = data[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 避免并发请求,在本地存储中容易出问题
|
||||||
|
await updateSystemThemeSettings(firstTheme.id);
|
||||||
|
await updateUserThemeSettings(firstTheme.id);
|
||||||
|
} catch (err) {
|
||||||
|
error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
}, [themeId, updateSystemThemeSettings, updateUserThemeSettings, data, api, refresh]);
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
error(err);
|
error(err);
|
||||||
return null;
|
return null;
|
||||||
|
@ -16,6 +16,12 @@ const locale = {
|
|||||||
'Edit based on current theme': '基于当前主题进行编辑',
|
'Edit based on current theme': '基于当前主题进行编辑',
|
||||||
'Create a brand new theme': '创建一个全新的主题',
|
'Create a brand new theme': '创建一个全新的主题',
|
||||||
|
|
||||||
|
// 内置主题的名字
|
||||||
|
'Default theme of antd': 'antd 默认主题',
|
||||||
|
Dark: '暗黑',
|
||||||
|
Compact: '紧凑',
|
||||||
|
'Compact dark': '紧凑暗黑',
|
||||||
|
|
||||||
// 主题编辑器
|
// 主题编辑器
|
||||||
'Theme Editor': '主题编辑器',
|
'Theme Editor': '主题编辑器',
|
||||||
Save: '保存',
|
Save: '保存',
|
||||||
|
41
packages/plugins/theme-editor/src/server/builtinThemes.ts
Normal file
41
packages/plugins/theme-editor/src/server/builtinThemes.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { ThemeItem } from '../types';
|
||||||
|
|
||||||
|
/** antd 默认主题 */
|
||||||
|
export const antd: Omit<ThemeItem, 'id'> = {
|
||||||
|
config: {
|
||||||
|
name: 'Default theme of antd',
|
||||||
|
},
|
||||||
|
optional: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dark: Omit<ThemeItem, 'id'> = {
|
||||||
|
config: {
|
||||||
|
name: 'Dark',
|
||||||
|
// @ts-ignore
|
||||||
|
algorithm: 'darkAlgorithm',
|
||||||
|
},
|
||||||
|
optional: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const compact: Omit<ThemeItem, 'id'> = {
|
||||||
|
config: {
|
||||||
|
name: 'Compact',
|
||||||
|
// @ts-ignore
|
||||||
|
algorithm: 'compactAlgorithm',
|
||||||
|
},
|
||||||
|
optional: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 同时包含 `紧凑` 和 `暗黑` 两种模式 */
|
||||||
|
export const compactDark: Omit<ThemeItem, 'id'> = {
|
||||||
|
config: {
|
||||||
|
name: 'Compact dark',
|
||||||
|
// @ts-ignore
|
||||||
|
algorithm: ['compactAlgorithm', 'darkAlgorithm'],
|
||||||
|
},
|
||||||
|
optional: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
};
|
@ -1,12 +1,16 @@
|
|||||||
|
import { Collection } from '@nocobase/database';
|
||||||
import { InstallOptions, Plugin } from '@nocobase/server';
|
import { InstallOptions, Plugin } from '@nocobase/server';
|
||||||
|
import { antd, compact, compactDark, dark } from './builtinThemes';
|
||||||
|
|
||||||
export class ThemeEditorPlugin extends Plugin {
|
export class ThemeEditorPlugin extends Plugin {
|
||||||
|
theme: Collection<any, any>;
|
||||||
|
|
||||||
afterAdd() {}
|
afterAdd() {}
|
||||||
|
|
||||||
beforeLoad() {}
|
beforeLoad() {}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
this.db.collection({
|
this.theme = this.db.collection({
|
||||||
name: 'themeConfig',
|
name: 'themeConfig',
|
||||||
fields: [
|
fields: [
|
||||||
// 主题配置内容,一个 JSON 字符串
|
// 主题配置内容,一个 JSON 字符串
|
||||||
@ -27,7 +31,13 @@ export class ThemeEditorPlugin extends Plugin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async install(options?: InstallOptions) {}
|
async install(options?: InstallOptions) {
|
||||||
|
if ((await this.theme.repository.count()) === 0) {
|
||||||
|
await this.theme.repository.create({
|
||||||
|
values: [antd, dark, compact, compactDark],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async afterEnable() {}
|
async afterEnable() {}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user