feat: 用户设置页面 (#1540)
Co-authored-by: sealday <sealday@gmail.com> Reviewed-on: daoyoucloud/tachybase#1540
This commit is contained in:
parent
bf2cae92b0
commit
26c53003e7
@ -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>
|
||||
);
|
||||
};
|
||||
|
81
packages/core/client/src/built-in/user-settings/style.ts
Normal file
81
packages/core/client/src/built-in/user-settings/style.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
@ -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]);
|
||||
};
|
||||
|
@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export const UserProfile = () => {
|
||||
return <div>user profile</div>;
|
||||
};
|
@ -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'),
|
||||
|
Loading…
Reference in New Issue
Block a user