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 = () => {
|
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 { MenuProps } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActionContextProvider,
|
ActionContextProvider,
|
||||||
@ -123,6 +124,7 @@ const schema: ISchema = {
|
|||||||
export const useEditProfile = () => {
|
export const useEditProfile = () => {
|
||||||
const ctx = useContext(DropdownVisibleContext);
|
const ctx = useContext(DropdownVisibleContext);
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useMemo<MenuProps['items'][0]>(() => {
|
return useMemo<MenuProps['items'][0]>(() => {
|
||||||
@ -130,22 +132,9 @@ export const useEditProfile = () => {
|
|||||||
key: 'profile',
|
key: 'profile',
|
||||||
eventKey: 'EditProfile',
|
eventKey: 'EditProfile',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setVisible(true);
|
navigate('/admin/profilers/user-profile');
|
||||||
ctx?.setVisible(false);
|
|
||||||
},
|
},
|
||||||
label: (
|
label: <div>{t('Edit profile')}</div>,
|
||||||
<div>
|
|
||||||
{t('Edit profile')}
|
|
||||||
<ActionContextProvider value={{ visible, setVisible }}>
|
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
|
||||||
<SchemaComponent
|
|
||||||
scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }}
|
|
||||||
schema={schema}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ActionContextProvider>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}, [visible]);
|
}, [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 ACLPlugin from '@tachybase/plugin-acl/client';
|
||||||
|
|
||||||
import { RoleUsersManager } from './RoleUsersManager';
|
import { RoleUsersManager } from './RoleUsersManager';
|
||||||
|
import { UserProfile } from './UserProfile';
|
||||||
import { UsersManagement } from './UsersManagement';
|
import { UsersManagement } from './UsersManagement';
|
||||||
|
|
||||||
class PluginUsersClient extends Plugin {
|
class PluginUsersClient extends Plugin {
|
||||||
@ -17,6 +18,12 @@ class PluginUsersClient extends Plugin {
|
|||||||
aclSnippet: 'pm.users',
|
aclSnippet: 'pm.users',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.userSettingsManager.add('user-profile', {
|
||||||
|
icon: 'UserOutlined',
|
||||||
|
title: tval('Edit profile'),
|
||||||
|
Component: UserProfile,
|
||||||
|
});
|
||||||
|
|
||||||
const acl = this.app.pm.get(ACLPlugin);
|
const acl = this.app.pm.get(ACLPlugin);
|
||||||
acl.rolesManager.add('users', {
|
acl.rolesManager.add('users', {
|
||||||
title: tval('Users'),
|
title: tval('Users'),
|
||||||
|
Loading…
Reference in New Issue
Block a user