refactor(perferences): use center wrapper layout
This commit is contained in:
@@ -198,6 +198,8 @@ export default {
|
||||
'common.table.user': 'User',
|
||||
'common.settings.instructions': 'Instructions',
|
||||
'common.settings.language': 'Language',
|
||||
'common.settings.language.tips':
|
||||
'Set the display language for the interface.',
|
||||
'common.delete.confirm':
|
||||
'Are you sure you want to delete the selected {type}?',
|
||||
'common.delete.single.confirm':
|
||||
@@ -251,6 +253,11 @@ export default {
|
||||
'common.appearance.tips': 'Default follows system preference.',
|
||||
'common.button.forgotpassword': 'Forgot password?',
|
||||
'common.appearance.theme': 'Theme',
|
||||
'common.appearance.description':
|
||||
'Customize how the interface looks on your device.',
|
||||
'common.security': 'Security',
|
||||
'common.security.description':
|
||||
'Manage the password used to sign in to your account.',
|
||||
'common.page.wentwrong': 'Something went wrong.',
|
||||
'common.page.refresh.tips':
|
||||
'The page may need to be updated. Try refreshing it!',
|
||||
|
||||
@@ -28,6 +28,8 @@ export default {
|
||||
'users.password.modify.title': 'Modify Password',
|
||||
'users.password.modify.description':
|
||||
"For your account's security, please change your initial password.",
|
||||
'users.password.modify.tips':
|
||||
'Regularly updating your password helps keep your account secure.',
|
||||
'users.password.confirm': 'Confirm New Password',
|
||||
'users.password.confirm.empty': 'Please confirm the new password.',
|
||||
'users.password.confirm.error': 'The two passwords entered do not match.',
|
||||
|
||||
@@ -199,6 +199,7 @@ export default {
|
||||
'common.table.user': 'ユーザー',
|
||||
'common.settings.instructions': '手順',
|
||||
'common.settings.language': '言語',
|
||||
'common.settings.language.tips': 'インターフェースの表示言語を設定します。',
|
||||
'common.delete.confirm': '選択した {type} を削除してもよろしいですか?',
|
||||
'common.delete.single.confirm':
|
||||
'<span style="font-size: 13px;font-weight: 700">{name}</span> を削除してもよろしいですか?',
|
||||
@@ -251,6 +252,11 @@ export default {
|
||||
'common.appearance.tips': 'Default follows system preference.',
|
||||
'common.button.forgotpassword': 'Forgot password?',
|
||||
'common.appearance.theme': 'Theme',
|
||||
'common.appearance.description':
|
||||
'デバイス上でのインターフェースの表示をカスタマイズします。',
|
||||
'common.security': 'セキュリティ',
|
||||
'common.security.description':
|
||||
'アカウントへのログインに使用するパスワードを管理します。',
|
||||
'common.page.wentwrong': 'Something went wrong.',
|
||||
'common.page.refresh.tips':
|
||||
'The page may need to be updated. Try refreshing it!',
|
||||
|
||||
@@ -29,6 +29,8 @@ export default {
|
||||
'users.password.modify.title': 'パスワードを変更',
|
||||
'users.password.modify.description':
|
||||
'アカウントのセキュリティのため、初期パスワードを変更してください。',
|
||||
'users.password.modify.tips':
|
||||
'パスワードを定期的に更新すると、アカウントの安全を保てます。',
|
||||
'users.password.confirm': '新しいパスワードを確認',
|
||||
'users.password.confirm.empty': '新しいパスワードを確認してください。',
|
||||
'users.password.confirm.error': '入力された2つのパスワードが一致しません。',
|
||||
|
||||
@@ -197,6 +197,7 @@ export default {
|
||||
'common.table.user': 'Пользователь',
|
||||
'common.settings.instructions': 'Инструкции',
|
||||
'common.settings.language': 'Язык',
|
||||
'common.settings.language.tips': 'Задайте язык отображения интерфейса.',
|
||||
'common.delete.confirm': 'Вы уверены, что хотите удалить выбранный {type}?',
|
||||
'common.delete.single.confirm':
|
||||
'Вы уверены, что хотите удалить <span style="font-size: 13px;font-weight: 700">{name}</span>?',
|
||||
@@ -250,6 +251,11 @@ export default {
|
||||
'common.appearance.tips': 'По умолчанию соответствует системным настройкам.',
|
||||
'common.button.forgotpassword': 'Забыли пароль?',
|
||||
'common.appearance.theme': 'Тема',
|
||||
'common.appearance.description':
|
||||
'Настройте внешний вид интерфейса на вашем устройстве.',
|
||||
'common.security': 'Безопасность',
|
||||
'common.security.description':
|
||||
'Управляйте паролем для входа в учётную запись.',
|
||||
'common.page.wentwrong': 'Что-то пошло не так.',
|
||||
'common.page.refresh.tips':
|
||||
'Страница может нуждаться в обновлении. Попробуйте обновить её!',
|
||||
|
||||
@@ -29,6 +29,8 @@ export default {
|
||||
'users.password.modify.title': 'Смена пароля',
|
||||
'users.password.modify.description':
|
||||
'В целях безопасности измените первоначальный пароль.',
|
||||
'users.password.modify.tips':
|
||||
'Регулярное обновление пароля помогает защитить вашу учётную запись.',
|
||||
'users.password.confirm': 'Подтвердите новый пароль',
|
||||
'users.password.confirm.empty': 'Подтвердите новый пароль',
|
||||
'users.password.confirm.error': 'Пароли не совпадают',
|
||||
|
||||
@@ -199,6 +199,7 @@ export default {
|
||||
'common.table.user': 'Kullanıcı',
|
||||
'common.settings.instructions': 'Talimatlar',
|
||||
'common.settings.language': 'Dil',
|
||||
'common.settings.language.tips': 'Arayüzün görüntüleme dilini ayarlayın.',
|
||||
'common.delete.confirm':
|
||||
'Seçili {type} öğesini silmek istediğinizden emin misiniz?',
|
||||
'common.delete.single.confirm':
|
||||
@@ -254,6 +255,11 @@ export default {
|
||||
'common.appearance.tips': 'Varsayılan olarak sistem tercihini takip eder.',
|
||||
'common.button.forgotpassword': 'Şifrenizi mi unuttunuz?',
|
||||
'common.appearance.theme': 'Tema',
|
||||
'common.appearance.description':
|
||||
'Arayüzün cihazınızdaki görünümünü özelleştirin.',
|
||||
'common.security': 'Güvenlik',
|
||||
'common.security.description':
|
||||
'Hesabınıza giriş yapmak için kullanılan parolayı yönetin.',
|
||||
'common.page.wentwrong': 'Bir şeyler ters gitti.',
|
||||
'common.page.refresh.tips':
|
||||
'Sayfanın güncellenmesi gerekebilir. Yenilemeyi deneyin!',
|
||||
|
||||
@@ -29,6 +29,8 @@ export default {
|
||||
'users.password.modify.title': 'Şifreyi Değiştir',
|
||||
'users.password.modify.description':
|
||||
'Hesabınızın güvenliği için lütfen başlangıç şifrenizi değiştirin.',
|
||||
'users.password.modify.tips':
|
||||
'Parolanızı düzenli olarak güncellemek hesabınızın güvenliğini korumaya yardımcı olur.',
|
||||
'users.password.confirm': 'Yeni Şifreyi Onayla',
|
||||
'users.password.confirm.empty': 'Lütfen yeni şifreyi tekrar girin.',
|
||||
'users.password.confirm.error': 'Girilen iki şifre eşleşmiyor.',
|
||||
|
||||
@@ -190,6 +190,7 @@ export default {
|
||||
'common.table.user': '用户',
|
||||
'common.settings.instructions': '操作指引',
|
||||
'common.settings.language': '语言',
|
||||
'common.settings.language.tips': '设置界面的显示语言。',
|
||||
'common.delete.confirm': '确定删除选中的{type}吗?',
|
||||
'common.delete.single.confirm':
|
||||
'确定删除以下内容?\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
@@ -246,6 +247,9 @@ export default {
|
||||
'common.appearance.tips': '默认跟随系统设置',
|
||||
'common.button.forgotpassword': '忘记密码?',
|
||||
'common.appearance.theme': '主题',
|
||||
'common.appearance.description': '自定义界面在您设备上的视觉表现。',
|
||||
'common.security': '安全设置',
|
||||
'common.security.description': '管理用于登录账户的密码。',
|
||||
'common.page.wentwrong': '哎呀,出了点问题',
|
||||
'common.page.refresh.tips': '页面似乎需要更新,刷新一下试试吧!',
|
||||
'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口',
|
||||
|
||||
@@ -27,6 +27,7 @@ export default {
|
||||
'users.password.length': '长度在6至64个字符之间',
|
||||
'users.password.modify.title': '修改密码',
|
||||
'users.password.modify.description': '为了确保您的账户安全,请修改初始密码',
|
||||
'users.password.modify.tips': '定期更新密码有助于保护账户安全。',
|
||||
'users.password.confirm': '确认新密码',
|
||||
'users.password.confirm.empty': '请确认新密码',
|
||||
'users.password.confirm.error': '两次输入的密码不一致',
|
||||
|
||||
@@ -1,92 +1,150 @@
|
||||
import useUserSettings from '@/hooks/use-user-settings';
|
||||
import langConfigMap from '@/locales/lang-config-map';
|
||||
import { MoonOutlined, SunOutlined } from '@ant-design/icons';
|
||||
import { CheckCircleFilled } from '@ant-design/icons';
|
||||
import { BaseSelect } from '@gpustack/core-ui';
|
||||
import { getAllLocales, setLocale, useIntl } from '@umijs/max';
|
||||
import { createStyles } from 'antd-style';
|
||||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
`;
|
||||
import { SettingRow, SettingsGroup } from './settings-group';
|
||||
import ThemePreview, { PreviewMode } from './theme-preview';
|
||||
|
||||
const SettingsItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 300px;
|
||||
.label {
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
cards: css`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
`,
|
||||
card: css`
|
||||
padding: 8px;
|
||||
border: 1px solid ${token.colorBorderSecondary};
|
||||
border-radius: ${token.borderRadiusLG + 2}px;
|
||||
background: ${token.colorBgContainer};
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: ${token.colorPrimaryBorderHover};
|
||||
}
|
||||
`,
|
||||
cardActive: css`
|
||||
border-color: ${token.colorPrimary};
|
||||
box-shadow: 0 0 0 1px ${token.colorPrimary};
|
||||
`,
|
||||
meta: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 8px 6px;
|
||||
`,
|
||||
label: css`
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-500);
|
||||
}
|
||||
`;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: ${token.colorText};
|
||||
`,
|
||||
check: css`
|
||||
font-size: 18px;
|
||||
color: ${token.colorPrimary};
|
||||
`,
|
||||
radio: css`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid ${token.colorBorder};
|
||||
`
|
||||
}));
|
||||
|
||||
const Appearance: React.FC = () => {
|
||||
const { setTheme, userSettings } = useUserSettings();
|
||||
|
||||
const intl = useIntl();
|
||||
const { styles } = useStyles();
|
||||
const allLocals = getAllLocales();
|
||||
|
||||
const ThemeOptions = [
|
||||
const themeOptions: { value: PreviewMode; label: string }[] = [
|
||||
{
|
||||
value: 'light',
|
||||
label: intl.formatMessage({ id: 'common.appearance.light' }),
|
||||
icon: <SunOutlined />
|
||||
label: intl.formatMessage({ id: 'common.appearance.lightmode' })
|
||||
},
|
||||
{
|
||||
value: 'realDark',
|
||||
label: intl.formatMessage({ id: 'common.appearance.dark' }),
|
||||
icon: <MoonOutlined />
|
||||
label: intl.formatMessage({ id: 'common.appearance.darkmode' })
|
||||
},
|
||||
{
|
||||
value: 'auto',
|
||||
label: intl.formatMessage({ id: 'common.appearance.system' }),
|
||||
icon: <SunOutlined />
|
||||
label: intl.formatMessage({ id: 'common.appearance.system' })
|
||||
}
|
||||
];
|
||||
|
||||
const handleOnChange = (value: 'light' | 'realDark' | 'auto') => {
|
||||
setTheme(value);
|
||||
};
|
||||
|
||||
const languageOptions = allLocals.map((locale) => ({
|
||||
value: locale,
|
||||
label: _.get(langConfigMap, [locale, 'label'])
|
||||
}));
|
||||
|
||||
const handleSelectTheme = (value: PreviewMode) => {
|
||||
setTheme(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<SettingsItem>
|
||||
<span className="label">
|
||||
<span>{intl.formatMessage({ id: 'common.appearance.theme' })}</span>
|
||||
</span>
|
||||
<BaseSelect
|
||||
defaultValue={'light'}
|
||||
value={userSettings.mode}
|
||||
options={ThemeOptions}
|
||||
onChange={handleOnChange}
|
||||
style={{ width: 200 }}
|
||||
></BaseSelect>
|
||||
</SettingsItem>
|
||||
<SettingsItem>
|
||||
<span className="label">
|
||||
<span>{intl.formatMessage({ id: 'common.settings.language' })}</span>
|
||||
</span>
|
||||
<BaseSelect
|
||||
value={intl.locale}
|
||||
options={languageOptions}
|
||||
onChange={(value) => {
|
||||
setLocale(value, false);
|
||||
}}
|
||||
style={{ width: 200 }}
|
||||
></BaseSelect>
|
||||
</SettingsItem>
|
||||
</Wrapper>
|
||||
<SettingsGroup>
|
||||
<SettingRow
|
||||
title={intl.formatMessage({ id: 'common.appearance.theme' })}
|
||||
description={intl.formatMessage({ id: 'common.appearance.tips' })}
|
||||
>
|
||||
<div className={styles.cards}>
|
||||
{themeOptions.map((option) => {
|
||||
const active = userSettings.mode === option.value;
|
||||
return (
|
||||
<div
|
||||
key={option.value}
|
||||
role="radio"
|
||||
aria-checked={active}
|
||||
tabIndex={0}
|
||||
className={classNames(styles.card, {
|
||||
[styles.cardActive]: active
|
||||
})}
|
||||
onClick={() => handleSelectTheme(option.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleSelectTheme(option.value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ThemePreview mode={option.value} />
|
||||
<div className={styles.meta}>
|
||||
<span className={styles.label}>{option.label}</span>
|
||||
{active ? (
|
||||
<CheckCircleFilled className={styles.check} />
|
||||
) : (
|
||||
<span className={styles.radio} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title={intl.formatMessage({ id: 'common.settings.language' })}
|
||||
description={intl.formatMessage({
|
||||
id: 'common.settings.language.tips'
|
||||
})}
|
||||
extra={
|
||||
<BaseSelect
|
||||
value={intl.locale}
|
||||
options={languageOptions}
|
||||
onChange={(value: string) => {
|
||||
setLocale(value, false);
|
||||
}}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SettingsGroup>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { PasswordReg } from '@/config';
|
||||
import { INPUT_WIDTH } from '@/constants';
|
||||
import useSubmitLock from '@/hooks/use-submit-lock';
|
||||
import { updatePassword } from '@/pages/login/apis';
|
||||
import { Input as CInput, FormButtons } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form, message } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
interface FormData {
|
||||
new_password: string;
|
||||
current_password: string;
|
||||
confirm_password?: string;
|
||||
}
|
||||
|
||||
interface ModifyPasswordFormProps {
|
||||
onCancel: () => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const ModifyPasswordForm: React.FC<ModifyPasswordFormProps> = ({
|
||||
onCancel,
|
||||
onSuccess
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const intl = useIntl();
|
||||
const { guard, run, release } = useSubmitLock();
|
||||
|
||||
const handleSubmit = () => {
|
||||
guard(() => form.submit());
|
||||
};
|
||||
|
||||
const onFinish = async (values: FormData) => {
|
||||
await run(async () => {
|
||||
await updatePassword({
|
||||
new_password: values.new_password,
|
||||
current_password: values.current_password
|
||||
});
|
||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||
onSuccess?.();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
name="modifyPasswordForm"
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={release}
|
||||
preserve={false}
|
||||
>
|
||||
<Form.Item<FormData>
|
||||
name="current_password"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage(
|
||||
{ id: 'common.form.rule.input' },
|
||||
{
|
||||
name: intl.formatMessage({
|
||||
id: 'users.form.currentpassword'
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CInput.Password
|
||||
autoComplete="current-password"
|
||||
label={intl.formatMessage({ id: 'users.form.currentpassword' })}
|
||||
required
|
||||
style={{ width: INPUT_WIDTH.default }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name="new_password"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
pattern: PasswordReg,
|
||||
message: intl.formatMessage({
|
||||
id: 'users.form.rule.password'
|
||||
})
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CInput.Password
|
||||
autoComplete="new-password"
|
||||
label={intl.formatMessage({ id: 'users.form.newpassword' })}
|
||||
required
|
||||
style={{ width: INPUT_WIDTH.default }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="confirm_password"
|
||||
dependencies={['new_password']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'users.password.confirm.empty'
|
||||
})
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('new_password') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
intl.formatMessage({ id: 'users.password.confirm.error' })
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<CInput.Password
|
||||
required
|
||||
autoComplete="new-password"
|
||||
style={{ width: INPUT_WIDTH.default }}
|
||||
label={intl.formatMessage({ id: 'users.password.confirm' })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<FormButtons htmlType="submit" onCancel={onCancel} showCancel />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
ModifyPasswordForm.displayName = 'ModifyPasswordForm';
|
||||
|
||||
export default ModifyPasswordForm;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { INPUT_WIDTH } from '@/constants';
|
||||
import { Textarea } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Form } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
interface PublicKeyFormData {
|
||||
public_key?: string;
|
||||
}
|
||||
|
||||
const PublicKey: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const [form] = Form.useForm<PublicKeyFormData>();
|
||||
|
||||
return (
|
||||
<Form style={{ width: '524px' }} name="publicKeyForm" form={form}>
|
||||
<Form.Item<PublicKeyFormData> name="public_key">
|
||||
<Textarea
|
||||
label="SSH 公钥"
|
||||
placeholder="将您的 SSH 公钥粘贴到此处"
|
||||
trim={false}
|
||||
alwaysFocus
|
||||
autoSize={{ minRows: 4, maxRows: 8 }}
|
||||
style={{ width: INPUT_WIDTH.default }}
|
||||
></Textarea>
|
||||
</Form.Item>
|
||||
<Button type="primary" style={{ width: 120, marginTop: 100 }}>
|
||||
{intl.formatMessage({ id: 'common.button.save' })}
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
PublicKey.displayName = 'PublicKey';
|
||||
|
||||
export default PublicKey;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import ModifyPasswordForm from './modify-password-form';
|
||||
import { SettingRow, SettingsGroup } from './settings-group';
|
||||
|
||||
const Security: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<SettingsGroup>
|
||||
<SettingRow
|
||||
title={intl.formatMessage({ id: 'users.form.updatepassword' })}
|
||||
description={intl.formatMessage({
|
||||
id: 'users.password.modify.tips'
|
||||
})}
|
||||
extra={
|
||||
!open && (
|
||||
<Button onClick={() => setOpen(true)}>
|
||||
{intl.formatMessage({ id: 'users.form.updatepassword' })}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
{open && (
|
||||
<ModifyPasswordForm
|
||||
onCancel={() => setOpen(false)}
|
||||
onSuccess={() => setOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</SettingRow>
|
||||
</SettingsGroup>
|
||||
);
|
||||
};
|
||||
|
||||
Security.displayName = 'Security';
|
||||
|
||||
export default Security;
|
||||
@@ -0,0 +1,84 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
import React from 'react';
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
group: css`
|
||||
/* Match the page panel surface (page-box.less): 8px radius + the
|
||||
lighter container border, rather than the heavier component token. */
|
||||
border: 1px solid ${token.colorBorder};
|
||||
border-radius: 8px;
|
||||
background: ${token.colorBgContainer};
|
||||
overflow: hidden;
|
||||
`,
|
||||
row: css`
|
||||
padding: 16px 20px;
|
||||
|
||||
& + & {
|
||||
border-top: 1px solid ${token.colorBorderSecondary};
|
||||
}
|
||||
`,
|
||||
head: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
`,
|
||||
info: css`
|
||||
min-width: 0;
|
||||
`,
|
||||
title: css`
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: ${token.colorText};
|
||||
line-height: 22px;
|
||||
`,
|
||||
description: css`
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
color: ${token.colorTextTertiary};
|
||||
line-height: 20px;
|
||||
`,
|
||||
extra: css`
|
||||
flex-shrink: 0;
|
||||
`,
|
||||
body: css`
|
||||
margin-top: 16px;
|
||||
`
|
||||
}));
|
||||
|
||||
export const SettingsGroup: React.FC<{ children: React.ReactNode }> = ({
|
||||
children
|
||||
}) => {
|
||||
const { styles } = useStyles();
|
||||
return <div className={styles.group}>{children}</div>;
|
||||
};
|
||||
|
||||
interface SettingRowProps {
|
||||
title: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
extra?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SettingRow: React.FC<SettingRowProps> = ({
|
||||
title,
|
||||
description,
|
||||
extra,
|
||||
children
|
||||
}) => {
|
||||
const { styles } = useStyles();
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<div className={styles.head}>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
{description && (
|
||||
<div className={styles.description}>{description}</div>
|
||||
)}
|
||||
</div>
|
||||
{extra && <div className={styles.extra}>{extra}</div>}
|
||||
</div>
|
||||
{children && <div className={styles.body}>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
import React from 'react';
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
section: css`
|
||||
& + & {
|
||||
margin-top: 40px;
|
||||
}
|
||||
`,
|
||||
header: css`
|
||||
margin-bottom: 16px;
|
||||
`,
|
||||
title: css`
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: ${token.colorTextHeading};
|
||||
line-height: 24px;
|
||||
`,
|
||||
description: css`
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
color: ${token.colorTextTertiary};
|
||||
line-height: 20px;
|
||||
`
|
||||
}));
|
||||
|
||||
interface SettingsSectionProps {
|
||||
title: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const SettingsSection: React.FC<SettingsSectionProps> = ({
|
||||
title,
|
||||
description,
|
||||
children
|
||||
}) => {
|
||||
const { styles } = useStyles();
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<div className={styles.header}>
|
||||
<h3 className={styles.title}>{title}</h3>
|
||||
{description && <p className={styles.description}>{description}</p>}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
SettingsSection.displayName = 'SettingsSection';
|
||||
|
||||
export default SettingsSection;
|
||||
@@ -0,0 +1,119 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
import React from 'react';
|
||||
|
||||
export type PreviewMode = 'light' | 'realDark' | 'auto';
|
||||
|
||||
const PALETTE = {
|
||||
light: {
|
||||
surface: '#ffffff',
|
||||
bar: '#e7ebf2',
|
||||
block: '#f1f4f9',
|
||||
accent: '#cdd7e8'
|
||||
},
|
||||
dark: {
|
||||
surface: '#0f1729',
|
||||
bar: '#1d2740',
|
||||
block: '#27324d',
|
||||
accent: '#39445f'
|
||||
}
|
||||
};
|
||||
|
||||
const useStyles = createStyles(({ css }) => ({
|
||||
preview: css`
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 10;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
`,
|
||||
half: css`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
`,
|
||||
topbar: css`
|
||||
height: 8px;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
`,
|
||||
body: css`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`,
|
||||
sidebar: css`
|
||||
width: 22%;
|
||||
border-radius: 4px;
|
||||
`,
|
||||
main: css`
|
||||
flex: 1;
|
||||
border-radius: 4px;
|
||||
`,
|
||||
footer: css`
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
`,
|
||||
systemIcon: css`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 24px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
mix-blend-mode: difference;
|
||||
z-index: 2;
|
||||
`,
|
||||
systemWrap: css`
|
||||
position: relative;
|
||||
`
|
||||
}));
|
||||
|
||||
const Mockup: React.FC<{ tone: 'light' | 'dark'; flex?: number }> = ({
|
||||
tone,
|
||||
flex = 1
|
||||
}) => {
|
||||
const { styles } = useStyles();
|
||||
const c = PALETTE[tone];
|
||||
return (
|
||||
<div
|
||||
className={styles.half}
|
||||
style={{ background: c.surface, flex, minWidth: 0 }}
|
||||
>
|
||||
<div className={styles.topbar} style={{ background: c.accent }} />
|
||||
<div className={styles.body}>
|
||||
<div className={styles.sidebar} style={{ background: c.block }} />
|
||||
<div className={styles.main} style={{ background: c.block }} />
|
||||
</div>
|
||||
<div className={styles.footer} style={{ background: c.bar }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ThemePreview: React.FC<{ mode: PreviewMode }> = ({ mode }) => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
if (mode === 'auto') {
|
||||
return (
|
||||
<div className={`${styles.preview} ${styles.systemWrap}`}>
|
||||
<Mockup tone="light" />
|
||||
<Mockup tone="dark" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.preview}>
|
||||
<Mockup tone={mode === 'realDark' ? 'dark' : 'light'} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ThemePreview.displayName = 'ThemePreview';
|
||||
|
||||
export default ThemePreview;
|
||||
@@ -0,0 +1,69 @@
|
||||
import useTabActive from '@/hooks/use-tab-active';
|
||||
import { useIntl, useModel } from '@umijs/max';
|
||||
import { Tabs, TabsProps } from 'antd';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import PageBox from '../_components/page-box';
|
||||
import Appearance from './components/appearance';
|
||||
import ModifyPasswordn from './components/modify-password';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.ant-page-header-heading {
|
||||
padding-inline: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Profile: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { initialState, setInitialState } = useModel('@@initialState') || {};
|
||||
const { setTabActive, getTabActive, tabsMap } = useTabActive();
|
||||
const [activeKey, setActiveKey] = useState(
|
||||
initialState?.currentUser?.source === 'Local'
|
||||
? 'modify-password'
|
||||
: 'appearance'
|
||||
);
|
||||
|
||||
const items: TabsProps['items'] = useMemo(() => {
|
||||
if (initialState?.currentUser?.source !== 'Local') {
|
||||
return [
|
||||
{
|
||||
key: 'appearance',
|
||||
label: intl.formatMessage({ id: 'common.appearance' }),
|
||||
children: <Appearance />
|
||||
}
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
key: 'modify-password',
|
||||
label: intl.formatMessage({ id: 'users.form.updatepassword' }),
|
||||
children: <ModifyPasswordn />
|
||||
},
|
||||
{
|
||||
key: 'appearance',
|
||||
label: intl.formatMessage({ id: 'common.appearance' }),
|
||||
children: <Appearance />
|
||||
}
|
||||
];
|
||||
}, [intl, initialState?.currentUser?.source]);
|
||||
|
||||
const handleChangeTab = useCallback((key: string) => {
|
||||
setActiveKey(key);
|
||||
setTabActive(tabsMap.userSettings, key);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageBox>
|
||||
<Tabs
|
||||
activeKey={activeKey}
|
||||
onChange={handleChangeTab}
|
||||
items={items}
|
||||
type="card"
|
||||
/>
|
||||
</PageBox>
|
||||
);
|
||||
};
|
||||
|
||||
Profile.displayName = 'Profile';
|
||||
|
||||
export default Profile;
|
||||
+36
-52
@@ -1,65 +1,49 @@
|
||||
import useTabActive from '@/hooks/use-tab-active';
|
||||
import { useIntl, useModel } from '@umijs/max';
|
||||
import { Tabs, TabsProps } from 'antd';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { createStyles } from 'antd-style';
|
||||
import React from 'react';
|
||||
import PageBox from '../_components/page-box';
|
||||
import Appearance from './components/appearance';
|
||||
import ModifyPasswordn from './components/modify-password';
|
||||
import Security from './components/security';
|
||||
import SettingsSection from './components/settings-section';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.ant-page-header-heading {
|
||||
padding-inline: 8px;
|
||||
}
|
||||
`;
|
||||
const useStyles = createStyles(({ css }) => ({
|
||||
wrapper: css`
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 8px 0 40px;
|
||||
`
|
||||
}));
|
||||
|
||||
const Profile: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { initialState, setInitialState } = useModel('@@initialState') || {};
|
||||
const { setTabActive, getTabActive, tabsMap } = useTabActive();
|
||||
const [activeKey, setActiveKey] = useState(
|
||||
initialState?.currentUser?.source === 'Local'
|
||||
? 'modify-password'
|
||||
: 'appearance'
|
||||
);
|
||||
|
||||
const items: TabsProps['items'] = useMemo(() => {
|
||||
if (initialState?.currentUser?.source !== 'Local') {
|
||||
return [
|
||||
{
|
||||
key: 'appearance',
|
||||
label: intl.formatMessage({ id: 'common.appearance' }),
|
||||
children: <Appearance />
|
||||
}
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
key: 'modify-password',
|
||||
label: intl.formatMessage({ id: 'users.form.updatepassword' }),
|
||||
children: <ModifyPasswordn />
|
||||
},
|
||||
{
|
||||
key: 'appearance',
|
||||
label: intl.formatMessage({ id: 'common.appearance' }),
|
||||
children: <Appearance />
|
||||
}
|
||||
];
|
||||
}, [intl, initialState?.currentUser?.source]);
|
||||
|
||||
const handleChangeTab = useCallback((key: string) => {
|
||||
setActiveKey(key);
|
||||
setTabActive(tabsMap.userSettings, key);
|
||||
}, []);
|
||||
const { styles } = useStyles();
|
||||
const { initialState } = useModel('@@initialState') || {};
|
||||
const isLocalUser = initialState?.currentUser?.source === 'Local';
|
||||
|
||||
return (
|
||||
<PageBox>
|
||||
<Tabs
|
||||
activeKey={activeKey}
|
||||
onChange={handleChangeTab}
|
||||
items={items}
|
||||
type="card"
|
||||
/>
|
||||
<div className={styles.wrapper}>
|
||||
<SettingsSection
|
||||
title={intl.formatMessage({ id: 'common.appearance' })}
|
||||
description={intl.formatMessage({
|
||||
id: 'common.appearance.description'
|
||||
})}
|
||||
>
|
||||
<Appearance />
|
||||
</SettingsSection>
|
||||
|
||||
{isLocalUser && (
|
||||
<SettingsSection
|
||||
title={intl.formatMessage({ id: 'common.security' })}
|
||||
description={intl.formatMessage({
|
||||
id: 'common.security.description'
|
||||
})}
|
||||
>
|
||||
<Security />
|
||||
</SettingsSection>
|
||||
)}
|
||||
</div>
|
||||
</PageBox>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user