0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(console): refactor modals with modallayout (#435)

This commit is contained in:
Xiao Yijun 2022-03-22 17:40:06 +08:00 committed by GitHub
parent 2e08ec9db0
commit 4f41162ac3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 211 additions and 226 deletions

View file

@ -30,5 +30,14 @@
"_.unit(${1:2})" "_.unit(${1:2})"
], ],
"description": "Use underscore unit function." "description": "Use underscore unit function."
},
"Use dimensions": {
"scope": "scss",
"prefix": "used",
"body": [
"@use '@/scss/dimensions' as dim;",
"$0"
],
"description": "Add @use dimensions in header."
} }
} }

View file

@ -9,6 +9,10 @@
margin-left: _.unit(4); margin-left: _.unit(4);
} }
.icon {
flex-shrink: 0;
}
.title { .title {
font: var(--font-body); font: var(--font-body);
color: var(--color-primary); color: var(--color-primary);

View file

@ -13,7 +13,7 @@ type Props = {
const ItemPreview = ({ title, subtitle, icon, to }: Props) => { const ItemPreview = ({ title, subtitle, icon, to }: Props) => {
return ( return (
<div className={styles.item}> <div className={styles.item}>
{icon} {icon && <div className={styles.icon}>{icon}</div>}
<div> <div>
{to && ( {to && (
<Link <Link

View file

@ -1,9 +1,10 @@
@use '@/scss/dimensions' as dim;
@use '@/scss/underscore' as _; @use '@/scss/underscore' as _;
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-width: 800px; max-width: dim.$modal-layout-max-width;
padding: _.unit(8); padding: _.unit(8);
.header { .header {

View file

@ -1,25 +1,10 @@
@use '@/scss/underscore' as _; @use '@/scss/underscore' as _;
@use '@/scss/dimensions' as dim;
.card {
padding: _.unit(8);
max-width: 800px;
}
.headline {
display: flex;
justify-content: space-between;
> *:not(:first-child) {
margin-left: _.unit(3);
}
> svg {
cursor: pointer;
}
}
.form { .form {
margin-top: _.unit(8); padding: _.unit(8) 0;
display: flex;
align-items: flex-end;
} }
.textField { .textField {
@ -27,6 +12,5 @@
} }
.submit { .submit {
margin-top: _.unit(8); padding-left: dim.$form-text-field-width;
text-align: right;
} }

View file

@ -1,15 +1,12 @@
import { Resource } from '@logto/schemas'; import { Resource } from '@logto/schemas';
import React from 'react'; import React, { useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import FormField from '@/components/FormField'; import FormField from '@/components/FormField';
import IconButton from '@/components/IconButton'; import ModalLayout from '@/components/ModalLayout';
import TextInput from '@/components/TextInput'; import TextInput from '@/components/TextInput';
import useApi from '@/hooks/use-api'; import useApi from '@/hooks/use-api';
import Close from '@/icons/Close';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
@ -24,26 +21,43 @@ type Props = {
const CreateForm = ({ onClose }: Props) => { const CreateForm = ({ onClose }: Props) => {
const { handleSubmit, register } = useForm<FormData>(); const { handleSubmit, register } = useForm<FormData>();
const [loading, setLoading] = useState(false);
const api = useApi(); const api = useApi();
const onSubmit = handleSubmit(async (data) => { const onSubmit = handleSubmit(async (data) => {
if (loading) {
return;
}
setLoading(true);
try { try {
const createdApiResource = await api.post('/api/resources', { json: data }).json<Resource>(); const createdApiResource = await api.post('/api/resources', { json: data }).json<Resource>();
onClose?.(createdApiResource); onClose?.(createdApiResource);
} catch (error: unknown) { } finally {
console.error(error); setLoading(false);
} }
}); });
return ( return (
<Card className={styles.card}> <ModalLayout
<div className={styles.headline}> title="api_resources.create"
<CardTitle title="api_resources.create" subtitle="api_resources.subtitle" /> subtitle="api_resources.subtitle"
<IconButton size="large" onClick={() => onClose?.()}> footer={
<Close /> <div className={styles.submit}>
</IconButton> <Button
</div> disabled={loading}
<form className={styles.form} onSubmit={onSubmit}> htmlType="submit"
title="admin_console.api_resources.create"
size="large"
type="primary"
onClick={onSubmit}
/>
</div>
}
onClose={onClose}
>
<form className={styles.form}>
<FormField <FormField
isRequired isRequired
title="admin_console.api_resources.api_name" title="admin_console.api_resources.api_name"
@ -58,16 +72,8 @@ const CreateForm = ({ onClose }: Props) => {
> >
<TextInput {...register('indicator', { required: true })} /> <TextInput {...register('indicator', { required: true })} />
</FormField> </FormField>
<div className={styles.submit}>
<Button
htmlType="submit"
title="admin_console.api_resources.create"
size="large"
type="primary"
/>
</div>
</form> </form>
</Card> </ModalLayout>
); );
}; };

View file

@ -1,6 +1,7 @@
import { Resource } from '@logto/schemas'; import { Resource } from '@logto/schemas';
import { conditional } from '@silverhand/essentials/lib/utilities/conditional.js'; import { conditional } from '@silverhand/essentials/lib/utilities/conditional.js';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Modal from 'react-modal'; import Modal from 'react-modal';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -49,6 +50,9 @@ const ApiResources = () => {
if (createdApiResource) { if (createdApiResource) {
void mutate(conditional(data && [...data, createdApiResource])); void mutate(conditional(data && [...data, createdApiResource]));
toast.success(
t('api_resources.api_resource_created', { name: createdApiResource.name })
);
navigate(buildDetailsLink(createdApiResource.id)); navigate(buildDetailsLink(createdApiResource.id));
} }
}} }}

View file

@ -1,24 +1,9 @@
@use '@/scss/underscore' as _; @use '@/scss/underscore' as _;
@use '@/scss/dimensions' as dim;
.card {
padding: _.unit(8);
}
.headline {
display: flex;
justify-content: space-between;
> *:not(:first-child) {
margin-left: _.unit(3);
}
> svg {
cursor: pointer;
}
}
.form { .form {
margin-top: _.unit(8); padding: _.unit(8) 0;
} }
.textField { .textField {
@ -26,8 +11,7 @@
} }
.submit { .submit {
margin-top: _.unit(8); padding-left: dim.$form-text-field-width;
text-align: right;
} }
.error { .error {

View file

@ -6,14 +6,11 @@ import Modal from 'react-modal';
import useSWR from 'swr'; import useSWR from 'swr';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import FormField from '@/components/FormField'; import FormField from '@/components/FormField';
import IconButton from '@/components/IconButton'; import ModalLayout from '@/components/ModalLayout';
import RadioGroup, { Radio } from '@/components/RadioGroup'; import RadioGroup, { Radio } from '@/components/RadioGroup';
import TextInput from '@/components/TextInput'; import TextInput from '@/components/TextInput';
import useApi, { RequestError } from '@/hooks/use-api'; import useApi, { RequestError } from '@/hooks/use-api';
import Close from '@/icons/Close';
import * as modalStyles from '@/scss/modal.module.scss'; import * as modalStyles from '@/scss/modal.module.scss';
import { applicationTypeI18nKey } from '@/types/applications'; import { applicationTypeI18nKey } from '@/types/applications';
@ -46,6 +43,7 @@ const CreateForm = ({ onClose }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { data: setting } = useSWR<Setting, RequestError>('/api/settings'); const { data: setting } = useSWR<Setting, RequestError>('/api/settings');
const api = useApi(); const api = useApi();
const [loading, setLoading] = useState(false);
const isGetStartedSkipped = setting?.adminConsole.applicationSkipGetStarted; const isGetStartedSkipped = setting?.adminConsole.applicationSkipGetStarted;
@ -55,6 +53,12 @@ const CreateForm = ({ onClose }: Props) => {
}; };
const onSubmit = handleSubmit(async (data) => { const onSubmit = handleSubmit(async (data) => {
if (loading) {
return;
}
setLoading(true);
try { try {
const createdApp = await api.post('/api/applications', { json: data }).json<Application>(); const createdApp = await api.post('/api/applications', { json: data }).json<Application>();
setCreatedApp(createdApp); setCreatedApp(createdApp);
@ -64,20 +68,30 @@ const CreateForm = ({ onClose }: Props) => {
} else { } else {
setIsQuickStartGuideOpen(true); setIsQuickStartGuideOpen(true);
} }
} catch (error: unknown) { } finally {
console.error(error); setLoading(false);
} }
}); });
return ( return (
<Card className={styles.card}> <ModalLayout
<div className={styles.headline}> title="applications.create"
<CardTitle title="applications.create" subtitle="applications.subtitle" /> subtitle="applications.subtitle"
<IconButton size="large" onClick={() => onClose?.()}> footer={
<Close /> <div className={styles.submit}>
</IconButton> <Button
</div> disabled={loading}
<form className={styles.form} onSubmit={onSubmit}> htmlType="submit"
title="admin_console.applications.create"
size="large"
type="primary"
onClick={onSubmit}
/>
</div>
}
onClose={onClose}
>
<form className={styles.form}>
<FormField title="admin_console.applications.select_application_type"> <FormField title="admin_console.applications.select_application_type">
<RadioGroup ref={ref} name={name} value={value} onChange={onChange}> <RadioGroup ref={ref} name={name} value={value} onChange={onChange}>
{Object.values(ApplicationType).map((value) => ( {Object.values(ApplicationType).map((value) => (
@ -106,21 +120,13 @@ const CreateForm = ({ onClose }: Props) => {
> >
<TextInput {...register('description')} /> <TextInput {...register('description')} />
</FormField> </FormField>
<div className={styles.submit}>
<Button
htmlType="submit"
title="admin_console.applications.create"
size="large"
type="primary"
/>
</div>
</form> </form>
{!isGetStartedSkipped && createdApp && ( {!isGetStartedSkipped && createdApp && (
<Modal isOpen={isQuickStartGuideOpen} className={modalStyles.fullScreen}> <Modal isOpen={isQuickStartGuideOpen} className={modalStyles.fullScreen}>
<GetStarted appName={createdApp.name} onClose={closeModal} /> <GetStarted appName={createdApp.name} onClose={closeModal} />
</Modal> </Modal>
)} )}
</Card> </ModalLayout>
); );
}; };

View file

@ -1,15 +1,10 @@
@use '@/scss/underscore' as _; @use '@/scss/underscore' as _;
@use '@/scss/dimensions' as dim;
.card {
min-width: _.unit(100);
}
.header h1 { .content {
font: var(--font-title-large); padding: _.unit(8) 0;
margin-top: 0; min-width: dim.$modal-layout-min-width;
}
.body {
font: var(--font-body-2); font: var(--font-body-2);
.info { .info {
@ -39,9 +34,6 @@
} }
.footer { .footer {
border-top: 1px solid var(--color-neutral-80);
margin-top: _.unit(6);
padding-top: _.unit(6);
display: flex; display: flex;
justify-content: right; justify-content: right;

View file

@ -5,8 +5,8 @@ import ReactModal from 'react-modal';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Card from '@/components/Card';
import IconButton from '@/components/IconButton'; import IconButton from '@/components/IconButton';
import ModalLayout from '@/components/ModalLayout';
import Eye from '@/icons/Eye'; import Eye from '@/icons/Eye';
import * as modalStyles from '@/scss/modal.module.scss'; import * as modalStyles from '@/scss/modal.module.scss';
@ -44,11 +44,20 @@ const CreateSuccess = ({ username }: Props) => {
return ( return (
<ReactModal isOpen className={modalStyles.content} overlayClassName={modalStyles.overlay}> <ReactModal isOpen className={modalStyles.content} overlayClassName={modalStyles.overlay}>
<Card className={styles.card}> <ModalLayout
<div className={styles.header}> title="user_details.created_title"
<h1>{t('user_details.created_title')}</h1> footer={
</div> <div className={styles.footer}>
<div className={styles.body}> <Button title="admin_console.user_details.created_button_close" onClick={handleClose} />
<Button
type="primary"
title="admin_console.user_details.created_button_copy"
onClick={handleCopy}
/>
</div>
}
>
<div className={styles.content}>
<div>{t('user_details.created_guide')}</div> <div>{t('user_details.created_guide')}</div>
<div className={styles.info}> <div className={styles.info}>
<div className={styles.infoLine}> <div className={styles.infoLine}>
@ -72,15 +81,7 @@ const CreateSuccess = ({ username }: Props) => {
</div> </div>
</div> </div>
</div> </div>
<div className={styles.footer}> </ModalLayout>
<Button title="admin_console.user_details.created_button_close" onClick={handleClose} />
<Button
type="primary"
title="admin_console.user_details.created_button_copy"
onClick={handleCopy}
/>
</div>
</Card>
</ReactModal> </ReactModal>
); );
}; };

View file

@ -1,29 +1,17 @@
@use '@/scss/underscore' as _; @use '@/scss/underscore' as _;
@use '@/scss/dimensions' as dim;
.card {
padding: _.unit(8);
min-width: 400px;
> :not(:first-child) { .content {
margin-top: _.unit(6); min-width: dim.$modal-layout-min-width;
} padding: _.unit(8) 0;
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.footer {
border-top: 1px solid var(--color-neutral-80);
margin-top: _.unit(6);
padding-top: _.unit(6);
display: flex;
justify-content: right;
button:not(:last-child) {
margin-right: _.unit(2);
}
}
} }
.footer {
display: flex;
justify-content: right;
button:not(:last-child) {
margin-right: _.unit(2);
}
}

View file

@ -4,11 +4,8 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Card from '@/components/Card'; import ModalLayout from '@/components/ModalLayout';
import CardTitle from '@/components/CardTitle';
import IconButton from '@/components/IconButton';
import useApi from '@/hooks/use-api'; import useApi from '@/hooks/use-api';
import Close from '@/icons/Close';
import * as styles from './DeleteForm.module.scss'; import * as styles from './DeleteForm.module.scss';
@ -39,24 +36,29 @@ const DeleteForm = ({ id, onClose }: Props) => {
}; };
return ( return (
<Card className={styles.card}> <ModalLayout
<div className={styles.header}> title="user_details.delete_title"
<CardTitle title="user_details.delete_title" /> footer={
<IconButton size="large" onClick={onClose}> <div className={styles.footer}>
<Close /> <Button
</IconButton> type="outline"
title="admin_console.user_details.delete_cancel"
onClick={onClose}
/>
<Button
disabled={loading}
type="danger"
title="admin_console.user_details.delete_confirm"
onClick={handleDelete}
/>
</div>
}
onClose={onClose}
>
<div className={styles.content}>
<div>{t('user_details.delete_description')}</div>
</div> </div>
<div>{t('user_details.delete_description')}</div> </ModalLayout>
<div className={styles.footer}>
<Button type="outline" title="admin_console.user_details.delete_cancel" onClick={onClose} />
<Button
disabled={loading}
type="danger"
title="admin_console.user_details.delete_confirm"
onClick={handleDelete}
/>
</div>
</Card>
); );
}; };

View file

@ -1,15 +1,14 @@
import { User } from '@logto/schemas'; import { User } from '@logto/schemas';
import React from 'react'; import React, { useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import FormField from '@/components/FormField'; import FormField from '@/components/FormField';
import IconButton from '@/components/IconButton'; import ModalLayout from '@/components/ModalLayout';
import TextInput from '@/components/TextInput'; import TextInput from '@/components/TextInput';
import useApi from '@/hooks/use-api'; import useApi from '@/hooks/use-api';
import Close from '@/icons/Close';
import * as styles from './ResetPasswordForm.module.scss'; import * as styles from './ResetPasswordForm.module.scss';
@ -23,22 +22,28 @@ type Props = {
}; };
const ResetPasswordForm = ({ onClose, userId }: Props) => { const ResetPasswordForm = ({ onClose, userId }: Props) => {
const { t } = useTranslation(undefined, {
keyPrefix: 'admin_console',
});
const { handleSubmit, register } = useForm<FormData>(); const { handleSubmit, register } = useForm<FormData>();
const api = useApi(); const api = useApi();
const [loading, setLoading] = useState(false);
const onSubmit = handleSubmit(async (data) => { const onSubmit = handleSubmit(async (data) => {
await api.patch(`/api/users/${userId}/password`, { json: data }).json<User>(); setLoading(true);
onClose?.();
try {
await api.patch(`/api/users/${userId}/password`, { json: data }).json<User>();
onClose?.();
toast.success(t('user_details.reset_password.reset_password_success'));
} finally {
setLoading(false);
}
}); });
return ( return (
<Card className={styles.card}> <ModalLayout title="user_details.reset_password.title" onClose={onClose}>
<div className={styles.headline}>
<CardTitle title="user_details.reset_password.title" />
<IconButton size="large" onClick={() => onClose?.()}>
<Close />
</IconButton>
</div>
<form className={styles.form} onSubmit={onSubmit}> <form className={styles.form} onSubmit={onSubmit}>
<FormField <FormField
isRequired isRequired
@ -49,6 +54,7 @@ const ResetPasswordForm = ({ onClose, userId }: Props) => {
</FormField> </FormField>
<div className={styles.submit}> <div className={styles.submit}>
<Button <Button
disabled={loading}
htmlType="submit" htmlType="submit"
title="admin_console.user_details.reset_password.reset_password" title="admin_console.user_details.reset_password.reset_password"
size="large" size="large"
@ -56,7 +62,7 @@ const ResetPasswordForm = ({ onClose, userId }: Props) => {
/> />
</div> </div>
</form> </form>
</Card> </ModalLayout>
); );
}; };

View file

@ -1,24 +1,9 @@
@use '@/scss/underscore' as _; @use '@/scss/underscore' as _;
@use '@/scss/dimensions' as dim;
.card {
padding: _.unit(8);
}
.headline {
display: flex;
justify-content: space-between;
> *:not(:first-child) {
margin-left: _.unit(3);
}
> svg {
cursor: pointer;
}
}
.form { .form {
margin-top: _.unit(8); padding: _.unit(8) 0;
} }
.textField { .textField {
@ -26,12 +11,5 @@
} }
.submit { .submit {
margin-top: _.unit(8); padding-left: dim.$form-text-field-width;
text-align: right;
}
.error {
font: var(--font-body-2);
color: var(--color-error);
margin-top: _.unit(2);
} }

View file

@ -1,15 +1,12 @@
import { User } from '@logto/schemas'; import { User } from '@logto/schemas';
import React from 'react'; import React, { useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import FormField from '@/components/FormField'; import FormField from '@/components/FormField';
import IconButton from '@/components/IconButton'; import ModalLayout from '@/components/ModalLayout';
import TextInput from '@/components/TextInput'; import TextInput from '@/components/TextInput';
import useApi from '@/hooks/use-api'; import useApi from '@/hooks/use-api';
import Close from '@/icons/Close';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
@ -27,20 +24,42 @@ const CreateForm = ({ onClose }: Props) => {
const { handleSubmit, register } = useForm<FormData>(); const { handleSubmit, register } = useForm<FormData>();
const api = useApi(); const api = useApi();
const [loading, setLoading] = useState(false);
const onSubmit = handleSubmit(async (data) => { const onSubmit = handleSubmit(async (data) => {
const createdUser = await api.post('/api/users', { json: data }).json<User>(); if (loading) {
onClose?.(createdUser, btoa(data.password)); return;
}
setLoading(true);
try {
const createdUser = await api.post('/api/users', { json: data }).json<User>();
onClose?.(createdUser, btoa(data.password));
} finally {
setLoading(false);
}
}); });
return ( return (
<Card className={styles.card}> <ModalLayout
<div className={styles.headline}> title="users.create"
<CardTitle title="users.create" subtitle="users.subtitle" /> subtitle="users.subtitle"
<IconButton size="large" onClick={() => onClose?.()}> footer={
<Close /> <div className={styles.submit}>
</IconButton> <Button
</div> disabled={loading}
<form className={styles.form} onSubmit={onSubmit}> htmlType="submit"
title="admin_console.users.create"
size="large"
type="primary"
onClick={onSubmit}
/>
</div>
}
onClose={onClose}
>
<form className={styles.form}>
<FormField <FormField
isRequired isRequired
title="admin_console.users.create_form_username" title="admin_console.users.create_form_username"
@ -62,16 +81,8 @@ const CreateForm = ({ onClose }: Props) => {
> >
<TextInput {...register('password', { required: true })} /> <TextInput {...register('password', { required: true })} />
</FormField> </FormField>
<div className={styles.submit}>
<Button
htmlType="submit"
title="admin_console.users.create"
size="large"
type="primary"
/>
</div>
</form> </form>
</Card> </ModalLayout>
); );
}; };

View file

@ -0,0 +1,3 @@
$modal-layout-max-width: 800px;
$modal-layout-min-width: 400px;
$form-text-field-width: 556px;

View file

@ -1,3 +1,5 @@
@use '@/scss/dimensions' as dim;
@function unit($factor: 1, $unit: 'px') { @function unit($factor: 1, $unit: 'px') {
@return #{$factor * 4}#{$unit}; @return #{$factor * 4}#{$unit};
} }
@ -16,7 +18,7 @@
} }
@mixin form-text-field { @mixin form-text-field {
width: 556px; width: dim.$form-text-field-width;
} }
@mixin vertical-bar { @mixin vertical-bar {

View file

@ -135,6 +135,7 @@ const translation = {
create: 'Create API Resource', create: 'Create API Resource',
api_name: 'API Name', api_name: 'API Name',
api_identifier: 'API Identifier', api_identifier: 'API Identifier',
api_resource_created: 'The API resource {{name}} has been successfully created!',
}, },
api_resource_details: { api_resource_details: {
back_to_api_resources: 'Back to my API resources', back_to_api_resources: 'Back to my API resources',
@ -222,6 +223,7 @@ const translation = {
title: 'Reset Password', title: 'Reset Password',
label: 'New password:', label: 'New password:',
reset_password: 'Reset password', reset_password: 'Reset password',
reset_password_success: 'Reset password successfully.',
}, },
tab_settings: 'Settings', tab_settings: 'Settings',
tab_logs: 'User Logs', tab_logs: 'User Logs',

View file

@ -135,6 +135,7 @@ const translation = {
create: 'Create API Resource', create: 'Create API Resource',
api_name: 'API Name', api_name: 'API Name',
api_identifier: 'API Identifier', api_identifier: 'API Identifier',
api_resource_created: 'The API resource {{name}} has been successfully created!',
}, },
api_resource_details: { api_resource_details: {
back_to_api_resources: 'Back to my API resources', back_to_api_resources: 'Back to my API resources',
@ -221,6 +222,7 @@ const translation = {
title: '重置密码', title: '重置密码',
label: '新密码:', label: '新密码:',
reset_password: '重置密码', reset_password: '重置密码',
reset_password_success: '密码已成功重置。',
}, },
tab_settings: '设置', tab_settings: '设置',
tab_logs: '用户日志', tab_logs: '用户日志',