Skip to content

Commit

Permalink
feat(admin/pages/current): 添加 profile 页面
Browse files Browse the repository at this point in the history
  • Loading branch information
caixw committed Oct 10, 2024
1 parent 8d0c00e commit 053bb44
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 41 deletions.
22 changes: 19 additions & 3 deletions admin/src/messages/cmn-Hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const messages: Messages = {
refresh: '刷新',
areYouSure: '你确定要这么做吗?',
page: { // 页面的翻译内容
save: '保存',
update: '更新',
editItem: '编辑',
deleteItem: '删除',
newItem: '新建',
Expand All @@ -36,17 +38,26 @@ const messages: Messages = {
unknown: '未知'
},
current: { // 登录框
dashboard: '仪表盘',
logout: '退出',
settings: '设置',

title: '登录',
username: '账号',
password: '密码',
home: '首页',
logout: '退出',
settings: '设置',

securitylog: '安全日志',
content: '内容',
ip: 'IP',
ua: 'UA',
uaInfo: '浏览器: {browser}({browserVersion}) 系统: {os}({osVersion}) 内核: {kernal}({kernalVersion})',

profile: '个人信息',
name: '姓名',
nickname: '昵称',
oldPassword: '旧密码',
newPassword: '新密码',
confirmPassword: '确认新密码',
},
system: {
apis: 'API',
Expand Down Expand Up @@ -157,6 +168,11 @@ const messages: Messages = {
pageNotFound: '页面不存在',
forbidden: '无权访问当前页面',
internalServerError: '服务端错误',

// 以下为一些内置的错误提示信息
canNotBeEmpty: '不能为空',
oldNewPasswordCanNotBeEqual: '新密码不能与旧密码相同',
newConfirmPasswordMustBeEqual: '确认密码与新密码并不相同',
},
theme: {
mode: '主题模式',
Expand Down
19 changes: 18 additions & 1 deletion admin/src/messages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const messages = {
refresh: 'refresh',
areYouSure: 'are you sure?',
page: { // 页面的翻译内容
save: 'save',
update: 'update',
editItem: 'edit item',
deleteItem: 'delete item',
newItem: 'new item',
Expand All @@ -34,17 +36,26 @@ const messages = {
unknown: 'unknown'
},
current: {
home: 'Home',
dashboard: 'Dashboard',
logout: 'logout',
settings: 'Settings',

title: 'login',
username: 'username',
password: 'password',

securitylog: 'security logs',
content: 'content',
ip: 'IP',
ua: 'user agent',
uaInfo: 'browser: {browser}({browserVersion}) os: {os}({osVersion}) kernal: {kernal}({kernalVersion})',

profile: 'profle',
name: 'name',
nickname: 'nickname',
oldPassword: 'old password',
newPassword: 'new password',
confirmPassword: 'confirm password',
},
system: {
apis: 'API',
Expand Down Expand Up @@ -155,6 +166,12 @@ const messages = {
pageNotFound: 'page not found',
forbidden: 'forbidden',
internalServerError: 'server error',

// 以下为一些内置的错误提示信息
canNotBeEmpty: 'can not be empty',
oldNewPasswordCanNotBeEqual: 'The old passowrd and new password can not be equal',
newConfirmPasswordMustBeEqual: 'The new passowrd and confirm password must be equal',

},
theme: {
mode: 'theme mode',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { JSX } from 'solid-js';
import { Page } from '@/components';

export default function(): JSX.Element {
return <Page title='_i.page.current.home'>
return <Page title='_i.page.current.dashboard'>
home
</Page>;
}
}
28 changes: 18 additions & 10 deletions admin/src/pages/current/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@
//
// SPDX-License-Identifier: MIT

import { Pages } from '@/pages/pages';
import { MenuItem, Route } from '@/app';
import { default as Home } from './home';
import { default as Settings } from './settings';
import { default as Logout } from './logout';
import { Pages } from '@/pages/pages';
import { default as Dashboard } from './dashboard';
import { default as Login } from './login';
import { default as Logout } from './logout';
import { default as Profile } from './profile';
import { default as SecurityLogs } from './securitylogs';
import { default as Settings } from './settings';

/**
* 提供了与当前登录用户直接相关的页面
*/
export class current implements Pages {
/**
* 提供当前用户的首页面板
* 提供当前用户的仪表盘
*/
static Home = Home;
static Dashboard = Dashboard;

/**
* 提供当前用户的设置面
Expand All @@ -34,6 +35,11 @@ export class current implements Pages {
*/
static Logout = Logout;

/**
* 当前用户的个人信息面板
*/
static Profile = Profile;

/**
* 用户的安全日志
*/
Expand All @@ -51,7 +57,8 @@ export class current implements Pages {

routes(): Array<Route> {
return [
{ path: this.#prefix + '/home', component:Home },
{ path: this.#prefix + '/dashboard', component:Dashboard },
{ path: this.#prefix + '/profile', component:Profile },
{ path: this.#prefix + '/settings', component: Settings },
{ path: this.#prefix + '/securitylogs', component: SecurityLogs },
{ path: this.#prefix + '/logout', component: Logout },
Expand All @@ -60,11 +67,12 @@ export class current implements Pages {

menus(): Array<MenuItem> {
return [
{ type: 'item', label: '_i.page.current.home', path: this.#prefix + '/home', icon: 'home' },
{ type: 'item', label: '_i.page.current.dashboard', path: this.#prefix + '/dashboard', icon: 'dashboard' },
{ type: 'item', label: '_i.page.current.profile', path: this.#prefix + '/profile', icon: 'id_card' },
{ type: 'item', label: '_i.page.current.settings', path: this.#prefix + '/settings', icon: 'settings' },
{ type: 'item', label: '_i.page.current.securitylog', path: this.#prefix + '/securitylogs', icon: 'badge' },
{ type: 'item', label: '_i.page.current.securitylog', path: this.#prefix + '/securitylogs', icon: 'security' },
{ type: 'divider' },
{ type: 'item', label: '_i.page.current.logout', path: this.#prefix + '/logout', icon: 'logout' },
];
}
}
}
107 changes: 107 additions & 0 deletions admin/src/pages/current/profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2024 caixw
//
// SPDX-License-Identifier: MIT

import { createEffect, JSX } from 'solid-js';

import { useApp, useOptions, User } from '@/app/context';
import { buildEnumsOptions, Button, Choice, Divider, Form, FormAccessor, Page, Password, TextField } from '@/components';
import { Sex, sexesMap } from '@/pages/admins/types';

export default function(): JSX.Element {
const opt = useOptions();
const ctx = useApp();

const infoAccess = new FormAccessor<User>({sex: 'unknown'}, ctx, 'PATCH', opt.api.info, async () => {
await ctx.refetchUser();
}, (obj) => {
if (!obj.name) {
return new Map([['name', ctx.locale().t('_i.error.canNotBeEmpty')]]);
}
if (!obj.nickname) {
return new Map([['nickname', ctx.locale().t('_i.error.canNotBeEmpty')]]);
}
});

const nameA = infoAccess.accessor<string>('name');
const nicknameA = infoAccess.accessor<string>('nickname');
const sexA = infoAccess.accessor<Sex>('sex');
const avatarA = infoAccess.accessor<string>('avatar');

createEffect(() => {
const u = ctx.user();
if (!u) { return; }

infoAccess.setPreset({ name: u.name, nickname: u.nickname, sex: u.sex });

nameA.setValue(u.name!);
nicknameA.setValue(u.nickname!);
sexA.setValue(u.sex!);
avatarA.setValue(u.avatar!);
});

return <Page title='_i.page.current.profile' class="p--profile max-w-md">
<div class="flex gap-4">
<img class="rounded-full border border-palette-bg-low w-24 h-24" alt="avatar" src={avatarA.getValue() ?? opt.logo} />
<div class="flex flex-col my-4 items-start justify-center gap-2">
<p class="text-2xl">{ctx.user()?.name}</p>
<Button palette='tertiary'>更改头像</Button>
</div>
</div>

<Divider padding='4px' />

<div class="content">
<Form formAccessor={infoAccess} class="form">
<TextField class="w-full" label={ctx.locale().t('_i.page.current.name')} accessor={nameA} />
<TextField class="w-full" label={ctx.locale().t('_i.page.current.nickname')} accessor={nicknameA} />
<Choice class="w-full" label={ctx.locale().t('_i.page.sex')} accessor={sexA} options={buildEnumsOptions(sexesMap, ctx)} />

<div class="actions">
<Button palette="secondary" type="reset" disabled={infoAccess.isPreset()}>{ctx.locale().t('_i.reset')}</Button>
<Button palette="primary" type="submit" disabled={infoAccess.isPreset()}>{ctx.locale().t('_i.page.save')}</Button>
</div>
</Form>

<hr class="w-full border-t border-palette-bg-low sm:hidden" />

<Pass />
</div>
</Page>;
}

interface ProfilePassword {
old: string;
new: string;
confirm: string;
}

function Pass(): JSX.Element {
const ctx = useApp();

const passAccess = new FormAccessor<ProfilePassword>({ old: '', new: '', confirm: '' }, ctx, 'PUT', '/password', undefined, (obj)=>{
if (obj.old === '') {
return new Map([['old', ctx.locale().t('_i.error.canNotBeEmpty')]]);
}
if (obj.new === '') {
return new Map([['new', ctx.locale().t('_i.error.canNotBeEmpty')]]);
}

if (obj.old === obj.new) {
return new Map([['new', ctx.locale().t('_i.error.oldNewPasswordCanNotBeEqual')]]);
}
if (obj.new !== obj.confirm) {
return new Map([['confirm', ctx.locale().t('_i.error.newConfirmPasswordMustBeEqual')]]);
}
});

return <Form formAccessor={passAccess} class="form">
<Password class="w-full" autocomplete='current-password' label={ctx.locale().t('_i.page.current.oldPassword')} accessor={passAccess.accessor('old')} />
<Password class="w-full" autocomplete='new-password' label={ctx.locale().t('_i.page.current.newPassword')} accessor={passAccess.accessor('new')} />
<Password class="w-full" autocomplete='off' label={ctx.locale().t('_i.page.current.confirmPassword')} accessor={passAccess.accessor('confirm')} />

<div class="actions">
<Button disabled={passAccess.isPreset()} palette="primary" type='submit'>{ctx.locale().t('_i.page.update')}</Button>
</div>
</Form>;
}
24 changes: 12 additions & 12 deletions admin/src/pages/current/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

import { JSX } from 'solid-js';

import { Divider, Options, Choice, Label, FieldAccessor, RadioGroup, Page, Description } from '@/components';
import { Mode, Scheme, Theme, Contrast, UnitStyle } from '@/core';
import { useOptions, useApp } from '@/app/context';
import { useApp, useOptions } from '@/app/context';
import { Choice, Description, Divider, FieldAccessor, Options, Page, RadioGroup } from '@/components';
import { Contrast, Mode, Scheme, Theme, UnitStyle } from '@/core';

export default function(): JSX.Element {
const ctx = useApp();
Expand Down Expand Up @@ -40,9 +40,9 @@ export default function(): JSX.Element {

<RadioGroup accessor={modeFA} block={/*@once*/false}
options={/*@once*/[
['system', <Label icon={/*@once*/'brightness_6'}>{ctx.locale().t('_i.theme.system')}</Label>],
['dark', <Label icon={/*@once*/'dark_mode'}>{ctx.locale().t('_i.theme.dark')}</Label>],
['light', <Label icon={/*@once*/'brightness_6'}>{ctx.locale().t('_i.theme.light')}</Label>]
['system', ctx.locale().t('_i.theme.system')],
['dark', ctx.locale().t('_i.theme.dark')],
['light', ctx.locale().t('_i.theme.light')]
]}
/>

Expand All @@ -54,9 +54,9 @@ export default function(): JSX.Element {

<RadioGroup accessor={contrastFA} block={/*@once*/false}
options={/*@once*/[
['more', <Label icon={/*@once*/'exposure_plus_1'}>{ctx.locale().t('_i.theme.more')}</Label>],
['nopreference', <Label icon={/*@once*/'exposure_zero'}>{ctx.locale().t('_i.theme.nopreference')}</Label>],
['less', <Label icon={/*@once*/'exposure_neg_1'}>{ctx.locale().t('_i.theme.less')}</Label>]
['more', ctx.locale().t('_i.theme.more')],
['nopreference', ctx.locale().t('_i.theme.nopreference')],
['less', ctx.locale().t('_i.theme.less')]
]}
/>

Expand Down Expand Up @@ -85,9 +85,9 @@ export default function(): JSX.Element {
</Description>

<RadioGroup accessor={unitFA} block={/*@once*/false} options={/*@once*/[
['narrow', <Label icon={/*@once*/'format_letter_spacing_standard'}>{ctx.locale().t('_i.locale.narrow')}</Label>],
['short', <Label icon={/*@once*/'format_letter_spacing_wide'}>{ctx.locale().t('_i.locale.short')}</Label>],
['full', <Label icon={/*@once*/'format_letter_spacing_wider'}>{ctx.locale().t('_i.locale.long')}</Label>],
['narrow', ctx.locale().t('_i.locale.narrow')],
['short', ctx.locale().t('_i.locale.short')],
['full', ctx.locale().t('_i.locale.long')],
]}/>

<div class="ml-1 pl-2 border-l-2 border-palette-bg-low">
Expand Down
14 changes: 14 additions & 0 deletions admin/src/pages/current/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,17 @@
@apply flex justify-center flex-col px-2 gap-2 w-full xs:w-80;
}
}

.p--profile {
.content {
@apply flex justify-between flex-col sm:flex-row box-border sm:gap-8 gap-10;
}

.form {
@apply flex flex-col sm:w-[50%] w-full;

.actions {
@apply w-full flex justify-end gap-5;
}
}
}
2 changes: 1 addition & 1 deletion admin/src/pages/roles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class roles implements Pages {

menus(): Array<MenuItem> {
return [
{ type: 'item', label: '_i.page.roles.roles', path: this.#prefix },
{ type: 'item', icon: 'groups', label: '_i.page.roles.roles', path: this.#prefix },
];
}
}
14 changes: 7 additions & 7 deletions admin/src/pages/system/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
//
// SPDX-License-Identifier: MIT

import { MenuItem, Route } from '@/app/options/route';
import { Pages } from '@/pages/pages';
import { default as APIs } from './apis';
import { default as Services } from './services';
import { default as Info } from './info';
import { Pages } from '@/pages/pages';
import { MenuItem, Route } from '@/app/options/route';
import { default as Services } from './services';

export class system implements Pages {
static APIs = APIs;
Expand Down Expand Up @@ -34,9 +34,9 @@ export class system implements Pages {

menus(): Array<MenuItem> {
return [
{ type: 'item', label: '_i.page.system.apis', path: this.#prefix+'/apis' },
{ type: 'item', label: '_i.page.system.services', path: this.#prefix+'/services' },
{ type: 'item', label: '_i.page.system.info', path: this.#prefix+'/info' },
{ type: 'item', icon: 'api', label: '_i.page.system.apis', path: this.#prefix+'/apis' },
{ type: 'item', icon: 'settings_slow_motion', label: '_i.page.system.services', path: this.#prefix+'/services' },
{ type: 'item', icon: 'help', label: '_i.page.system.info', path: this.#prefix+'/info' },
];
}
}
}
Loading

0 comments on commit 053bb44

Please sign in to comment.