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:
被雨水过滤的空气-Rain 2023-07-21 10:42:50 +08:00 committed by GitHub
parent a4d8ef4b71
commit eb9ee38a2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 111 additions and 19 deletions

View File

@ -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 }}

View File

@ -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;

View File

@ -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: '保存',

View 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,
};

View File

@ -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() {}