feat: 用户设置页面 (#1540)

Co-authored-by: sealday <sealday@gmail.com>
Reviewed-on: daoyoucloud/tachybase#1540
This commit is contained in:
sealday 2024-09-15 12:15:05 +08:00
parent bf2cae92b0
commit 26c53003e7
5 changed files with 263 additions and 18 deletions

View File

@ -1,7 +1,170 @@
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import { Outlet } from 'react-router';
import { Layout, Menu, Result } from 'antd';
import { Navigate, Outlet, useLocation, useNavigate, useParams } from 'react-router';
import { PluginSettingsPageType, useApp } from '../../application';
import { USER_SETTINGS_PATH, UserSettingsPageType } from '../../application/UserSettingsManager';
import { PageHeader, useCompile } from '../../schema-component';
import { useStyles } from './style';
function getMenuItems(list: PluginSettingsPageType[]) {
return list.map((item) => {
return {
key: item.name,
label: item.label,
title: item.title,
icon: item.icon,
children: item.children?.length ? getMenuItems(item.children) : undefined,
};
});
}
function matchRoute(data, url) {
const keys = Object.keys(data);
if (data[url]) {
return data[url];
}
for (const pattern of keys) {
const regexPattern = pattern.replace(/:[^/]+/g, '([^/]+)');
const match = url.match(new RegExp(`^${regexPattern}$`));
if (match) {
return data[pattern];
}
}
return null;
}
function replaceRouteParams(urlTemplate, params) {
// 使用正则表达式替换占位符
return urlTemplate.replace(/:\w+/g, (match) => {
const paramName = match.substring(1);
return params?.[paramName] || match;
});
}
export const UserSettingsLayout = () => {
return <Outlet />;
const { styles, theme } = useStyles();
const app = useApp();
const navigate = useNavigate();
const location = useLocation();
const params = useParams();
const compile = useCompile();
const settings = useMemo(() => {
const list = app.userSettingsManager.getList();
// compile title
function traverse(settings: UserSettingsPageType[]) {
settings.forEach((item) => {
item.title = compile(item.title);
item.label = compile(item.title);
if (item.children?.length) {
traverse(item.children);
}
});
}
traverse(list);
return list;
}, [app.userSettingsManager, compile]);
const getFirstDeepChildPath = useCallback((settings: UserSettingsPageType[]) => {
if (!settings || !settings.length) {
return '/admin';
}
const first = settings[0];
if (first.children?.length) {
return getFirstDeepChildPath(first.children);
}
return first.path;
}, []);
const settingsMapByPath = useMemo<Record<string, UserSettingsPageType>>(() => {
const map = {};
const traverse = (settings: UserSettingsPageType[]) => {
settings.forEach((item) => {
map[item.path] = item;
if (item.children?.length) {
traverse(item.children);
}
});
};
traverse(settings);
return map;
}, [settings]);
const currentSetting = useMemo(
() => matchRoute(settingsMapByPath, location.pathname),
[location.pathname, settingsMapByPath],
);
const currentTopLevelSetting = useMemo(() => {
if (!currentSetting) {
return null;
}
return settings.find((item) => item.name === currentSetting.topLevelName);
}, [currentSetting, settings]);
const sidebarMenus = useMemo(() => {
return getMenuItems(settings.filter((v) => v.isTopLevel !== false).map((item) => ({ ...item, children: null })));
}, [settings]);
if (!currentSetting || location.pathname === USER_SETTINGS_PATH || location.pathname === USER_SETTINGS_PATH + '/') {
return <Navigate replace to={getFirstDeepChildPath(settings)} />;
}
if (location.pathname === currentTopLevelSetting.path && currentTopLevelSetting.children?.length > 0) {
return <Navigate replace to={getFirstDeepChildPath(currentTopLevelSetting.children)} />;
}
return (
<div>
<Layout>
<Layout.Sider theme={'light'}>
<Menu
selectedKeys={[currentSetting?.pluginKey || currentSetting.topLevelName]}
style={{ height: 'calc(100vh - 46px)', overflowY: 'auto', overflowX: 'hidden' }}
onClick={({ key }) => {
const plugin = settings.find((item) => item.name === key);
if (plugin.children?.length) {
return navigate(getFirstDeepChildPath(plugin.children));
} else {
return navigate(plugin.path);
}
}}
items={sidebarMenus}
/>
</Layout.Sider>
<Layout.Content>
{currentSetting && (
<PageHeader
className={styles.pageHeader}
style={{
paddingBottom:
currentTopLevelSetting.children?.length > 0 && currentTopLevelSetting.showTabs !== false
? 0
: theme.paddingSM,
}}
ghost={false}
title={currentTopLevelSetting.title}
footer={
currentTopLevelSetting.children?.length > 0 &&
currentTopLevelSetting.showTabs !== false && (
<Menu
style={{ marginLeft: -theme.margin }}
onClick={({ key }) => {
navigate(replaceRouteParams(app.pluginSettingsManager.getRoutePath(key), params));
}}
selectedKeys={[currentSetting?.name]}
mode="horizontal"
items={getMenuItems(currentTopLevelSetting.children)}
></Menu>
)
}
/>
)}
<div className={styles.pageContent}>
{currentSetting ? (
<Outlet />
) : (
<Result status="404" title="404" subTitle="Sorry, the page you visited does not exist." />
)}
</div>
</Layout.Content>
</Layout>
</div>
);
};

View File

@ -0,0 +1,81 @@
import { createStyles } from 'antd-style';
export const useStyles = createStyles(({ token, css }) => {
return {
cardActionDisabled: {
color: token.colorTextDisabled,
cursor: 'not-allowed',
},
pageHeader: {
backgroundColor: token.colorBgContainer,
paddingTop: token.paddingSM,
paddingBottom: 0,
paddingInline: token.paddingLG,
'.ant-page-header-footer': { marginBlockStart: '0' },
'& .ant-tabs-nav': {
marginBottom: 0,
},
'.ant-page-header-heading-title': {
color: token.colorText,
},
},
pageContent: {
margin: token.marginLG,
},
// pageContent: {
// marginTop: token.margin,
// marginBottom: token.marginLG,
// background: 'transparent',
// minHeight: '80vh',
// },
PluginDetailBaseInfo: {
display: 'flex',
flexDirection: 'column',
marginBottom: token.margin,
},
PluginDocument: {
background: token.colorBgContainer,
padding: token.paddingLG,
height: '60vh',
overflowY: 'auto',
},
avatar: {
'.ant-card-meta-avatar': {
marginTop: '8px',
'.ant-avatar': { borderRadius: '2px' },
},
},
version: {
display: 'block',
color: token.colorTextDescription,
fontWeight: 'normal',
fontSize: token.fontSize,
},
card: css`
.ant-card-actions {
li .ant-space {
gap: 2px !important;
}
li a {
.anticon {
margin-right: 3px;
/* display: none; */
}
}
li:last-child {
width: 20% !important;
}
li:first-child {
width: 80% !important;
border-inline-end: 0;
text-align: left;
padding-left: 16px;
}
}
`,
};
});

View File

@ -3,6 +3,7 @@ import { ISchema, uid, useForm } from '@tachybase/schema';
import { MenuProps } from 'antd';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
import {
ActionContextProvider,
@ -123,6 +124,7 @@ const schema: ISchema = {
export const useEditProfile = () => {
const ctx = useContext(DropdownVisibleContext);
const [visible, setVisible] = useState(false);
const navigate = useNavigate();
const { t } = useTranslation();
return useMemo<MenuProps['items'][0]>(() => {
@ -130,22 +132,9 @@ export const useEditProfile = () => {
key: 'profile',
eventKey: 'EditProfile',
onClick: () => {
setVisible(true);
ctx?.setVisible(false);
navigate('/admin/profilers/user-profile');
},
label: (
<div>
{t('Edit profile')}
<ActionContextProvider value={{ visible, setVisible }}>
<div onClick={(e) => e.stopPropagation()}>
<SchemaComponent
scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }}
schema={schema}
/>
</div>
</ActionContextProvider>
</div>
),
label: <div>{t('Edit profile')}</div>,
};
}, [visible]);
};

View File

@ -0,0 +1,5 @@
import React from 'react';
export const UserProfile = () => {
return <div>user profile</div>;
};

View File

@ -2,6 +2,7 @@ import { Plugin, tval } from '@tachybase/client';
import ACLPlugin from '@tachybase/plugin-acl/client';
import { RoleUsersManager } from './RoleUsersManager';
import { UserProfile } from './UserProfile';
import { UsersManagement } from './UsersManagement';
class PluginUsersClient extends Plugin {
@ -17,6 +18,12 @@ class PluginUsersClient extends Plugin {
aclSnippet: 'pm.users',
});
this.userSettingsManager.add('user-profile', {
icon: 'UserOutlined',
title: tval('Edit profile'),
Component: UserProfile,
});
const acl = this.app.pm.get(ACLPlugin);
acl.rolesManager.add('users', {
title: tval('Users'),