fix: 更新模板

This commit is contained in:
jackycheng
2024-12-24 14:25:06 +08:00
parent 38cfa091c6
commit de8f3b3921
77 changed files with 703 additions and 1492 deletions

View File

@@ -1,44 +0,0 @@
import Layout from '@/layout';
import withPage from '@/utils/withPage';
import { lazy } from 'react';
import {
createBrowserRouter,
RouteObject,
RouterProvider,
} from 'react-router-dom';
import { metaInfo } from './meta.js';
// routes
const parseMeta2Routes = (meta: Meta[]) => {
const routes: RouteObject[] = [];
meta.forEach((item) => {
if (item._children?.length) {
// 是分组文件夹
routes.push(...parseMeta2Routes(item._children));
return;
}
const Element = lazy(() => import(`@/pages/${item._fullpath}`));
const PageElement = withPage(Element);
const route: RouteObject = {
path: item._fullroute,
element: <PageElement />,
};
routes.push(route);
});
return routes;
};
const routes = parseMeta2Routes(metaInfo);
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: routes,
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;

View File

@@ -1,7 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons';
const Icon = createFromIconfontCN({
scriptUrl: {{iconFontSrc}},
scriptUrl: '{{iconFontSrc}}',
});
export default Icon;

View File

@@ -1,3 +1,5 @@
import React from 'react';
const Root: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => {
return <div {...props} />;
};

View File

@@ -1,33 +1,18 @@
import { Locale, Theme } from '@/enum';
import { MenuItem } from '@/utils/menu';
import storageService from '@/utils/storage';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { Locale, Theme } from './enum';
import storageService from './storage';
import { RootState } from './store';
type MenuKey = {
headKey: string;
sideKey?: string;
};
export interface CommonState {
locale: Locale;
theme: Theme;
primaryColor: string;
currentMenu: MenuKey;
headMenus: MenuItem[];
sideMenus: MenuItem[];
}
/* DO NOT EDIT initialState */
const initialState: CommonState = {
locale: storageService.getItem('locale', Locale.zh_CN),
theme: storageService.getItem('theme', Theme.DEFAULT),
primaryColor: storageService.getItem('primaryColor', '#1361B3'),
currentMenu: {
headKey: '',
sideKey: '',
},
headMenus: [],
sideMenus: [],
};
export const commonSlice = createSlice({
@@ -46,27 +31,10 @@ export const commonSlice = createSlice({
state.primaryColor = action.payload;
storageService.setItem('primaryColor', action.payload);
},
setCurrentMenu: (state, action: PayloadAction<MenuKey>) => {
state.currentMenu = action.payload;
// 设置侧边菜单内容
const { headKey } = action.payload;
state.sideMenus =
state.headMenus.find((menu) => menu.key === headKey)?.children ?? [];
},
setHeadMenus: (state, action: PayloadAction<MenuItem[]>) => {
state.headMenus = action.payload ?? [];
},
},
});
export const {
setLocale,
setTheme,
setPrimaryColor,
setCurrentMenu,
setHeadMenus,
} = commonSlice.actions;
export const { setLocale, setTheme, setPrimaryColor } = commonSlice.actions;
export const selectCommon = (state: RootState) => state.common;

View File

@@ -1,6 +1,5 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, RootState } from './store';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@@ -0,0 +1,81 @@
import { customValueTypeMap } from '@/components';
import { ConfigProvider, App as TocoApp, theme } from '@df/toco-ui';
import { Router } from '@remix-run/router';
import enUS from 'antd/locale/en_US';
import zhCN from 'antd/locale/zh_CN';
import { useEffect, useMemo } from 'react';
import { IntlProvider } from 'react-intl';
import { Provider as StoreProvider } from 'react-redux';
import { RouterProvider } from 'react-router-dom';
import { Locale, Theme } from './enum';
import { useAppSelector } from './hooks';
import * as langs from './langs';
import globalMessage from './message';
import { store } from './store';
const { darkAlgorithm } = theme;
const AppInternal: React.FC<{ router: Router }> = (props) => {
const { router } = props;
const { message } = TocoApp.useApp();
const locale = useAppSelector((state) => state.common.locale);
const theme = useAppSelector((state) => state.common.theme);
const primaryColor = useAppSelector((state) => state.common.primaryColor);
const antdLocaleMap = {
[Locale.zh_CN]: zhCN,
[Locale.en_US]: enUS,
};
const langsMap = {
[Locale.zh_CN]: langs.zh_CN,
[Locale.en_US]: langs.en_US,
};
const themeConfig = useMemo(() => {
return {
algorithm: theme === Theme.DARK ? [darkAlgorithm] : [],
cssVar: true,
token: primaryColor
? {
colorPrimary: primaryColor,
}
: undefined,
};
}, [primaryColor, theme]);
useEffect(() => {
globalMessage.setMessage(message);
}, [message]);
useEffect(() => {
document.documentElement.setAttribute(
'data-color-scheme',
theme === Theme.DARK ? 'dark' : 'light',
);
}, [theme]);
return (
<IntlProvider locale={locale} messages={langsMap[locale]}>
<ConfigProvider
locale={antdLocaleMap[locale]}
theme={themeConfig}
valueTypeMap={customValueTypeMap}
>
<RouterProvider router={router} />
</ConfigProvider>
</IntlProvider>
);
};
const App: React.FC<{ router: Router }> = (props) => {
return (
<StoreProvider store={store}>
<TocoApp style={{ height: '100%' }}>
<AppInternal {...props} />
</TocoApp>
</StoreProvider>
);
};
export default App;

View File

@@ -8,9 +8,6 @@ const en_US: Language = {
system_settings: 'System Settings',
theme_light: 'Light',
theme_dark: 'Dark',
demo: 'Demo',
demo2: 'Demo2',
test: 'Test',
};
export default en_US;

View File

@@ -9,9 +9,6 @@ export type Language = {
system_settings: string;
theme_light: string;
theme_dark: string;
demo: string;
demo2: string;
test: string;
};
export { en_US, zh_CN };

View File

@@ -8,9 +8,6 @@ const zh_CN: Language = {
system_settings: '系统设置',
theme_light: '浅色模式',
theme_dark: '深色模式',
demo: '示例',
demo2: '示例2',
test: '测试',
};
export default zh_CN;

View File

@@ -1,8 +1,8 @@
import message from '@/components/message';
import axios, { AxiosError } from 'axios';
import { AxiosResponse } from 'axios/index';
import message from './message';
export type ResponseType<T = any> = {
export type ResponseType<T = unknown> = {
code: number;
message: string;
data: T | null;

View File

@@ -1,7 +1,7 @@
import { STORAGE_KEY_PREFIX } from '@/config';
const STORAGE_KEY_PREFIX = 'toco_storage_';
class StorageService {
setItem(key: string, value: any): void {
setItem<T>(key: string, value: T): void {
try {
const serializedValue = JSON.stringify(value);
localStorage.setItem(STORAGE_KEY_PREFIX + key, serializedValue);

View File

@@ -1,11 +1,9 @@
import demoReducer from '@/pages/demo/slice';
import { Action, configureStore, ThunkAction } from '@reduxjs/toolkit';
import commonReducer from './common';
export const store = configureStore({
reducer: {
common: commonReducer,
demo: demoReducer,
},
});

View File

@@ -1,102 +0,0 @@
import { RootState } from '@/app/store';
import { customValueTypeMap } from '@/components';
import globalMessage from '@/components/message';
import { Locale, Theme } from '@/enum';
import * as langs from '@/langs';
import * as themes from '@/themes';
import { CSSVariablesStyle } from '@/themes/CSSVariablesStyle';
import { ConfigProvider } from '@df/toco-ui';
import { Store } from '@reduxjs/toolkit';
import { message as tocoMessage } from 'antd';
import enUS from 'antd/locale/en_US';
import zhCN from 'antd/locale/zh_CN';
import React, { useEffect, useMemo } from 'react';
import { HelmetProvider } from 'react-helmet-async';
import { IntlProvider } from 'react-intl';
import { Provider, useSelector } from 'react-redux';
const helmetContext = {};
type AppWrapperProps = {
children: React.ReactNode;
store: Store;
};
const AppWrapper = (props: AppWrapperProps) => {
const { children, store } = props;
return (
<Provider store={store}>
<Insider>{children}</Insider>
</Provider>
);
};
type InsiderProps = {
children: React.ReactNode;
};
const Insider = (props: InsiderProps) => {
const { children } = props;
const [message, messageContextHolder] = tocoMessage.useMessage();
const locale = useSelector((state: RootState) => state.common.locale);
const theme = useSelector((state: RootState) => state.common.theme);
const primaryColor = useSelector(
(state: RootState) => state.common.primaryColor,
);
const antdLocaleMap = {
[Locale.zh_CN]: zhCN,
[Locale.en_US]: enUS,
};
const langsMap = {
[Locale.zh_CN]: langs.zh_CN,
[Locale.en_US]: langs.en_US,
};
const themeConfig = useMemo(() => {
const themeMap = {
[Theme.DEFAULT]: themes.defaultTheme,
[Theme.DARK]: themes.darkTheme,
};
const config = themeMap[theme];
if (primaryColor) {
return {
...config,
token: {
...(config.token ?? {}),
colorPrimary: primaryColor,
},
};
}
return config;
}, [primaryColor, theme]);
useEffect(() => {
globalMessage.setMessage(message);
}, [message]);
useEffect(() => {
document.documentElement.setAttribute(
'data-color-scheme',
theme === Theme.DARK ? 'dark' : 'light',
);
}, [theme]);
return (
<HelmetProvider context={helmetContext}>
<IntlProvider locale={locale} messages={langsMap[locale]}>
<ConfigProvider
locale={antdLocaleMap[locale]}
theme={themeConfig}
valueTypeMap={customValueTypeMap}
>
<CSSVariablesStyle />
{messageContextHolder}
{children}
</ConfigProvider>
</IntlProvider>
</HelmetProvider>
);
};
export default AppWrapper;

View File

@@ -1,78 +0,0 @@
import { Input, RenderFieldPropsType } from '@df/toco-ui';
import { useState } from 'react';
type NameType = {
first?: string;
last?: string;
title?: string;
};
type AddressProps = {
defaultValue?: NameType;
value?: NameType;
onChange?: (value: NameType) => void;
};
const Fullname = (props: AddressProps) => {
const { value, onChange } = props;
const [currentValue, setCurrentValue] = useState<NameType | undefined>(value);
return (
<div>
<div>
<span>first</span>
<Input
defaultValue={value?.first}
onChange={(e) =>
onChange?.({
...currentValue,
first: e.target.value,
})
}
/>
</div>
<div>
<span>last</span>
<Input
defaultValue={value?.last}
onChange={(e) =>
onChange?.({
...currentValue,
last: e.target.value,
})
}
/>
</div>
<div>
<span>title</span>
<Input
defaultValue={value?.title}
onChange={(e) =>
onChange?.({
...currentValue,
title: e.target.value,
})
}
/>
</div>
</div>
);
};
export const valueTypeOptions: {
name: string;
renderFieldProps: RenderFieldPropsType;
} = {
name: 'fullname',
renderFieldProps: {
render: (value, props) => {
return (
<span>
{value?.title} {value?.first} {value?.last}
</span>
);
},
renderFormItem: (_, props) => <Fullname {...props.fieldProps} />,
},
};
export default Fullname;

View File

@@ -1,6 +1 @@
import { RenderFieldPropsType } from '@df/toco-ui';
import { valueTypeOptions as fullnameValueTypeOptions } from './fullname';
export const customValueTypeMap: Record<string, RenderFieldPropsType> = {
[fullnameValueTypeOptions.name]: fullnameValueTypeOptions.renderFieldProps,
};
export {};

View File

@@ -1,3 +0,0 @@
export const CSS_VARIABLE_PREFIX = 'toco';
export const STORAGE_KEY_PREFIX = 'toco_storage_';

View File

@@ -1,31 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-var */
declare global {
let tocoRefs: Record<string, any>;
let tocoModals: Record<string, any>;
var tocoRefs: Record<string, any>;
var tocoModals: Record<string, any>;
}
interface Meta {
displayName?: string; // 展示在菜单上的名字, 可以用 {{language_id}} 包起来,用于国际化显示
icon?: string; // 展示在菜单上的图标
headMenu?: // 是否展示在头部菜单 或 所属头部菜单的名字
| boolean
| {
key: string;
label: string; // 可以用 {{language_id}} 包起来,用于国际化显示
icon?: string;
order?: number; // 菜单序号,数字越小越靠前
};
sideMenu?: // 是否展示在侧边菜单 或 所属侧边菜单的名字
| boolean
| {
key: string;
label: string; // 可以用 {{language_id}} 包起来,用于国际化显示
icon?: string;
order?: number; // 菜单序号,数字越小越靠前
};
order?: number; // 菜单序号,数字越小越靠前
route?: string; // 当前页面路由名字,支持 ':id' 等路由参数,不同则默认等于目录名
_fullroute?: string; // 完整路由,最终路由
_path: string; // 目录名
_fullpath?: string; // 完整目录结构
_children: Meta[];
}
export {};

View File

@@ -1,20 +1,17 @@
import { store } from '@/app/store';
import AppWrapper from '@/components/app-wrapper';
import '@/style/index.css';
import { tocoGlobal } from '@df/toco-ui';
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import App from './app';
import reportWebVitals from './reportWebVitals';
import { router } from './router';
const container = document.getElementById('root')!;
const root = createRoot(container);
root.render(
<React.StrictMode>
<AppWrapper store={store}>
<App />
</AppWrapper>
<App router={router} />
</React.StrictMode>,
);
@@ -24,7 +21,5 @@ root.render(
reportWebVitals();
// global variables
(globalThis as any).tocoRefs = tocoGlobal.getRefs();
(globalThis as any).tocoModals = tocoGlobal.getModals();
export { store };
globalThis.tocoRefs = tocoGlobal.getRefs();
globalThis.tocoModals = tocoGlobal.getModals();

View File

@@ -1,50 +0,0 @@
.header {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--toco-colorBgContainer);
border-bottom: 1px solid var(--toco-colorBorder);
}
.left {
display: flex;
align-items: center;
flex: 1;
}
.settings {
cursor: pointer;
font-size: 14px;
color: var(--toco-colorTextBase);
}
.logo {
width: 125px;
padding-right: 10px;
}
.menu {
flex: 1;
min-width: 0;
border-bottom: transparent;
}
.content {
padding: 24px;
height: calc(100vh - 112px);
overflow-y: auto;
}
.breadcrumb {
margin: 16px 0;
}
.main {
padding: 24px;
background: var(--toco-colorBgContainer);
border-radius: var(--toco-borderRadiusLG);
}
.footer {
text-align: center;
}

View File

@@ -1,61 +0,0 @@
import { selectCommon } from '@/app/common';
import { useAppSelector } from '@/app/hooks';
import Settings from '@/components/settings';
import { Theme } from '@/enum';
import { LayoutProps } from '@/layout';
import { SettingOutlined } from '@ant-design/icons';
import { Image, Layout, Menu, Popover } from '@df/toco-ui';
import { useIntl } from 'react-intl';
import styles from './index.module.css';
const { Header, Content, Footer } = Layout;
const HeaderAndFooterLayout = (props: LayoutProps) => {
const { children, onHeadMenuSelect } = props;
const { headMenus, currentMenu, theme } = useAppSelector(selectCommon);
const { headKey, sideKey = '' } = currentMenu;
const intl = useIntl();
return (
<Layout>
<Header className={styles.header}>
<div className={styles.left}>
<div className={styles.logo}>
<Image
preview={false}
src={
process.env.PUBLIC_URL +
(theme === Theme.DEFAULT ? '/logo.png' : '/logo_dark.png')
}
/>
</div>
<Menu
className={styles.menu}
mode="horizontal"
items={headMenus}
selectedKeys={[headKey, sideKey]}
onSelect={onHeadMenuSelect}
/>
</div>
<div className={styles.right}>
<Popover
title={intl.formatMessage({ id: 'system_settings' })}
content={<Settings />}
trigger="hover"
>
<SettingOutlined className={styles.settings} />
</Popover>
</div>
</Header>
<Content className={styles.content}>
<div className={styles.main}>{children}</div>
</Content>
<Footer className={styles.footer}>
TOCO Design ©{new Date().getFullYear()}
</Footer>
</Layout>
);
};
export default HeaderAndFooterLayout;

View File

@@ -1,49 +0,0 @@
.header {
display: flex;
align-items: center;
background: var(--toco-colorBgContainer);
border-bottom: 1px solid var(--toco-colorBorder);
}
.left {
display: flex;
align-items: center;
flex: 1;
}
.logo {
width: 125px;
margin-right: 32px;
}
.menu {
flex: 1;
min-width: 0;
border-bottom: none;
}
.sider {
width: 200px;
background: var(--toco-colorBgContainer);
}
.siderMenu {
height: 100%;
border-right: 0;
}
.contentWrapper {
padding: 24px;
}
.content {
padding: 24px;
margin: 0;
min-height: 280px;
background: var(--toco-colorBgContainer);
border-radius: var(--toco-borderRadiusLG);
}
.breadcrumb {
margin: 16px 0;
}

View File

@@ -1,79 +0,0 @@
import { selectCommon } from '@/app/common';
import { useAppSelector } from '@/app/hooks';
import Settings from '@/components/settings';
import { Theme } from '@/enum';
import { LayoutProps } from '@/layout';
import { SettingOutlined } from '@ant-design/icons';
import { Image, Layout, Menu, Popover } from '@df/toco-ui';
import { useMemo } from 'react';
import { useIntl } from 'react-intl';
import styles from './index.module.css';
const { Header, Content, Sider } = Layout;
const HeaderAndFooterLayout = (props: LayoutProps) => {
const { children, onHeadMenuSelect, onSideMenuSelect } = props;
const { headMenus, sideMenus, currentMenu, theme } =
useAppSelector(selectCommon);
const { headKey, sideKey = '' } = currentMenu;
const intl = useIntl();
const flatHeadMenus = useMemo(
() =>
headMenus.map((m) => {
return { ...m, children: undefined };
}),
[headMenus],
);
return (
<Layout>
<Header className={styles.header}>
<div className={styles.left}>
<div className={styles.logo}>
<Image
preview={false}
src={
process.env.PUBLIC_URL +
(theme === Theme.DEFAULT ? '/logo.png' : '/logo_dark.png')
}
/>
</div>
<Menu
className={styles.menu}
mode="horizontal"
items={flatHeadMenus}
selectedKeys={[headKey]}
onSelect={onHeadMenuSelect}
/>
</div>
<div className={styles.right}>
<Popover
title={intl.formatMessage({ id: 'system_settings' })}
content={<Settings />}
trigger="hover"
>
<SettingOutlined className={styles.settings} />
</Popover>
</div>
</Header>
<Layout>
{sideMenus.length === 0 ? null : (
<Sider className={styles.sider}>
<Menu
className={styles.siderMenu}
mode="inline"
items={sideMenus}
selectedKeys={[sideKey]}
onSelect={onSideMenuSelect}
/>
</Sider>
)}
<Layout className={styles.contentWrapper}>
<Content className={styles.content}>{children}</Content>
</Layout>
</Layout>
</Layout>
);
};
export default HeaderAndFooterLayout;

View File

@@ -0,0 +1,70 @@
.layout {
height: 100%;
--header-height: 63px;
--extra-height: 63px;
.logoContainer{
height: 100%;
background: var(--ant-color-bg-container);
}
.logo {
&:global(.dark) {
background: var(--ant-layout-sider-bg);
}
height: var(--header-height);
padding: 16px;
.icon {
display: flex;
}
}
.header {
display: flex;
padding: 0;
background: var(--ant-color-bg-container);
border-bottom: 1px solid var(--ant-color-border);
.headerMenu {
flex: 1;
min-width: 0;
border-bottom: none;
}
}
.sider {
border-right: 1px solid var(--ant-color-border);
:global(.ant-layout-sider-children) {
display: flex;
flex-direction: column;
.siderContainer {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.siderMenu {
border-right: none;
}
}
}
.content {
padding: 24px;
background: var(--ant-color-bg-container);
}
.extra {
&:global(.dark) {
background: var(--ant-layout-sider-bg);
}
height: var(--extra-height);
display: flex;
align-items: center;
justify-content: center;
}
}

View File

@@ -1,96 +1,74 @@
import { setCurrentMenu, setHeadMenus } from '@/app/common';
import { useAppDispatch } from '@/app/hooks';
import SuspenseLayout from '@/components/suspense-layout';
import {
findFirstSubMenu,
findHeadMenuByHeadKey,
findHeadMenuBySideKey,
menuItems,
} from '@/utils/menu';
import { MenuProps } from '@df/toco-ui';
import { useCallback, useEffect } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import Layout from './HeaderAndSider';
// import Layout from './HeaderAndFooter';
import { Theme } from '@/app/enum';
import { useAppSelector } from '@/app/hooks';
import { SettingOutlined } from '@ant-design/icons';
import { Button, Image, Popover } from '@df/toco-ui';
import classNames from 'classnames';
import { useMemo } from 'react';
import styles from './index.module.css';
import { HeaderOnly, HeaderSider, SiderOnly } from './layouts';
import Settings from './settings';
import SuspenseLayout from './suspense-layout';
export type LayoutProps = {
children?: React.ReactNode;
onHeadMenuSelect?: MenuProps['onSelect'];
onSideMenuSelect?: MenuProps['onSelect'];
export enum LayoutType {
Default = 0,
Header = 1 << 1,
Sider = 1 << 2,
}
type LayoutProps = {
type?: LayoutType;
};
const LayoutRoot = () => {
const dispatch = useAppDispatch();
const location = useLocation();
const navigate = useNavigate();
const Layout: React.FC<LayoutProps> = (props) => {
const { type: typeProp } = props;
const theme = useAppSelector((state) => state.common.theme);
// 设置head菜单
useEffect(() => {
dispatch(setHeadMenus(menuItems));
}, [dispatch]);
// 监听路由变化
useEffect(() => {
const key = location.pathname.replace(/^\//, '').replace(/\/$/, '');
const headMenu = findHeadMenuByHeadKey(key);
if (headMenu) {
// 一级页面
dispatch(
setCurrentMenu({
headKey: headMenu.key,
}),
);
} else {
// 非一级页面
const headMenu = findHeadMenuBySideKey(key);
dispatch(
setCurrentMenu({
headKey: headMenu?.key ?? '',
sideKey: key,
}),
);
const Component = useMemo(() => {
const type = typeProp ?? LayoutType.Default;
const hasHeader = type & LayoutType.Header;
const hasSider = type & LayoutType.Sider;
if (hasHeader && hasSider) {
return HeaderSider;
} else if (hasHeader) {
return HeaderOnly;
} else if (hasSider) {
return SiderOnly;
}
}, [dispatch, location]);
const handleHeadMenuSelect: MenuProps['onSelect'] = useCallback(
({ item, key, keyPath, domEvent }) => {
const headMenu = findHeadMenuByHeadKey(key);
if (!headMenu) {
// 顶部嵌套的菜单即实际点击的sideMenu
navigate(key);
return;
}
if (!headMenu.children?.length) {
// 一级页面
navigate(key);
} else {
// 非一级页面点击一级菜单,主动跳转第一个子页面
const subMenu = findFirstSubMenu(headMenu);
if (subMenu) {
navigate(subMenu.key);
}
}
},
[navigate],
);
const handleSideMenuSelect: MenuProps['onSelect'] = useCallback(
({ item, key, keyPath, domEvent }) => {
navigate(key);
},
[navigate],
);
return HeaderSider;
}, [typeProp]);
return (
<Layout
onHeadMenuSelect={handleHeadMenuSelect}
onSideMenuSelect={handleSideMenuSelect}
>
<SuspenseLayout>
<Outlet />
</SuspenseLayout>
</Layout>
<SuspenseLayout>
<Component
theme={theme}
siderWidth={250}
logo={
<div
className={classNames(styles.logo, { dark: theme === Theme.DARK })}
>
<Image
className={styles.icon}
preview={false}
src={
process.env.PUBLIC_URL +
(theme === Theme.DEFAULT ? '/logo.png' : '/logo_dark.png')
}
/>
</div>
}
extra={
<div
className={classNames(styles.extra, { dark: theme === Theme.DARK })}
>
<Popover content={<Settings />}>
<Button type="text">
<SettingOutlined />
</Button>
</Popover>
</div>
}
/>
</SuspenseLayout>
);
};
export default LayoutRoot;
export default Layout;

View File

@@ -0,0 +1,65 @@
import { Theme } from '@/app/enum';
import { routerConfig } from '@/router';
import { Layout, Menu, MenuProps } from '@df/toco-ui';
import { SelectInfo } from 'rc-menu/lib/interface';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Outlet, useMatches, useNavigate } from 'react-router-dom';
import { LayoutProps } from '.';
import styles from '../index.module.css';
import { parseMenuItems, parseRouteMatchs } from './utils';
const { Header, Content } = Layout;
const HeaderOnly: React.FC<LayoutProps> = (props) => {
const { theme: themeProp, logo, extra, siderWidth } = props;
const matches = useMatches();
const navigate = useNavigate();
const [selectedKeys, setSelectedKeys] = useState<string[]>();
useEffect(() => {
const parts = parseRouteMatchs(matches);
setSelectedKeys(parts);
}, [matches]);
const items: MenuProps['items'] = useMemo(() => {
return parseMenuItems(routerConfig?.children || [], true);
}, []);
const onKeyChange = useCallback(
(info: SelectInfo) => {
const keys = info.keyPath;
setSelectedKeys(info.keyPath);
navigate(keys.reverse().join('/'));
},
[navigate],
);
const theme = useMemo(() => {
return themeProp === Theme.DARK ? 'dark' : 'light';
}, [themeProp]);
return (
<Layout className={styles.layout}>
<Header className={styles.header}>
<div className={styles.logoContainer} style={{ width: siderWidth }}>
{logo}
</div>
<Menu
className={styles.headerMenu}
theme={theme}
mode="horizontal"
items={items}
selectedKeys={selectedKeys}
onSelect={onKeyChange}
/>
{extra}
</Header>
<Layout className={styles.content}>
<Content>
<Outlet />
</Content>
</Layout>
</Layout>
);
};
export default HeaderOnly;

View File

@@ -0,0 +1,104 @@
import { Theme } from '@/app/enum';
import { routerConfig } from '@/router';
import { Layout, Menu, MenuProps } from '@df/toco-ui';
import { SelectInfo } from 'rc-menu/lib/interface';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Outlet, useMatches, useNavigate } from 'react-router-dom';
import { LayoutProps } from '.';
import styles from '../index.module.css';
import { parseMenuItems, parseRouteMatchs } from './utils';
const { Header, Content, Sider } = Layout;
const HeaderSider: React.FC<LayoutProps> = (props) => {
const { theme: themeProp, siderWidth, logo, extra } = props;
const matches = useMatches();
const navigate = useNavigate();
const [headerKey, setHeaderKey] = useState<string>();
const [siderSelectedKeys, setSiderSelectedKeys] = useState<string[]>();
const [siderExpandedKeys, setSiderExpandedKeys] = useState<string[]>();
useEffect(() => {
const parts = parseRouteMatchs(matches);
const [headerKey, ...siderKey] = parts;
setHeaderKey(headerKey);
setSiderSelectedKeys(siderKey);
setSiderExpandedKeys((prev) => {
const set = new Set<string>(prev);
siderKey.slice(0, -1).forEach((key) => {
set.add(key);
});
return Array.from(set);
});
}, [matches]);
const headerItems: MenuProps['items'] = useMemo(() => {
return parseMenuItems(routerConfig.children, false);
}, []);
const onHeaderKeyChange = useCallback(
(info: SelectInfo) => {
setHeaderKey(info.key);
navigate(info.key);
},
[navigate],
);
const siderItems: MenuProps['items'] = useMemo(() => {
const item = routerConfig.children.find((item) => item.path === headerKey);
return parseMenuItems(item?.children || [], true);
}, [headerKey]);
const onSiderKeyChange = useCallback(
(info: SelectInfo) => {
const keys = info.keyPath;
setSiderSelectedKeys(info.keyPath);
const parts = [headerKey, ...keys];
navigate(parts.join('/'));
},
[navigate, headerKey],
);
const theme = useMemo(() => {
return themeProp === Theme.DARK ? 'dark' : 'light';
}, [themeProp]);
return (
<Layout className={styles.layout}>
<Header className={styles.header}>
<div className={styles.logoContainer} style={{ width: siderWidth }}>
{logo}
</div>
<Menu
className={styles.headerMenu}
theme={theme}
mode="horizontal"
items={headerItems}
selectedKeys={headerKey ? [headerKey] : []}
onSelect={onHeaderKeyChange}
/>
{extra}
</Header>
<Layout>
<Sider className={styles.sider} theme={theme} width={siderWidth}>
<Menu
className={styles.siderMenu}
mode="inline"
theme={theme}
items={siderItems}
selectedKeys={siderSelectedKeys}
onSelect={onSiderKeyChange}
openKeys={siderExpandedKeys}
onOpenChange={setSiderExpandedKeys}
/>
</Sider>
<Layout className={styles.content}>
<Content>
<Outlet />
</Content>
</Layout>
</Layout>
</Layout>
);
};
export default HeaderSider;

View File

@@ -0,0 +1,75 @@
import { Theme } from '@/app/enum';
import { routerConfig } from '@/router';
import { Layout, Menu, MenuProps } from '@df/toco-ui';
import { SelectInfo } from 'rc-menu/lib/interface';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Outlet, useMatches, useNavigate } from 'react-router-dom';
import { LayoutProps } from '.';
import styles from '../index.module.css';
import { parseMenuItems, parseRouteMatchs } from './utils';
const { Content, Sider } = Layout;
const SiderOnly: React.FC<LayoutProps> = (props) => {
const { theme: themeProp, logo, extra, siderWidth } = props;
const matches = useMatches();
const navigate = useNavigate();
const [selectedKeys, setSelectedKeys] = useState<string[]>();
const [expandedKeys, setExpandedKeys] = useState<string[]>();
useEffect(() => {
const parts = parseRouteMatchs(matches);
setSelectedKeys(parts);
setExpandedKeys((prev) => {
const set = new Set<string>(prev);
parts.slice(0, -1).forEach((key) => {
set.add(key);
});
return Array.from(set);
});
}, [matches]);
const items: MenuProps['items'] = useMemo(() => {
return parseMenuItems(routerConfig?.children || [], true);
}, []);
const onKeyChange = useCallback(
(info: SelectInfo) => {
const keys = info.keyPath;
setSelectedKeys(info.keyPath);
navigate(keys.reverse().join('/'));
},
[navigate],
);
const theme = useMemo(() => {
return themeProp === Theme.DARK ? 'dark' : 'light';
}, [themeProp]);
return (
<Layout className={styles.layout}>
<Sider className={styles.sider} theme={theme} width={siderWidth}>
{logo}
<div className={styles.siderContainer}>
<Menu
className={styles.siderMenu}
theme={theme}
mode="inline"
items={items}
selectedKeys={selectedKeys}
onSelect={onKeyChange}
openKeys={expandedKeys}
onOpenChange={setExpandedKeys}
/>
{extra}
</div>
</Sider>
<Layout className={styles.content}>
<Content>
<Outlet />
</Content>
</Layout>
</Layout>
);
};
export default SiderOnly;

View File

@@ -0,0 +1,13 @@
import { Theme } from '@/app/enum';
import React from 'react';
export { default as HeaderOnly } from './HeaderOnly';
export { default as HeaderSider } from './HeaderSider';
export { default as SiderOnly } from './SiderOnly';
export type LayoutProps = {
theme?: Theme;
logo?: React.ReactNode;
extra?: React.ReactNode;
siderWidth?: number;
};

View File

@@ -0,0 +1,49 @@
import { RootRoute } from '@/router';
import { MenuProps } from '@df/toco-ui';
import { parse } from 'path-to-regexp';
import { UIMatch } from 'react-router-dom';
const removeLeadingSlashes = (path: string) => {
return path.replace(/^\/+/, '');
};
export const parseMenuItems = (
routes: RootRoute['children'],
recursively: boolean,
): MenuProps['items'] => {
return routes
.filter((item) => typeof item.path === 'string')
.map((item) => {
const res = parse(item.path);
if (res.tokens.some((token) => token.type !== 'text') || !item.menu) {
// 不生成菜单项:
// 1. 路由中不全为text例如有参数 :id
// 2. 没有设置menu
return undefined;
}
const children =
recursively && item.children
? parseMenuItems(item.children, true)
: undefined;
return {
key: removeLeadingSlashes(item.path),
label: item.menu.title,
children: children && children.length > 0 ? children : 0,
};
})
.filter((p) => !!p);
};
export const parseRouteMatchs = (matches: UIMatch[]) => {
return matches.reduce<string[]>((acc, match, index, array) => {
const last = array[index - 1];
const part = last
? match.pathname.replace(last.pathname, '')
: match.pathname;
const path = removeLeadingSlashes(part);
if (path) {
acc.push(path);
}
return acc;
}, []);
};

View File

@@ -1,10 +1,9 @@
import { RootState } from '@/app/store';
import { Theme } from '@/enum';
import { Theme } from '@/app/enum';
import { useAppSelector } from '@/app/hooks';
import { CheckOutlined } from '@ant-design/icons';
import { useControllableValue } from 'ahooks';
import classNames from 'classnames';
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import styles from './index.module.css';
export type ColorSelectOption = {
@@ -22,7 +21,7 @@ export type ColorSelectProps = {
export const ColorSelect: React.FC<ColorSelectProps> = (props) => {
const [value, setValue] = useControllableValue<string | undefined>(props);
const theme = useSelector((state: RootState) => state.common.theme);
const theme = useAppSelector((state) => state.common.theme);
const onChange = useCallback(
(option: ColorSelectOption) => {

View File

@@ -1,4 +1,4 @@
import { Theme } from '@/enum';
import { Theme } from '@/app/enum';
import React from 'react';
import { ColorSelect } from './color-select';

View File

@@ -4,8 +4,8 @@ import {
setPrimaryColor,
setTheme,
} from '@/app/common';
import { Locale, Theme } from '@/app/enum';
import { useAppDispatch, useAppSelector } from '@/app/hooks';
import { Locale, Theme } from '@/enum';
import { Radio, RadioChangeEvent, Tooltip } from '@df/toco-ui';
import React, { useCallback } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';

View File

@@ -1,4 +1,4 @@
import { Spin as AntSpin, SpinProps } from 'antd';
import { Spin as AntSpin, SpinProps } from '@df/toco-ui';
const Spin = (props: SpinProps) => {
const { size = 'default' } = props;
const sizeMap = {

View File

@@ -1,11 +1,8 @@
import Spin from '@/components/spin';
import React, { Suspense } from 'react';
import Spin from '../spin';
import styles from './index.module.css';
type SuspenseLayoutProps = {
children: React.ReactElement;
};
const SuspenseLayout = (props: SuspenseLayoutProps) => {
const SuspenseLayout: React.FC<React.PropsWithChildren> = (props) => {
const { children } = props;
return (
<Suspense

View File

@@ -1,27 +0,0 @@
import axiosInstance from '@/app/request';
import { AxiosResponse } from 'axios';
export interface User {
name: {
first: string;
last: string;
title: string;
};
phone: string;
gender: 'female' | 'male';
}
export const getUsers = async (): Promise<User[]> => {
const response: AxiosResponse<{
info: any;
results: User[];
}> = await axiosInstance.get(
`https://randomuser.me/api/?results=8&seed=toco`,
{
params: {
errorHandler: false,
},
},
);
return response.data?.results || [];
};

View File

@@ -1,56 +0,0 @@
import { useAppDispatch, useAppSelector } from '@/app/hooks';
import { customValueTypeMap } from '@/components';
import { User } from '@/pages/demo/api';
import { fetchUsers, selectDemo } from '@/pages/demo/slice';
import Root from '@/Root';
import { EditTableColumnsType, Table } from '@df/toco-ui';
import { useEffect } from 'react';
const columns: EditTableColumnsType<User, keyof typeof customValueTypeMap> = [
{
title: 'name',
dataIndex: 'name',
key: 'name',
valueType: 'fullname',
},
{
title: 'phone',
dataIndex: 'phone',
key: 'phone',
},
{
title: 'gender',
dataIndex: 'gender',
key: 'gender',
valueType: 'select',
fieldProps: {
options: [
{ label: '女', value: 'female' },
{ label: '男', value: 'male' },
],
},
},
];
const Demo = () => {
const { users } = useAppSelector(selectDemo);
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(fetchUsers());
}, [dispatch]);
return (
<Root>
<Table
id="demoTable"
rowKey="email"
value={users}
columns={columns}
editMode="single"
/>
</Root>
);
};
export default Demo;

View File

@@ -1,6 +0,0 @@
{
"displayName": "{{demo}}",
"icon": "CrownOutlined",
"headMenu": true,
"order": 0
}

View File

@@ -1,33 +0,0 @@
import { AppThunk, RootState } from '@/app/store';
import { getUsers, User } from '@/pages/demo/api';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface DemoState {
users: User[];
}
/* DO NOT EDIT initialState */
const initialState: DemoState = {
users: [],
};
export const slice = createSlice({
name: 'demo',
initialState,
reducers: {
setUsers: (state, action: PayloadAction<User[]>) => {
state.users = action.payload;
},
},
});
export const { setUsers } = slice.actions;
export const fetchUsers = (): AppThunk => async (dispatch, getState) => {
const data = await getUsers();
dispatch(setUsers(data));
};
export const selectDemo = (state: RootState) => state.demo;
export default slice.reducer;

View File

@@ -1,26 +0,0 @@
import axiosInstance from '@/app/request';
import { AxiosResponse } from 'axios';
export interface User {
name: {
first: string;
last: string;
};
phone: string;
gender: 'female' | 'male';
}
export const getUsers = async (): Promise<User[]> => {
const response: AxiosResponse<{
info: any;
results: User[];
}> = await axiosInstance.get(
`https://randomuser.me/api/?results=8&seed=toco`,
{
params: {
errorHandler: false,
},
},
);
return response.data?.results || [];
};

View File

@@ -1,31 +0,0 @@
import { useAppSelector } from '@/app/hooks';
import Root from '@/Root';
import { useEffect } from 'react';
import * as tocoServices from './api';
const Test = (props) => {
const tocoStore = useAppSelector((state) => state);
useEffect(() => {
// 一些常用变量或方法
console.log('props', props);
console.log('store', tocoStore);
console.log('services', tocoServices);
console.log('refs', tocoRefs);
console.log('modals', tocoModals);
}, [props, tocoStore]);
return (
<Root>
<h1>Store</h1>
<p>the primary color is: {tocoStore.common.primaryColor}</p>
<h1>Service</h1>
<h1>Refs</h1>
<div>
<p>children page</p>
</div>
</Root>
);
};
export default Test;

View File

@@ -1,10 +0,0 @@
{
"displayName": "{{test}}",
"icon": "RobotOutlined",
"headMenu": {
"key": "demo2",
"label": "{{demo2}}"
},
"sideMenu": true,
"order": 0
}

View File

@@ -1,26 +0,0 @@
import axiosInstance from '@/app/request';
import { AxiosResponse } from 'axios';
export interface User {
name: {
first: string;
last: string;
};
phone: string;
gender: 'female' | 'male';
}
export const getUsers = async (): Promise<User[]> => {
const response: AxiosResponse<{
info: any;
results: User[];
}> = await axiosInstance.get(
`https://randomuser.me/api/?results=8&seed=toco`,
{
params: {
errorHandler: false,
},
},
);
return response.data?.results || [];
};

View File

@@ -1,25 +0,0 @@
import { useAppSelector } from '@/app/hooks';
import Root from '@/Root';
import { useEffect } from 'react';
import * as tocoServices from './api';
const Xxx = (props) => {
const tocoStore = useAppSelector((state) => state);
useEffect(() => {
// 一些常用变量或方法
console.log('props', props);
console.log('services', tocoServices);
console.log('refs', tocoRefs);
console.log('modals', tocoModals);
console.log('store', tocoStore);
}, [props, tocoStore]);
return (
<Root>
<h1>xxx</h1>
</Root>
);
};
export default Xxx;

View File

@@ -1,8 +0,0 @@
{
"displayName": "xxx",
"icon": "RobotOutlined",
"headMenu": false,
"sideMenu": false,
"order": 0,
"route": "xx/:oo"
}

View File

@@ -0,0 +1,9 @@
import Layout, { LayoutType } from '@/layout';
import { Navigate } from 'react-router-dom';
import { RootRoute } from './types';
export const routerConfig: RootRoute = {
path: '/',
element: <Layout type={LayoutType.Default} />,
children: [],
};

View File

@@ -0,0 +1,32 @@
import { createBrowserRouter, Navigate, RouteObject } from 'react-router-dom';
import { routerConfig } from './config';
import { RootRoute } from './types';
import withPage, { WithPageProps } from './withPage';
export { routerConfig };
export type { RootRoute, WithPageProps };
const recursivelyFormatRoute = (
config: RootRoute['children'],
): RouteObject[] => {
return config?.map((route) => {
const { menu, children, Component, ...rest } = route;
const ret = {
...rest,
children: children ? recursivelyFormatRoute(children) : undefined,
Component: Component ? withPage(Component) : undefined,
} as RouteObject;
return ret;
});
};
export const router = createBrowserRouter([
{
...routerConfig,
children: recursivelyFormatRoute(routerConfig.children),
} as RouteObject,
{
path: '*',
element: <Navigate to="/" replace />,
},
]);

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { IndexRouteObject, NonIndexRouteObject } from 'react-router-dom';
import { WithPageProps } from './withPage';
export type RootRoute = {
menu?: undefined;
children: (IndexRoute | Route)[];
Component?: React.ComponentType<WithPageProps>;
} & Omit<NonIndexRouteObject, 'children' | 'Component'>;
export type IndexRoute = {
index: true;
menu?: undefined;
path?: undefined;
Component?: React.ComponentType<WithPageProps>;
} & Omit<IndexRouteObject, 'path' | 'Component'>;
export type Route = {
menu?: { title: string };
path: string;
children?: (IndexRoute | Route)[];
Component?: React.ComponentType<WithPageProps>;
} & Omit<NonIndexRouteObject, 'children' | 'path' | 'Component'>;

View File

@@ -1,19 +1,29 @@
import { useMemo } from 'react';
import {
Location,
NavigateFunction,
Params,
useLocation,
useNavigate,
useParams,
useSearchParams,
} from 'react-router-dom';
const withPage = (BaseComponent: React.ComponentType) => {
const WithPageComponent = (props: any) => {
export type WithPageProps<T = Record<string, unknown>> = T & {
location: Location;
params: Readonly<Params<string>>;
search: Record<string, string>;
navigate: NavigateFunction;
};
function withPage<P>(BaseComponent: React.ComponentType<WithPageProps<P>>) {
const WithPageComponent = (props: P) => {
const navigate = useNavigate();
const params = useParams();
const location = useLocation();
const [searchParamsOri] = useSearchParams();
const searchParams = useMemo(() => {
const params = {};
const params: Record<string, string> = {};
for (const [key, value] of searchParamsOri.entries()) {
params[key] = value;
}
@@ -21,21 +31,19 @@ const withPage = (BaseComponent: React.ComponentType) => {
}, [searchParamsOri]);
return (
<>
<BaseComponent
{...props}
location={location}
params={params}
search={searchParams}
navigate={navigate}
/>
</>
<BaseComponent
{...props}
location={location}
params={params}
search={searchParams}
navigate={navigate}
/>
);
};
WithPageComponent.displayName =
BaseComponent.displayName ?? 'WithPageComponent';
return WithPageComponent;
};
}
export default withPage;

View File

@@ -1,11 +0,0 @@
/* change dev-server proxy here */
// const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
// app.use(
// '/api',
// createProxyMiddleware({
// target: 'https://my.test-api.com',
// changeOrigin: true,
// }),
// );
};

View File

@@ -1,45 +0,0 @@
import { CSS_VARIABLE_PREFIX } from '@/config';
import { toCamelCase } from '@/utils';
import { GlobalToken, theme } from 'antd';
import { useMemo } from 'react';
const { useToken } = theme;
/**
* 这里把主题token注入css全局变量
*/
export function CSSVariablesStyle() {
const { token } = useToken();
const globalVars = useMemo(() => {
const varLines: string[] = [];
Object.keys(token).forEach((key) => {
const tokenValue = token[key as keyof GlobalToken];
const formatTokenValue = (v) =>
typeof v === 'number' && Number.isInteger(v) ? v + 'px' : v;
if (tokenValue) {
if (typeof tokenValue === 'object') {
// 组件token
Object.keys(tokenValue).forEach((name) => {
const cpTokenValue = tokenValue[name];
varLines.push(
`--${CSS_VARIABLE_PREFIX}-${toCamelCase(`${key.toLocaleLowerCase()}-${name}`)}: ${formatTokenValue(cpTokenValue)};`,
);
});
} else {
// 全局token
varLines.push(
`--${CSS_VARIABLE_PREFIX}-${toCamelCase(key)}: ${formatTokenValue(tokenValue)};`,
);
}
}
});
return `html {
${varLines.join('\n')}
}`;
}, [token]);
return <style>{globalVars}</style>;
}

View File

@@ -1,23 +0,0 @@
import { theme, ThemeConfig } from '@df/toco-ui';
import { SeedToken } from 'antd/es/theme/interface/seeds';
import { MapToken } from './index';
const { compactAlgorithm, darkAlgorithm } = theme;
const customAlgorithm: (token: SeedToken) => MapToken = (token) => {
const calcTokens = darkAlgorithm(token);
return {
...calcTokens,
// custom tokens
};
};
const darkTheme: ThemeConfig = {
token: {
// antd tokens
},
algorithm: [customAlgorithm, compactAlgorithm],
};
export default darkTheme;

View File

@@ -1,23 +0,0 @@
import { theme, ThemeConfig } from '@df/toco-ui';
import { SeedToken } from 'antd/es/theme/interface/seeds';
import { MapToken } from './index';
const { compactAlgorithm, defaultAlgorithm } = theme;
const customAlgorithm: (token: SeedToken) => MapToken = (token) => {
const calcTokens = defaultAlgorithm(token);
return {
...calcTokens,
// custom tokens
};
};
const defaultTheme: ThemeConfig = {
token: {
// antd tokens
},
algorithm: [customAlgorithm, compactAlgorithm],
};
export default defaultTheme;

View File

@@ -1,33 +0,0 @@
import { CommonMapToken } from 'antd/es/theme/interface/maps';
import { ColorMapToken } from 'antd/es/theme/interface/maps/colors';
import { FontMapToken } from 'antd/es/theme/interface/maps/font';
import {
HeightMapToken,
SizeMapToken,
} from 'antd/es/theme/interface/maps/size';
import { StyleMapToken } from 'antd/es/theme/interface/maps/style';
import {
ColorPalettes,
LegacyColorPalettes,
} from 'antd/es/theme/interface/presetColors';
import { SeedToken } from 'antd/es/theme/interface/seeds';
import darkTheme from './dark';
import defaultTheme from './default';
export interface MapToken
extends SeedToken,
ColorPalettes,
LegacyColorPalettes,
ColorMapToken,
SizeMapToken,
HeightMapToken,
StyleMapToken,
FontMapToken,
CommonMapToken,
CustomTokens {}
export type CustomTokens = {
// Define your custom tokens here
};
export { darkTheme, defaultTheme };

View File

@@ -1,11 +0,0 @@
// 将下划线或者中划线命名转换为驼峰命名
export const toCamelCase = (str) => {
return str
.replace(/[_-]([a-zA-Z])/g, (match, p1) => p1.toUpperCase())
.replace(/^[A-Z]/, (match) => match.toLowerCase());
};
// 将下划线命名转换为驼峰命名, 首字母大写
export const toPascalCase = (str) => {
return str.replace(/(^|[_-])([a-z])/g, (match, p1, p2) => p2.toUpperCase());
};

View File

@@ -1,150 +0,0 @@
import { metaInfo } from '@/meta';
import React from 'react';
import { FormattedMessage } from 'react-intl';
export type MenuItem = {
key: string;
label: string | React.ReactElement;
disabled?: boolean;
icon?: React.ReactNode;
children?: MenuItem[];
_order?: number;
};
const formatDisplayName = (displayName: string) => {
// 用 {{xxx}} 包起来的字符串需要处理国际化
if (/^{{(.*)}}$/.test(displayName)) {
const key = displayName.match(/^{{(.*)}}$/)![1].trim();
return <FormattedMessage id={key} />;
}
return displayName;
};
const flatten = (info: Meta[]) => {
const result: Meta[] = [];
info.forEach((item) => {
if (item._children?.length) {
result.push(...flatten(item._children));
} else {
result.push(item);
}
});
return result;
};
const deeplySortMenu = (menus: MenuItem[]) => {
const newMenus = [...menus];
newMenus.sort((a, b) => a._order! - b._order!);
newMenus.forEach((menu) => {
if (menu.children) {
menu.children = deeplySortMenu(menu.children);
}
});
return newMenus;
};
const getMenuItems = (): MenuItem[] => {
const menus: MenuItem[] = [];
const flatMetaInfo = flatten(metaInfo);
// head
flatMetaInfo.forEach((item) => {
if (!item.headMenu) return;
// 非路由页面头部菜单
if (typeof item.headMenu !== 'boolean') {
const find = menus.find(
(menu) => menu.key === (item.headMenu as any)?.key,
);
if (!find) {
menus.push({
key: (item.headMenu as any)?.key,
label: formatDisplayName((item.headMenu as any)?.label),
_order: (item.headMenu as any)?.order ?? 0,
});
}
}
// 路由页面头部菜单
if (item.headMenu === true) {
menus.push({
key: item._fullpath ?? '',
label: formatDisplayName(item.displayName ?? ''),
_order: item.order ?? 0,
});
}
});
// side
flatMetaInfo.forEach((item) => {
if (!item.sideMenu) return;
const head = menus.find((menu) => menu.key === (item.headMenu as any)?.key);
if (!head) return;
if (!head.children) head.children = [];
// 非路由页面侧边菜单(分组)
if (typeof item.sideMenu !== 'boolean') {
const find = head.children.find(
(menu) => menu.key === (item.sideMenu as any)?.key,
);
if (!find) {
head.children.push({
key: (item.sideMenu as any)?.key,
label: formatDisplayName((item.sideMenu as any)?.label),
_order: (item.sideMenu as any)?.order,
children: [
{
key: item._fullpath ?? '',
label: formatDisplayName(item.displayName ?? ''),
_order: item.order ?? 0,
},
],
});
} else {
find.children!.push({
key: item._fullpath ?? '',
label: formatDisplayName(item.displayName ?? ''),
_order: item.order ?? 0,
});
}
}
// 路由页面侧边菜单
if (item.sideMenu === true) {
head.children.push({
key: item._fullpath ?? '',
label: formatDisplayName(item.displayName ?? ''),
_order: item.order ?? 0,
});
}
});
// sort
return deeplySortMenu(menus);
};
export const menuItems = getMenuItems();
export const findHeadMenuByHeadKey = (headKey) => {
return menuItems.find((menu) => menu.key === headKey);
};
export const findHeadMenuBySideKey = (
sideKey: string,
menus: MenuItem[] = menuItems,
): MenuItem | null => {
const find = menus.find((menu) =>
menu.children?.some(
(child) =>
child.key === sideKey ||
!!findHeadMenuBySideKey(sideKey, menu.children ?? []),
),
);
if (find) return find;
return null;
};
export const findFirstSubMenu = (menu: MenuItem): MenuItem | undefined => {
if (!menu?.children?.[0]?.children?.length) return menu?.children?.[0];
return findFirstSubMenu(menu.children[0]);
};