Compare commits
10 Commits
011ae4b467
...
d70d3fb093
Author | SHA1 | Date | |
---|---|---|---|
|
d70d3fb093 | ||
|
ebd3a66499 | ||
|
a9638cfa38 | ||
|
112b737cc4 | ||
|
18d79b79a7 | ||
|
24f7f9524e | ||
|
e729426370 | ||
|
894d296159 | ||
|
8cfbfa85cb | ||
|
2704a9ee9e |
@ -1,3 +1,3 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [require.resolve('@3rapp/code-config/react')],
|
extends: [require.resolve('@3rapp/code-config/stylelint')],
|
||||||
};
|
};
|
@ -12,15 +12,23 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@3rapp/api": "workspace:*",
|
||||||
|
"@3rapp/common": "workspace:*",
|
||||||
"@ant-design/cssinjs": "^1.20.0",
|
"@ant-design/cssinjs": "^1.20.0",
|
||||||
"antd": "^5.17.0",
|
"antd": "^5.17.0",
|
||||||
|
"axios": "^1.6.8",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"react-use": "^17.5.0",
|
||||||
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@3rapp/code-config": "workspace:*",
|
"@3rapp/code-config": "workspace:*",
|
||||||
"@3rapp/utils": "workspace:*",
|
"@types/lodash": "^4.17.1",
|
||||||
"@types/node": "^20.12.10",
|
"@types/node": "^20.12.10",
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { pathResolve } from '@3rapp/utils';
|
import { pathResolve } from '@3rapp/common';
|
||||||
import merge from 'deepmerge';
|
import merge from 'deepmerge';
|
||||||
import { ConfigEnv, UserConfig } from 'vite';
|
import { ConfigEnv, UserConfig } from 'vite';
|
||||||
|
|
||||||
@ -19,6 +19,16 @@ export const createConfig = (params: ConfigEnv, configure?: Configure): UserConf
|
|||||||
localsConvention: 'camelCaseOnly',
|
localsConvention: 'camelCaseOnly',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3001/api',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cors: true,
|
||||||
|
},
|
||||||
plugins: createPlugins(isBuild),
|
plugins: createPlugins(isBuild),
|
||||||
},
|
},
|
||||||
typeof configure === 'function' ? configure(params, isBuild) : {},
|
typeof configure === 'function' ? configure(params, isBuild) : {},
|
||||||
|
51
apps/admin/src/Wrapper.tsx
Normal file
51
apps/admin/src/Wrapper.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { StyleProvider } from '@ant-design/cssinjs';
|
||||||
|
import { ConfigProvider, App as AntdApp } from 'antd';
|
||||||
|
import { FC, useMemo } from 'react';
|
||||||
|
|
||||||
|
import $styles from './app.module.css';
|
||||||
|
import { localeData } from './components/demo/constants';
|
||||||
|
import ContextDemo from './components/demo/context';
|
||||||
|
import CustomDemo from './components/demo/custom';
|
||||||
|
import { useLocale } from './components/demo/hooks';
|
||||||
|
import { useAntdAlgorithm } from './components/theme/hooks';
|
||||||
|
|
||||||
|
export const Wrapper: FC = () => {
|
||||||
|
const locale = useLocale();
|
||||||
|
const antdLocaleData = useMemo(() => {
|
||||||
|
if (!Object.keys(localeData).find((v) => v === locale.name)) {
|
||||||
|
return localeData[0];
|
||||||
|
}
|
||||||
|
return localeData[locale.name];
|
||||||
|
}, [locale.name]);
|
||||||
|
// const themeState = useTheme();
|
||||||
|
// const algorithm = useMemo(() => {
|
||||||
|
// const result = [themeState.compact ? theme.compactAlgorithm : theme.defaultAlgorithm];
|
||||||
|
// if (themeState.mode === 'dark') result.push(theme.darkAlgorithm);
|
||||||
|
// return result;
|
||||||
|
// }, [themeState]);
|
||||||
|
const algorithm = useAntdAlgorithm();
|
||||||
|
return (
|
||||||
|
<ConfigProvider
|
||||||
|
locale={antdLocaleData}
|
||||||
|
theme={{
|
||||||
|
algorithm,
|
||||||
|
token: {},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyleProvider hashPriority="high">
|
||||||
|
<AntdApp>
|
||||||
|
<div className={$styles.app}>
|
||||||
|
{/* <StateDemo />
|
||||||
|
<EffectDemo />
|
||||||
|
<RefDemo />
|
||||||
|
<MemoDemo />
|
||||||
|
<CallbackDemo /> */}
|
||||||
|
<ContextDemo />
|
||||||
|
{/* <ReducerDemo /> */}
|
||||||
|
<CustomDemo />
|
||||||
|
</div>
|
||||||
|
</AntdApp>
|
||||||
|
</StyleProvider>
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
.app {
|
.app {
|
||||||
@apply tw-bg-yellow-300 tw-flex tw-flex-auto tw-items-center tw-justify-center;
|
@apply tw-flex tw-flex-auto tw-flex-wrap tw-items-center tw-justify-center;
|
||||||
|
|
||||||
& > .container {
|
& > .container {
|
||||||
@apply tw-shadow-md tw-p-5 tw-bg-black tw-text-center tw-text-white tw-text-lg;
|
@apply tw-shadow-md tw-p-5 tw-bg-black tw-text-center tw-text-white tw-text-lg;
|
||||||
|
@ -1,48 +1,63 @@
|
|||||||
// src/app.tsx
|
|
||||||
|
|
||||||
import { StyleProvider } from '@ant-design/cssinjs';
|
import { StyleProvider } from '@ant-design/cssinjs';
|
||||||
import { Button, ConfigProvider, theme, App as AntdApp } from 'antd';
|
import { ConfigProvider, App as AntdApp } from 'antd';
|
||||||
// import 'dayjs/locale/zh-cn';
|
// import 'dayjs/locale/zh-cn';
|
||||||
import zhCN from 'antd/locale/zh_CN';
|
|
||||||
|
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
|
|
||||||
import $styles from './app.module.css';
|
import $styles from './app.module.css';
|
||||||
|
import { Locale } from './components/demo/context';
|
||||||
|
import { useLocaleData } from './components/i18n/hooks';
|
||||||
|
import Setting from './components/setting';
|
||||||
|
import Theme from './components/theme';
|
||||||
|
import { useAntdAlgorithm } from './components/theme/hooks';
|
||||||
|
|
||||||
const App: FC = () => {
|
const Wrapper: FC = () => {
|
||||||
|
const locale = useLocaleData();
|
||||||
|
// const locale = useLocale();
|
||||||
|
// const antdLocaleData = useMemo(() => {
|
||||||
|
// if (!Object.keys(localeData).find((v) => v === locale.name)) {
|
||||||
|
// return localeData[0];
|
||||||
|
// }
|
||||||
|
// return localeData[locale.name];
|
||||||
|
// }, [locale.name]);
|
||||||
|
// const themeState = useTheme();
|
||||||
|
// const algorithm = useMemo(() => {
|
||||||
|
// const result = [themeState.compact ? theme.compactAlgorithm : theme.defaultAlgorithm];
|
||||||
|
// if (themeState.mode === 'dark') result.push(theme.darkAlgorithm);
|
||||||
|
// return result;
|
||||||
|
// }, [themeState]);
|
||||||
|
const algorithm = useAntdAlgorithm();
|
||||||
return (
|
return (
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
locale={zhCN}
|
locale={locale.antd}
|
||||||
theme={{
|
theme={{
|
||||||
algorithm: theme.defaultAlgorithm,
|
algorithm,
|
||||||
token: {
|
token: {},
|
||||||
colorPrimary: '#00B96B',
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
Layout: {
|
|
||||||
colorBgBody: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StyleProvider hashPriority="high">
|
<StyleProvider hashPriority="high">
|
||||||
<AntdApp>
|
<AntdApp>
|
||||||
<div className={$styles.app}>
|
<div className={$styles.app}>
|
||||||
<div className={$styles.container}>
|
{/* <StateDemo />
|
||||||
欢迎来到3R教室,这是<span>React课程第一节</span>
|
<EffectDemo />
|
||||||
<Button
|
<RefDemo />
|
||||||
type="primary"
|
<MemoDemo />
|
||||||
className="!bg-lime-400 !text-emerald-900"
|
<CallbackDemo />
|
||||||
href="https://pincman.com/3r"
|
<ContextDemo />
|
||||||
target="_blank"
|
<ReducerDemo />
|
||||||
>
|
<CustomDemo /> */}
|
||||||
点此打开
|
<Setting />
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</AntdApp>
|
</AntdApp>
|
||||||
</StyleProvider>
|
</StyleProvider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
const App: FC = () => (
|
||||||
|
<Locale>
|
||||||
|
<Theme>
|
||||||
|
<Wrapper />
|
||||||
|
</Theme>
|
||||||
|
</Locale>
|
||||||
|
);
|
||||||
export default App;
|
export default App;
|
||||||
|
BIN
apps/admin/src/assets/images/bg-dark.png
Normal file
BIN
apps/admin/src/assets/images/bg-dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 266 KiB |
BIN
apps/admin/src/assets/images/bg-light.png
Normal file
BIN
apps/admin/src/assets/images/bg-light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 923 KiB |
30
apps/admin/src/components/demo/callback.tsx
Normal file
30
apps/admin/src/components/demo/callback.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Button } from 'antd';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { FC, memo, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import $styles from './style.module.css';
|
||||||
|
|
||||||
|
const Info: FC<{ call: () => void }> = memo(() => {
|
||||||
|
console.log('渲染消息');
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CallbackDemo: FC = () => {
|
||||||
|
const [, setCount] = useState<number>(0);
|
||||||
|
const changeCount = () => setCount(Math.ceil(Math.random() * 10));
|
||||||
|
const getInfo = useCallback(() => {}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('getInfo函数的值改变');
|
||||||
|
}, [getInfo]);
|
||||||
|
return (
|
||||||
|
<div className={clsx($styles.container, 'tw-w-[20rem]')}>
|
||||||
|
<h2 className="tw-text-center">useCallback Demo</h2>
|
||||||
|
<div className="tw-flex tw-justify-around">
|
||||||
|
<Info call={getInfo} />
|
||||||
|
<Button onClick={changeCount} type="dashed">
|
||||||
|
变化coun1
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
27
apps/admin/src/components/demo/constants.ts
Normal file
27
apps/admin/src/components/demo/constants.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Locale } from 'antd/es/locale';
|
||||||
|
import enUS from 'antd/es/locale/en_US';
|
||||||
|
import zhCN from 'antd/es/locale/zh_CN';
|
||||||
|
|
||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
import { LocaleType, ThemeContextType, ThemeState } from './types';
|
||||||
|
|
||||||
|
export const localeData: Record<string, Locale> = {
|
||||||
|
en_US: enUS,
|
||||||
|
zh_CN: zhCN,
|
||||||
|
};
|
||||||
|
export const locales: LocaleType[] = [
|
||||||
|
{
|
||||||
|
name: 'en_US',
|
||||||
|
label: '🇺🇸 english(US)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'zh_CN',
|
||||||
|
label: '🇨🇳 简体中文',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const defaultThemeConfig: ThemeState = {
|
||||||
|
mode: 'light',
|
||||||
|
compact: false,
|
||||||
|
};
|
||||||
|
export const ThemeContext = createContext<ThemeContextType | null>(null);
|
71
apps/admin/src/components/demo/context.tsx
Normal file
71
apps/admin/src/components/demo/context.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { Pagination, Select } from 'antd';
|
||||||
|
import { createContext, FC, ReactNode, useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { localeData, locales } from './constants';
|
||||||
|
import { useLocale, useLocaleAction } from './hooks';
|
||||||
|
import $styles from './style.module.css';
|
||||||
|
import { LocaleState, LocaleType } from './types';
|
||||||
|
|
||||||
|
export const LocaleContext = createContext<LocaleState>({
|
||||||
|
locale: locales[0],
|
||||||
|
setLocale: (_locale: LocaleType) => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const LocaleProvider: FC<LocaleState & { children?: ReactNode }> = ({
|
||||||
|
locale,
|
||||||
|
setLocale,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const value = useMemo(() => ({ locale, setLocale }), [locale]);
|
||||||
|
return <LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Locale: FC<{ children?: ReactNode }> = ({ children }) => {
|
||||||
|
const [locale, setLocale] = useState<LocaleType>(locales[0]);
|
||||||
|
const changeLocale = useCallback((value: LocaleType) => {
|
||||||
|
if (Object.keys(localeData).find((v) => v === value.name)) {
|
||||||
|
setLocale(value);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<LocaleProvider locale={locale} setLocale={changeLocale}>
|
||||||
|
{children}
|
||||||
|
</LocaleProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LocaleConfig: FC = () => {
|
||||||
|
const locale = useLocale();
|
||||||
|
const setLocale = useLocaleAction();
|
||||||
|
const changeLocale = (value: string) => {
|
||||||
|
const current = locales.find((item) => item.name === value);
|
||||||
|
current && setLocale(current);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Select defaultValue={locale.name} style={{ width: 120 }} onChange={changeLocale}>
|
||||||
|
{locales.map(({ name, label }) => (
|
||||||
|
<Select.Option key={name} value={name}>
|
||||||
|
{label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContextDemo: FC = () => {
|
||||||
|
const locale = useLocale();
|
||||||
|
return (
|
||||||
|
<div className={$styles.container}>
|
||||||
|
<h2 className="tw-text-center">useContext Demo</h2>
|
||||||
|
<p className="tw-text-center tw-py-5">当前语言: {locale.label}</p>
|
||||||
|
<div className="tw-w-full">
|
||||||
|
<h3>Antd语言切换测试</h3>
|
||||||
|
<div className="tw-w-full tw-my-4">
|
||||||
|
<LocaleConfig />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Pagination defaultCurrent={0} total={500} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ContextDemo;
|
25
apps/admin/src/components/demo/custom.tsx
Normal file
25
apps/admin/src/components/demo/custom.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Button } from 'antd';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { FC, useState } from 'react';
|
||||||
|
|
||||||
|
import { useUpdateEffect } from './hooks';
|
||||||
|
import $styles from './style.module.css';
|
||||||
|
|
||||||
|
const CustomDemo: FC = () => {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
useUpdateEffect(() => {
|
||||||
|
console.log('changed');
|
||||||
|
}, [count]);
|
||||||
|
return (
|
||||||
|
<div className={clsx($styles.container, 'tw-w-[20rem]')}>
|
||||||
|
<h2 className="tw-text-center">Custom Demo</h2>
|
||||||
|
<p className="tw-text-center tw-py-5">{count}</p>
|
||||||
|
<div className="tw-flex tw-justify-around">
|
||||||
|
<Button onClick={() => setCount(Math.ceil(Math.random() * 10))} type="dashed">
|
||||||
|
变化
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default CustomDemo;
|
43
apps/admin/src/components/demo/effect.tsx
Normal file
43
apps/admin/src/components/demo/effect.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Button } from 'antd';
|
||||||
|
import { FC, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import $styles from './style.module.css';
|
||||||
|
|
||||||
|
const EffectDemo: FC = () => {
|
||||||
|
const [red, setRed] = useState<boolean>(false);
|
||||||
|
const [ghost, setGhost] = useState<boolean>(false);
|
||||||
|
const [width, setWidth] = useState(window.innerWidth);
|
||||||
|
const toggleGhostBtn = () => setGhost(!ghost);
|
||||||
|
const resizeHandle = () => setWidth(window.innerWidth);
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('浏览器宽度改变');
|
||||||
|
window.addEventListener('resize', resizeHandle);
|
||||||
|
}, [width]);
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('切换幽灵按钮');
|
||||||
|
}, [ghost]);
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('只在第一次或重新渲染组件时触发');
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(() => resolve(true), 1000);
|
||||||
|
});
|
||||||
|
setRed(ghost);
|
||||||
|
})();
|
||||||
|
}, [ghost]);
|
||||||
|
return (
|
||||||
|
<div className={$styles.container}>
|
||||||
|
<h2 className="tw-text-center">useEffect Demo</h2>
|
||||||
|
<p className="tw-text-center tw-py-5">{ghost ? 'ghost' : '普通'}按钮</p>
|
||||||
|
<div className="tw-flex tw-justify-center tw-flex-col">
|
||||||
|
<Button type="primary" onClick={toggleGhostBtn} ghost={ghost} danger={red}>
|
||||||
|
切换按钮样式
|
||||||
|
</Button>
|
||||||
|
<p className="tw-pt-5 tw-text-center">宽度为: {width}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default EffectDemo;
|
46
apps/admin/src/components/demo/hooks.tsx
Normal file
46
apps/admin/src/components/demo/hooks.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { isEqual, isNil } from 'lodash';
|
||||||
|
import {
|
||||||
|
DependencyList,
|
||||||
|
EffectCallback,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { defaultThemeConfig, locales, ThemeContext } from './constants';
|
||||||
|
import { LocaleContext } from './context';
|
||||||
|
|
||||||
|
export const useUpdateEffect = (effect: EffectCallback, deps?: DependencyList) => {
|
||||||
|
const inited = useRef(deps);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEqual(inited.current, deps)) {
|
||||||
|
inited.current = deps;
|
||||||
|
effect();
|
||||||
|
}
|
||||||
|
}, [deps]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = useContext(ThemeContext) ?? ({} as Record<string, any>);
|
||||||
|
return useMemo(
|
||||||
|
() => (isNil(context.state) ? defaultThemeConfig : context.state),
|
||||||
|
[context.state],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useThemeAction = () => {
|
||||||
|
const context = useContext(ThemeContext) ?? ({} as Record<string, any>);
|
||||||
|
return useCallback(isNil(context.dispatch) ? null : context.dispatch, [context.dispatch]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLocale = () => {
|
||||||
|
const context = useContext(LocaleContext) ?? ({} as Record<string, any>);
|
||||||
|
return useMemo(() => (isNil(context.locale) ? locales[0] : context.locale), [context.locale]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLocaleAction = () => {
|
||||||
|
const context = useContext(LocaleContext) ?? ({} as Record<string, any>);
|
||||||
|
return useCallback(isNil(context.setLocale) ? null : context.setLocale, [context.setLocale]);
|
||||||
|
};
|
40
apps/admin/src/components/demo/memo.tsx
Normal file
40
apps/admin/src/components/demo/memo.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Button } from 'antd';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { FC, memo, useState } from 'react';
|
||||||
|
|
||||||
|
import $styles from './style.module.css';
|
||||||
|
|
||||||
|
const ChildCom1: FC<{ value: number }> = memo(() => {
|
||||||
|
console.log('渲染子组件1');
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
const ChildCom2: FC<{ value: number }> = memo(() => {
|
||||||
|
console.log('渲染子组件2');
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
const MemoDemo: FC = () => {
|
||||||
|
const [count1, setCount1] = useState<number>(0);
|
||||||
|
const [count2, setCount2] = useState<number>(0);
|
||||||
|
// const ChildWrap1 = useMemo(() => <ChildCom1 value={count1} />, [count1]);
|
||||||
|
// const ChildWrap2 = useMemo(() => <ChildCom2 value={count2} />, [count2]);
|
||||||
|
return (
|
||||||
|
<div className={clsx($styles.container, 'tw-w-[20rem]')}>
|
||||||
|
<h2 className="tw-text-center">useMemo Demo</h2>
|
||||||
|
<div className="tw-flex tw-justify-around">
|
||||||
|
<Button onClick={() => setCount1(Math.ceil(Math.random() * 10))} type="dashed">
|
||||||
|
变化coun1
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setCount2(Math.ceil(Math.random() * 10))} type="dashed">
|
||||||
|
变化coun2
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="tw-flex tw-justify-around">
|
||||||
|
<ChildCom1 value={count1} />
|
||||||
|
<ChildCom2 value={count2} />
|
||||||
|
{/* {ChildWrap1}
|
||||||
|
{ChildWrap2} */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default MemoDemo;
|
90
apps/admin/src/components/demo/reducer.tsx
Normal file
90
apps/admin/src/components/demo/reducer.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { Calendar, Switch } from 'antd';
|
||||||
|
import { produce } from 'immer';
|
||||||
|
import { isNil } from 'lodash';
|
||||||
|
import { FC, ReactNode, Reducer, useContext, useEffect, useMemo, useReducer } from 'react';
|
||||||
|
|
||||||
|
import { defaultThemeConfig, ThemeContext } from './constants';
|
||||||
|
import $styles from './style.module.css';
|
||||||
|
import { ThemeAction, ThemeState } from './types';
|
||||||
|
|
||||||
|
const ThemeReducer: Reducer<ThemeState, ThemeAction> = produce((draft, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'change_mode':
|
||||||
|
draft.mode = action.value;
|
||||||
|
break;
|
||||||
|
case 'change_compact':
|
||||||
|
draft.compact = action.value;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Theme: FC<{ data?: ThemeState; children?: ReactNode }> = ({ data = {}, children }) => {
|
||||||
|
const [state, dispatch] = useReducer(ThemeReducer, data, (initData) => ({
|
||||||
|
...defaultThemeConfig,
|
||||||
|
...initData,
|
||||||
|
}));
|
||||||
|
useEffect(() => {
|
||||||
|
const body = document.getElementsByTagName('body');
|
||||||
|
if (body.length) {
|
||||||
|
body[0].classList.remove('light');
|
||||||
|
body[0].classList.remove('dark');
|
||||||
|
body[0].classList.add(state.mode === 'dark' ? 'dark' : 'light');
|
||||||
|
}
|
||||||
|
}, [state.mode]);
|
||||||
|
const value = useMemo(() => ({ state, dispatch }), [state]);
|
||||||
|
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ThemeConfig: FC = () => {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (isNil(context)) return null;
|
||||||
|
const { state, dispatch } = context;
|
||||||
|
const toggleMode = () =>
|
||||||
|
dispatch({ type: 'change_mode', value: state.mode === 'light' ? 'dark' : 'light' });
|
||||||
|
const toggleCompact = () => dispatch({ type: 'change_compact', value: !state.compact });
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Switch
|
||||||
|
checkedChildren="🌛"
|
||||||
|
unCheckedChildren="☀️"
|
||||||
|
onChange={toggleMode}
|
||||||
|
checked={state.mode === 'dark'}
|
||||||
|
defaultChecked={state.mode === 'dark'}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
checkedChildren="紧凑"
|
||||||
|
unCheckedChildren="正常"
|
||||||
|
onChange={toggleCompact}
|
||||||
|
checked={state.compact}
|
||||||
|
defaultChecked={state.compact}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReducerDemo: FC = () => {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (isNil(context)) return null;
|
||||||
|
const {
|
||||||
|
state: { mode, compact },
|
||||||
|
} = context;
|
||||||
|
return (
|
||||||
|
<div className={$styles.container}>
|
||||||
|
<h2 className="tw-text-center">useReducer Demo</h2>
|
||||||
|
<div className="tw-flex tw-items-center tw-flex-col">
|
||||||
|
<p>主题模式: 「{mode === 'dark' ? '暗黑' : '明亮'}」</p>
|
||||||
|
<p>是否紧凑: 「{compact ? '是' : '否'}」</p>
|
||||||
|
<div className="tw-flex-auto tw-mb-5">
|
||||||
|
<ThemeConfig />
|
||||||
|
</div>
|
||||||
|
<div className="tw-max-w-md">
|
||||||
|
<Calendar fullscreen={false} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReducerDemo;
|
75
apps/admin/src/components/demo/ref.tsx
Normal file
75
apps/admin/src/components/demo/ref.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { Button } from 'antd';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { isNaN, isNil } from 'lodash';
|
||||||
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
|
FC,
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import $styles from './style.module.css';
|
||||||
|
|
||||||
|
interface RefFunc {
|
||||||
|
focus: () => void;
|
||||||
|
memo: () => number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MyInput = forwardRef<RefFunc, { value: number; changeValue: (v: number) => void }>(
|
||||||
|
({ value, changeValue }, ref) => {
|
||||||
|
const [local, setLocal] = useState<number | string>(value);
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
focus: () => inputRef.current.focus(),
|
||||||
|
memo: () => value,
|
||||||
|
}),
|
||||||
|
[value],
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
changeValue(isNaN(Number(local)) ? 0 : Number(local));
|
||||||
|
}, [changeValue, local]);
|
||||||
|
const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback((e) => {
|
||||||
|
setLocal(e.target.value);
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<input value={value} ref={inputRef} placeholder="请输入值" onChange={handleChange} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const RefDemo: FC = () => {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
const inited = useRef(count);
|
||||||
|
const ref = useRef<RefFunc | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current.focus();
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
if (inited.current !== count) {
|
||||||
|
inited.current = count;
|
||||||
|
console.log('changed');
|
||||||
|
}
|
||||||
|
}, [count]);
|
||||||
|
return (
|
||||||
|
<div className={clsx($styles.container, 'tw-w-[20rem]')}>
|
||||||
|
<h2 className="tw-text-center">useRef Demo</h2>
|
||||||
|
<p className="tw-text-center tw-py-5">{count}</p>
|
||||||
|
<div className="tw-flex tw-justify-around">
|
||||||
|
<Button onClick={() => setCount(Math.ceil(Math.random() * 10))} type="dashed">
|
||||||
|
变化
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{!isNil(ref.current) && <p className="tw-my-3">前一个值:{ref.current.memo()}</p>}
|
||||||
|
<MyInput ref={ref} value={count} changeValue={setCount} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default RefDemo;
|
29
apps/admin/src/components/demo/state.tsx
Normal file
29
apps/admin/src/components/demo/state.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Button } from 'antd';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
import { FC, useState } from 'react';
|
||||||
|
|
||||||
|
import $styles from './style.module.css';
|
||||||
|
|
||||||
|
const StateDemo: FC = () => {
|
||||||
|
const [count, setCount] = useState(1);
|
||||||
|
const [isShow, toggleShow] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx($styles.container, 'tw-w-[20rem]')}>
|
||||||
|
<h2 className="tw-text-center">useState Demo</h2>
|
||||||
|
{isShow && <p className="tw-text-center tw-py-5">{count}</p>}
|
||||||
|
<div className="tw-flex tw-justify-around">
|
||||||
|
<Button onClick={() => setCount(count + 1)} type="dashed">
|
||||||
|
增加
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setCount(count - 1)} type="dashed">
|
||||||
|
减少
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => toggleShow(!isShow)} type="dashed">
|
||||||
|
{isShow ? '显示' : '隐藏'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default StateDemo;
|
9
apps/admin/src/components/demo/style.module.css
Normal file
9
apps/admin/src/components/demo/style.module.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.container {
|
||||||
|
@apply tw-bg-neutral-100/40 tw-shadow-black/20 tw-backdrop-blur-sm tw-shadow-md tw-rounded-md tw-p-5 tw-m-5 tw-min-w-[20rem];
|
||||||
|
}
|
||||||
|
|
||||||
|
body(:global(.dark)) {
|
||||||
|
.container {
|
||||||
|
@apply tw-bg-neutral-800/40 tw-shadow-slate-100/30;
|
||||||
|
}
|
||||||
|
}
|
38
apps/admin/src/components/demo/types.ts
Normal file
38
apps/admin/src/components/demo/types.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Dispatch } from 'react';
|
||||||
|
|
||||||
|
export type LocaleType = {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
export type LocaleState = {
|
||||||
|
locale: LocaleType;
|
||||||
|
setLocale: (locale: LocaleType) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum ThemeMode {
|
||||||
|
LIGHT = 'light',
|
||||||
|
DARK = 'dark',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThemeState = {
|
||||||
|
mode: `${ThemeMode}`;
|
||||||
|
compact: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum ThemeActionType {
|
||||||
|
CHANGE_MODE = 'change_mode',
|
||||||
|
TOOGLE_MODE = 'toggle_mode',
|
||||||
|
CHANGE_COMPACT = 'change_compact',
|
||||||
|
TOOGLE_COMPACT = 'toggle_compact',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThemeAction =
|
||||||
|
| { type: `${ThemeActionType.CHANGE_MODE}`; value: `${ThemeMode}` }
|
||||||
|
| { type: `${ThemeActionType.TOOGLE_MODE}` }
|
||||||
|
| { type: `${ThemeActionType.CHANGE_COMPACT}`; value: boolean }
|
||||||
|
| { type: `${ThemeActionType.TOOGLE_COMPACT}` };
|
||||||
|
|
||||||
|
export type ThemeContextType = {
|
||||||
|
state: ThemeState;
|
||||||
|
dispatch: Dispatch<ThemeAction>;
|
||||||
|
};
|
3
apps/admin/src/components/i18n/constants.ts
Normal file
3
apps/admin/src/components/i18n/constants.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { LangType } from './types';
|
||||||
|
|
||||||
|
export const langs: `${LangType}`[] = ['en_US', 'zh_CN'];
|
15
apps/admin/src/components/i18n/data.ts
Normal file
15
apps/admin/src/components/i18n/data.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import enUS from 'antd/es/locale/en_US';
|
||||||
|
import zhCN from 'antd/es/locale/zh_CN';
|
||||||
|
|
||||||
|
import { LangType, LocaleItem } from './types';
|
||||||
|
|
||||||
|
export const localeData: Record<`${LangType}`, LocaleItem> = {
|
||||||
|
en_US: {
|
||||||
|
label: '🇺🇸 english(US)',
|
||||||
|
antd: enUS,
|
||||||
|
},
|
||||||
|
zh_CN: {
|
||||||
|
label: '🇨🇳 简体中文',
|
||||||
|
antd: zhCN,
|
||||||
|
},
|
||||||
|
};
|
11
apps/admin/src/components/i18n/hooks.ts
Normal file
11
apps/admin/src/components/i18n/hooks.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { localeData } from './data';
|
||||||
|
import { useLocaleStore } from './store';
|
||||||
|
|
||||||
|
export const useLocale = () => useLocaleStore((state) => state.lang);
|
||||||
|
export const useLocaleChange = () => useLocaleStore((state) => state.changeLang);
|
||||||
|
export const useLocaleData = () => {
|
||||||
|
const lang = useLocale();
|
||||||
|
return useMemo(() => localeData[lang], [lang]);
|
||||||
|
};
|
12
apps/admin/src/components/i18n/index.tsx
Normal file
12
apps/admin/src/components/i18n/index.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { isNil } from 'lodash';
|
||||||
|
import { FC, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { useLocaleChange } from './hooks';
|
||||||
|
import { LangType } from './types';
|
||||||
|
|
||||||
|
const Locale: FC<{ children?: ReactNode } & { lang: `${LangType}` }> = ({ children, lang }) => {
|
||||||
|
const changeLocale = useLocaleChange();
|
||||||
|
if (!isNil(changeLocale)) changeLocale(lang);
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
export default Locale;
|
23
apps/admin/src/components/i18n/store.ts
Normal file
23
apps/admin/src/components/i18n/store.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { isNil } from 'lodash';
|
||||||
|
|
||||||
|
import { createPersistStore } from '../store';
|
||||||
|
|
||||||
|
import { langs } from './constants';
|
||||||
|
import { LangType } from './types';
|
||||||
|
|
||||||
|
export const useLocaleStore = createPersistStore<{
|
||||||
|
lang: `${LangType}`;
|
||||||
|
changeLang: (name: `${LangType}`) => void;
|
||||||
|
}>(
|
||||||
|
(set) => ({
|
||||||
|
lang: langs[0],
|
||||||
|
changeLang: (name: `${LangType}`) =>
|
||||||
|
set((state) => {
|
||||||
|
const item = langs.find((n) => n === name);
|
||||||
|
if (!isNil(item)) state.lang = item;
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'locale',
|
||||||
|
},
|
||||||
|
);
|
10
apps/admin/src/components/i18n/types.ts
Normal file
10
apps/admin/src/components/i18n/types.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Locale } from 'antd/es/locale';
|
||||||
|
|
||||||
|
export enum LangType {
|
||||||
|
EN_US = 'en_US',
|
||||||
|
ZH_CN = 'zh_CN',
|
||||||
|
}
|
||||||
|
export interface LocaleItem {
|
||||||
|
label: string;
|
||||||
|
antd: Locale;
|
||||||
|
}
|
64
apps/admin/src/components/setting/index.tsx
Normal file
64
apps/admin/src/components/setting/index.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Calendar, Select, Switch } from 'antd';
|
||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { langs } from '../i18n/constants';
|
||||||
|
import { localeData } from '../i18n/data';
|
||||||
|
import { useLocale, useLocaleChange } from '../i18n/hooks';
|
||||||
|
import { useTheme, useThemeActions } from '../theme/hooks';
|
||||||
|
|
||||||
|
import $styles from './style.module.css';
|
||||||
|
|
||||||
|
const ThemeSetting: FC = () => {
|
||||||
|
const { mode, compact } = useTheme();
|
||||||
|
const { toggleMode, toggleCompact } = useThemeActions();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Switch
|
||||||
|
checkedChildren="🌛"
|
||||||
|
unCheckedChildren="☀️"
|
||||||
|
onChange={toggleMode}
|
||||||
|
checked={mode === 'dark'}
|
||||||
|
defaultChecked={mode === 'dark'}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
checkedChildren="紧凑"
|
||||||
|
unCheckedChildren="正常"
|
||||||
|
onChange={toggleCompact}
|
||||||
|
checked={compact}
|
||||||
|
defaultChecked={compact}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LocaleSetting: FC = () => {
|
||||||
|
const locale = useLocale();
|
||||||
|
const changeLocale = useLocaleChange();
|
||||||
|
return (
|
||||||
|
<Select defaultValue={locale} style={{ width: 120 }} onChange={changeLocale}>
|
||||||
|
{langs.map((name) => (
|
||||||
|
<Select.Option key={name} value={name}>
|
||||||
|
{localeData[name].label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Setting: FC = () => (
|
||||||
|
<div className={$styles.container}>
|
||||||
|
<h2 className="tw-text-center">Setting Demo</h2>
|
||||||
|
<div className="tw-flex tw-items-center tw-flex-col">
|
||||||
|
<div className="tw-flex-auto tw-mb-5">
|
||||||
|
<ThemeSetting />
|
||||||
|
</div>
|
||||||
|
<div className="tw-flex-auto tw-mb-5">
|
||||||
|
<LocaleSetting />
|
||||||
|
</div>
|
||||||
|
<div className="tw-max-w-md">
|
||||||
|
<Calendar fullscreen={false} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export default Setting;
|
9
apps/admin/src/components/setting/style.module.css
Normal file
9
apps/admin/src/components/setting/style.module.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.container {
|
||||||
|
@apply tw-bg-neutral-100/40 tw-shadow-black/20 tw-backdrop-blur-sm tw-shadow-md tw-rounded-md tw-p-5 tw-m-5 tw-min-w-[20rem];
|
||||||
|
}
|
||||||
|
|
||||||
|
body(:global(.dark)) {
|
||||||
|
.container {
|
||||||
|
@apply tw-bg-neutral-800/40 tw-shadow-slate-100/30;
|
||||||
|
}
|
||||||
|
}
|
167
apps/admin/src/components/store/index.ts
Normal file
167
apps/admin/src/components/store/index.ts
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import { capitalize } from 'lodash';
|
||||||
|
import { create, StateCreator, Mutate, StoreApi, UseBoundStore } from 'zustand';
|
||||||
|
import {
|
||||||
|
subscribeWithSelector,
|
||||||
|
devtools,
|
||||||
|
persist,
|
||||||
|
PersistOptions,
|
||||||
|
DevtoolsOptions,
|
||||||
|
redux,
|
||||||
|
} from 'zustand/middleware';
|
||||||
|
import { immer } from 'zustand/middleware/immer';
|
||||||
|
|
||||||
|
export type ZustandHookSelectors<StateType> = {
|
||||||
|
[Key in keyof StateType as `use${Capitalize<string & Key>}`]: () => StateType[Key];
|
||||||
|
};
|
||||||
|
export interface ZustandGetterSelectors<StateType> {
|
||||||
|
getters: {
|
||||||
|
[key in keyof StateType]: () => StateType[key];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建包含订阅,immer以及devtoools功能的普通状态商店
|
||||||
|
* @param creator
|
||||||
|
* @param devtoolsOptions
|
||||||
|
*/
|
||||||
|
export const createStore = <T extends object>(
|
||||||
|
creator: StateCreator<
|
||||||
|
T,
|
||||||
|
[
|
||||||
|
['zustand/subscribeWithSelector', never],
|
||||||
|
['zustand/immer', never],
|
||||||
|
['zustand/devtools', never],
|
||||||
|
]
|
||||||
|
>,
|
||||||
|
devtoolsOptions?: DevtoolsOptions,
|
||||||
|
) => {
|
||||||
|
return create<T>()(subscribeWithSelector(immer(devtools(creator, devtoolsOptions))));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建包含订阅,immer以及devtoools功能的普通状态商店
|
||||||
|
* 同时支持自动存储到客户端,默认存储到localstorage
|
||||||
|
* @param creator
|
||||||
|
* @param persistOptions
|
||||||
|
* @param devtoolsOptions
|
||||||
|
*/
|
||||||
|
export const createPersistStore = <T extends object, P = T>(
|
||||||
|
creator: StateCreator<
|
||||||
|
T,
|
||||||
|
[
|
||||||
|
['zustand/subscribeWithSelector', never],
|
||||||
|
['zustand/immer', never],
|
||||||
|
['zustand/devtools', never],
|
||||||
|
['zustand/persist', P],
|
||||||
|
]
|
||||||
|
>,
|
||||||
|
persistOptions: PersistOptions<T, P>,
|
||||||
|
devtoolsOptions?: DevtoolsOptions,
|
||||||
|
) => {
|
||||||
|
return create<T>()(
|
||||||
|
subscribeWithSelector(
|
||||||
|
immer(devtools(persist(creator as unknown as any, persistOptions), devtoolsOptions)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建包含订阅,immer以及devtoools功能的reducer状态商店
|
||||||
|
* 同时支持自动存储到客户端,默认存储到localstorage
|
||||||
|
* @param reducer
|
||||||
|
* @param initialState
|
||||||
|
* @param devtoolsOptions
|
||||||
|
*/
|
||||||
|
export const createReduxStore = <
|
||||||
|
T extends object,
|
||||||
|
A extends {
|
||||||
|
type: string;
|
||||||
|
},
|
||||||
|
>(
|
||||||
|
reducer: (state: T, action: A) => T,
|
||||||
|
initialState: T,
|
||||||
|
devtoolsOptions?: DevtoolsOptions,
|
||||||
|
) => create(subscribeWithSelector(immer(devtools(redux(reducer, initialState), devtoolsOptions))));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建包含订阅,immer以及devtoools功能的reducer状态商店
|
||||||
|
* @param reducer
|
||||||
|
* @param initialState
|
||||||
|
* @param persistOptions
|
||||||
|
* @param devtoolsOptions
|
||||||
|
*/
|
||||||
|
export const createPersistReduxStore = <
|
||||||
|
T extends object,
|
||||||
|
A extends {
|
||||||
|
type: string;
|
||||||
|
},
|
||||||
|
P = T,
|
||||||
|
>(
|
||||||
|
reducer: (state: T, action: A) => T,
|
||||||
|
initialState: T,
|
||||||
|
persistOptions: PersistOptions<T, P>,
|
||||||
|
devtoolsOptions?: DevtoolsOptions,
|
||||||
|
) =>
|
||||||
|
create(
|
||||||
|
subscribeWithSelector(
|
||||||
|
immer(
|
||||||
|
devtools(
|
||||||
|
persist(redux(reducer, initialState), persistOptions as any),
|
||||||
|
devtoolsOptions,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接通过getters获取状态值,比如store.getters.xxx()
|
||||||
|
* @param store
|
||||||
|
*/
|
||||||
|
export function createStoreGetters<T extends object>(
|
||||||
|
store: UseBoundStore<
|
||||||
|
Mutate<
|
||||||
|
StoreApi<T>,
|
||||||
|
[
|
||||||
|
['zustand/subscribeWithSelector', never],
|
||||||
|
['zustand/immer', never],
|
||||||
|
['zustand/devtools', never],
|
||||||
|
]
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
const storeIn = store as any;
|
||||||
|
|
||||||
|
storeIn.getters = {};
|
||||||
|
Object.keys(storeIn.getState()).forEach((key) => {
|
||||||
|
const selector = (state: T) => state[key as keyof T];
|
||||||
|
storeIn.getters[key] = () => storeIn(selector);
|
||||||
|
});
|
||||||
|
|
||||||
|
return storeIn as typeof store & ZustandGetterSelectors<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接通过类似hooks的方法获取状态值,比如store.useXxx()
|
||||||
|
* @param store
|
||||||
|
*/
|
||||||
|
export function createStoreHooks<T extends Record<string, any>>(
|
||||||
|
store: UseBoundStore<
|
||||||
|
Mutate<
|
||||||
|
StoreApi<T>,
|
||||||
|
[
|
||||||
|
['zustand/subscribeWithSelector', never],
|
||||||
|
['zustand/immer', never],
|
||||||
|
['zustand/devtools', never],
|
||||||
|
]
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
const storeIn = store as any;
|
||||||
|
|
||||||
|
Object.keys(storeIn.getState()).forEach((key) => {
|
||||||
|
const selector = (state: T) => state[key as keyof T];
|
||||||
|
storeIn[`use${capitalize(key)}`] = () => storeIn(selector);
|
||||||
|
});
|
||||||
|
|
||||||
|
return storeIn as typeof store & ZustandHookSelectors<T>;
|
||||||
|
}
|
6
apps/admin/src/components/theme/constants.ts
Normal file
6
apps/admin/src/components/theme/constants.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { ThemeState } from './types';
|
||||||
|
|
||||||
|
export const defaultThemeConfig: ThemeState = {
|
||||||
|
mode: 'light',
|
||||||
|
compact: false,
|
||||||
|
};
|
50
apps/admin/src/components/theme/hooks.ts
Normal file
50
apps/admin/src/components/theme/hooks.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { theme } from 'antd';
|
||||||
|
import { debounce, omit } from 'lodash';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
|
import { useThemeStore } from './store';
|
||||||
|
import { ThemeMode } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取主题状态
|
||||||
|
*/
|
||||||
|
export const useTheme = () => useThemeStore(useShallow((state) => omit(state, ['dispatch'])));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Antd主题算法
|
||||||
|
*/
|
||||||
|
export const useAntdAlgorithm = () => {
|
||||||
|
const { mode, compact } = useTheme();
|
||||||
|
return useMemo(() => {
|
||||||
|
const result = [compact ? theme.compactAlgorithm : theme.defaultAlgorithm];
|
||||||
|
if (mode === 'dark') result.push(theme.darkAlgorithm);
|
||||||
|
return result;
|
||||||
|
}, [mode, compact]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题操作函数
|
||||||
|
*/
|
||||||
|
export const useThemeActions = () => {
|
||||||
|
const dispatch = useThemeStore((state) => state.dispatch);
|
||||||
|
return {
|
||||||
|
changeMode: useCallback(
|
||||||
|
debounce(
|
||||||
|
(v: `${ThemeMode}`) => () => dispatch({ type: 'change_mode', value: v }),
|
||||||
|
100,
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
toggleMode: useCallback(
|
||||||
|
debounce(() => dispatch({ type: 'toggle_mode' }), 100, {}),
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
changeCompact: useCallback(
|
||||||
|
(v: boolean) => dispatch({ type: 'change_compact', value: v }),
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
toggleCompact: useCallback(() => dispatch({ type: 'toggle_compact' }), []),
|
||||||
|
};
|
||||||
|
};
|
43
apps/admin/src/components/theme/index.tsx
Normal file
43
apps/admin/src/components/theme/index.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { isNil } from 'lodash';
|
||||||
|
import { FC, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { useLifecycles } from 'react-use';
|
||||||
|
|
||||||
|
import { useThemeActions } from './hooks';
|
||||||
|
import { useThemeStore } from './store';
|
||||||
|
import { ThemeMode } from './types';
|
||||||
|
|
||||||
|
const Theme: FC<{ children?: ReactNode; mode?: `${ThemeMode}`; compact?: boolean }> = ({
|
||||||
|
children,
|
||||||
|
mode,
|
||||||
|
compact,
|
||||||
|
}) => {
|
||||||
|
const { changeMode, changeCompact } = useThemeActions();
|
||||||
|
let unSub: () => void;
|
||||||
|
useLifecycles(
|
||||||
|
() => {
|
||||||
|
useThemeStore.subscribe(
|
||||||
|
(state) => state.mode,
|
||||||
|
(m) => {
|
||||||
|
const body = document.getElementsByTagName('body');
|
||||||
|
if (body.length) {
|
||||||
|
body[0].classList.remove('light');
|
||||||
|
body[0].classList.remove('dark');
|
||||||
|
body[0].classList.add(m === 'dark' ? 'dark' : 'light');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fireImmediately: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (!isNil(unSub)) unSub();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!isNil(mode)) changeMode(mode);
|
||||||
|
if (!isNil(compact)) changeCompact(compact);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
export default Theme;
|
32
apps/admin/src/components/theme/store.ts
Normal file
32
apps/admin/src/components/theme/store.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { produce } from 'immer';
|
||||||
|
import { Reducer } from 'react';
|
||||||
|
|
||||||
|
import { ThemeAction } from '../demo/types';
|
||||||
|
import { createPersistReduxStore } from '../store';
|
||||||
|
|
||||||
|
import { defaultThemeConfig } from './constants';
|
||||||
|
import { ThemeState } from './types';
|
||||||
|
|
||||||
|
const ThemeReducer: Reducer<ThemeState, ThemeAction> = produce((draft, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'change_mode':
|
||||||
|
draft.mode = action.value;
|
||||||
|
break;
|
||||||
|
case 'toggle_mode':
|
||||||
|
draft.mode = draft.mode === 'dark' ? 'light' : 'dark';
|
||||||
|
break;
|
||||||
|
case 'change_compact':
|
||||||
|
draft.compact = action.value;
|
||||||
|
break;
|
||||||
|
case 'toggle_compact':
|
||||||
|
draft.compact = !draft.compact;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useThemeStore = createPersistReduxStore(ThemeReducer, defaultThemeConfig, {
|
||||||
|
name: 'theme',
|
||||||
|
partialize: (state) => ({ mode: state.mode, compact: state.compact }),
|
||||||
|
});
|
9
apps/admin/src/components/theme/types.ts
Normal file
9
apps/admin/src/components/theme/types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export enum ThemeMode {
|
||||||
|
LIGHT = 'light',
|
||||||
|
DARK = 'dark',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThemeState = {
|
||||||
|
mode: `${ThemeMode}`;
|
||||||
|
compact: boolean;
|
||||||
|
};
|
@ -4,3 +4,11 @@ body,
|
|||||||
.ant-app {
|
.ant-app {
|
||||||
@apply tw-h-[100vh] tw-w-full tw-flex tw-p-0 tw-m-0;
|
@apply tw-h-[100vh] tw-w-full tw-flex tw-p-0 tw-m-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply tw-bg-[url(@/assets/images/bg-light.png)];
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark {
|
||||||
|
@apply tw-bg-[url(@/assets/images/bg-dark.png)];
|
||||||
|
}
|
||||||
|
4
apps/admin/src/type.ts
Normal file
4
apps/admin/src/type.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface Post {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
/** @type {import("eslint").Linter.Config} */
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
|
@ -3,8 +3,6 @@
|
|||||||
"collection": "@nestjs/schematics",
|
"collection": "@nestjs/schematics",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"deleteOutDir": true,
|
"deleteOutDir": true
|
||||||
"builder": "swc",
|
|
||||||
"typeCheck": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,15 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"types": "dist/main.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/main.d.ts"
|
||||||
|
},
|
||||||
|
"./*": "./dist/*.d.ts"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cli": "nest",
|
"cli": "nest",
|
||||||
"db": "mikro-orm",
|
"db": "mikro-orm",
|
||||||
@ -23,15 +30,18 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@3rapp/common": "workspace:*",
|
||||||
"@mikro-orm/cli": "^6.2.5",
|
"@mikro-orm/cli": "^6.2.5",
|
||||||
"@mikro-orm/core": "^6.2.5",
|
"@mikro-orm/core": "^6.2.5",
|
||||||
"@mikro-orm/migrations": "^6.2.5",
|
"@mikro-orm/migrations": "^6.2.5",
|
||||||
"@mikro-orm/nestjs": "^5.2.3",
|
"@mikro-orm/nestjs": "^5.2.3",
|
||||||
"@mikro-orm/postgresql": "^6.2.5",
|
"@mikro-orm/postgresql": "^6.2.5",
|
||||||
|
"@mikro-orm/reflection": "^6.2.5",
|
||||||
"@mikro-orm/seeder": "^6.2.5",
|
"@mikro-orm/seeder": "^6.2.5",
|
||||||
"@nestjs/common": "^10.3.8",
|
"@nestjs/common": "^10.3.8",
|
||||||
"@nestjs/core": "^10.3.8",
|
"@nestjs/core": "^10.3.8",
|
||||||
"@nestjs/platform-fastify": "^10.3.8",
|
"@nestjs/platform-fastify": "^10.3.8",
|
||||||
|
"deepmerge": "^4.3.1",
|
||||||
"fastify": "^4.26.2",
|
"fastify": "^4.26.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
@ -40,6 +50,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@3rapp/code-config": "workspace:*",
|
"@3rapp/code-config": "workspace:*",
|
||||||
|
"@faker-js/faker": "^8.4.1",
|
||||||
|
"@mikro-orm/sql-highlighter": "^1.0.1",
|
||||||
"@nestjs/cli": "^10.3.2",
|
"@nestjs/cli": "^10.3.2",
|
||||||
"@nestjs/schematics": "^10.1.1",
|
"@nestjs/schematics": "^10.1.1",
|
||||||
"@nestjs/testing": "^10.3.8",
|
"@nestjs/testing": "^10.3.8",
|
||||||
|
@ -1,20 +1,14 @@
|
|||||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { database } from './config/database.config';
|
import { database } from './config/database.config';
|
||||||
import { moduleImports } from './module.list';
|
import { ContentModule } from './modules/content/content.module';
|
||||||
|
import { DatabaseModule } from './modules/database/database.module';
|
||||||
|
import { UserModule } from './modules/user/user.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [DatabaseModule.forRoot(database()), UserModule, ContentModule],
|
||||||
MikroOrmModule.forRoot({
|
|
||||||
...database(),
|
|
||||||
registerRequestContext: true,
|
|
||||||
// autoLoadEntities: true,
|
|
||||||
}),
|
|
||||||
...moduleImports,
|
|
||||||
],
|
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
|
@ -1,26 +1,16 @@
|
|||||||
import { Migrator } from '@mikro-orm/migrations';
|
import { defineConfig } from '@mikro-orm/postgresql';
|
||||||
import { defineConfig, Utils } from '@mikro-orm/postgresql';
|
|
||||||
|
|
||||||
import { SeedManager } from '@mikro-orm/seeder';
|
import { PostgreSqlOptions } from '@mikro-orm/postgresql/PostgreSqlMikroORM';
|
||||||
|
|
||||||
import { moduleImports } from '@/module.list';
|
import { getBbOptions } from '@/modules/database/helpers';
|
||||||
import { getEntities } from '@/modules/database/helpers';
|
|
||||||
|
|
||||||
export const database = () =>
|
export const database = () =>
|
||||||
defineConfig({
|
defineConfig(
|
||||||
host: '127.0.0.1',
|
getBbOptions<PostgreSqlOptions>({
|
||||||
user: 'postgres',
|
host: '127.0.0.1',
|
||||||
password: '123456',
|
user: 'postgres',
|
||||||
dbName: '3rapp',
|
password: '123456',
|
||||||
entities: getEntities(moduleImports),
|
dbName: '3rapp',
|
||||||
extensions: [Migrator, SeedManager],
|
}),
|
||||||
migrations: {
|
);
|
||||||
tableName: 'migrations',
|
|
||||||
path: Utils.detectTsNode() ? 'src/database/migrations' : 'dist/database/migrations',
|
|
||||||
},
|
|
||||||
seeder: {
|
|
||||||
path: Utils.detectTsNode() ? 'src/database/seeders' : 'dist/database/seeders',
|
|
||||||
defaultSeeder: 'DatabaseSeeder',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export default database();
|
export default database();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Migration } from '@mikro-orm/migrations';
|
import { Migration } from '@mikro-orm/migrations';
|
||||||
|
|
||||||
export class Migration20240508080237 extends Migration {
|
export class Migration20240508201511 extends Migration {
|
||||||
async up(): Promise<void> {
|
async up(): Promise<void> {
|
||||||
this.addSql(
|
this.addSql(
|
||||||
'create table "3rapp_posts" ("id" uuid not null, "title" varchar(255) not null, "body" text not null, "keywords" text[] null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "3rapp_posts_pkey" primary key ("id"));',
|
'create table "3rapp_posts" ("id" uuid not null, "title" varchar(255) not null, "body" text not null, "keywords" text[] null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "3rapp_posts_pkey" primary key ("id"));',
|
@ -1,10 +1,10 @@
|
|||||||
import type { EntityManager } from '@mikro-orm/core';
|
import type { EntityManager } from '@mikro-orm/core';
|
||||||
import { Seeder } from '@mikro-orm/seeder';
|
import { Seeder } from '@mikro-orm/seeder';
|
||||||
|
|
||||||
import { PostSeeder } from './PostSeeder';
|
import { ContentPostSeeder } from './content/PostSeeder';
|
||||||
|
|
||||||
export class DatabaseSeeder extends Seeder {
|
export class DatabaseSeeder extends Seeder {
|
||||||
async run(em: EntityManager): Promise<void> {
|
async run(em: EntityManager): Promise<void> {
|
||||||
return this.call(em, [PostSeeder]);
|
return this.call(em, [ContentPostSeeder]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import type { EntityManager } from '@mikro-orm/core';
|
|
||||||
import { Seeder } from '@mikro-orm/seeder';
|
|
||||||
|
|
||||||
export class PostSeeder extends Seeder {
|
|
||||||
async run(em: EntityManager): Promise<void> {}
|
|
||||||
}
|
|
17
apps/api/src/database/seeders/content/PostFactory.ts
Normal file
17
apps/api/src/database/seeders/content/PostFactory.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Factory } from '@mikro-orm/seeder';
|
||||||
|
|
||||||
|
import { PostEntity } from '@/modules/content/entities/post.entity';
|
||||||
|
import { getFaker } from '@/modules/database/helpers';
|
||||||
|
|
||||||
|
export class ContentPostFactory extends Factory<PostEntity> {
|
||||||
|
model = PostEntity;
|
||||||
|
|
||||||
|
definition(): Partial<PostEntity> {
|
||||||
|
const faker = getFaker();
|
||||||
|
return {
|
||||||
|
title: faker.lorem.sentence({ min: 3, max: 9 }),
|
||||||
|
body: faker.lorem.paragraphs({ min: 1, max: 5 }, '<br/>\n'),
|
||||||
|
keywords: faker.lorem.words({ min: 0, max: 6 }).split(' '),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
10
apps/api/src/database/seeders/content/PostSeeder.ts
Normal file
10
apps/api/src/database/seeders/content/PostSeeder.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { EntityManager } from '@mikro-orm/core';
|
||||||
|
import { Seeder } from '@mikro-orm/seeder';
|
||||||
|
|
||||||
|
import { ContentPostFactory } from './PostFactory';
|
||||||
|
|
||||||
|
export class ContentPostSeeder extends Seeder {
|
||||||
|
async run(em: EntityManager): Promise<void> {
|
||||||
|
new ContentPostFactory(em).create(15);
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,8 @@ async function bootstrap() {
|
|||||||
});
|
});
|
||||||
// 设置全局访问前缀
|
// 设置全局访问前缀
|
||||||
app.setGlobalPrefix('api');
|
app.setGlobalPrefix('api');
|
||||||
|
// 关闭nestjs进程自动关闭mikroorm等进程
|
||||||
|
app.enableShutdownHooks();
|
||||||
// 启动后的输出
|
// 启动后的输出
|
||||||
await app.listen(3001, () => {
|
await app.listen(3001, () => {
|
||||||
console.log();
|
console.log();
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import { ModuleMetadata } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { ContentModule } from './modules/content/content.module';
|
|
||||||
import { UserModule } from './modules/user/user.module';
|
|
||||||
|
|
||||||
export const moduleImports: ModuleMetadata['imports'] = [UserModule, ContentModule];
|
|
@ -1,11 +1,14 @@
|
|||||||
import { AnyEntity, EntityName } from '@mikro-orm/core';
|
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PostController } from './controllers/post.controller';
|
||||||
import { PostEntity } from './entities/post.entity';
|
import { PostEntity } from './entities/post.entity';
|
||||||
|
import { PostService } from './services/post.service';
|
||||||
|
|
||||||
@Module({})
|
@Module({
|
||||||
export class ContentModule {
|
imports: [MikroOrmModule.forFeature({ entities: [PostEntity] })],
|
||||||
static entities(): EntityName<AnyEntity>[] {
|
controllers: [PostController],
|
||||||
return [PostEntity];
|
providers: [PostService],
|
||||||
}
|
exports: [PostService],
|
||||||
}
|
})
|
||||||
|
export class ContentModule {}
|
||||||
|
13
apps/api/src/modules/content/controllers/post.controller.ts
Normal file
13
apps/api/src/modules/content/controllers/post.controller.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PostService } from '../services/post.service';
|
||||||
|
|
||||||
|
@Controller('posts')
|
||||||
|
export class PostController {
|
||||||
|
constructor(private postService: PostService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async list() {
|
||||||
|
return this.postService.list();
|
||||||
|
}
|
||||||
|
}
|
@ -11,10 +11,10 @@ export class PostEntity {
|
|||||||
id = v4();
|
id = v4();
|
||||||
|
|
||||||
@Property({ index: true })
|
@Property({ index: true })
|
||||||
title: string;
|
title!: string;
|
||||||
|
|
||||||
@Property({ type: 'text', lazy: true })
|
@Property({ type: 'text', lazy: true })
|
||||||
body: string;
|
body!: string;
|
||||||
|
|
||||||
@Property({ type: ArrayType, nullable: true })
|
@Property({ type: ArrayType, nullable: true })
|
||||||
keywords?: string[];
|
keywords?: string[];
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { CreateRequestContext } from '@mikro-orm/core';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { PostRepository } from '../repositories/post.repository';
|
import { PostRepository } from '../repositories/post.repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PostService {
|
export class PostService {
|
||||||
constructor(protected postRepo: PostRepository) {}
|
constructor(private readonly postRepo: PostRepository) {}
|
||||||
|
|
||||||
@CreateRequestContext<PostService>((t) => t.postRepo)
|
async list() {
|
||||||
async doSomething() {
|
return this.postRepo.findAll();
|
||||||
console.log('test');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
28
apps/api/src/modules/database/database.module.ts
Normal file
28
apps/api/src/modules/database/database.module.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { MikroORM } from '@mikro-orm/core';
|
||||||
|
import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs';
|
||||||
|
// import { SqlHighlighter } from '@mikro-orm/sql-highlighter';
|
||||||
|
import { DynamicModule, Module, OnModuleInit } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { getBbOptions } from './helpers';
|
||||||
|
|
||||||
|
@Module({})
|
||||||
|
export class DatabaseModule implements OnModuleInit {
|
||||||
|
constructor(private readonly orm: MikroORM) {}
|
||||||
|
|
||||||
|
async onModuleInit(): Promise<void> {
|
||||||
|
await this.orm.getMigrator().up();
|
||||||
|
}
|
||||||
|
|
||||||
|
static forRoot(options: MikroOrmModuleSyncOptions = {}): DynamicModule {
|
||||||
|
return {
|
||||||
|
global: true,
|
||||||
|
module: DatabaseModule,
|
||||||
|
imports: [
|
||||||
|
MikroOrmModule.forRootAsync({
|
||||||
|
useFactory: () => getBbOptions<MikroOrmModuleSyncOptions>(options),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
exports: [MikroOrmModule],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,37 @@
|
|||||||
import { ModuleMetadata } from '@nestjs/common';
|
import { deepMerge } from '@3rapp/common';
|
||||||
import { isFunction } from 'lodash';
|
import { base, zh_CN, en, Faker } from '@faker-js/faker';
|
||||||
|
import { Utils } from '@mikro-orm/core';
|
||||||
|
import { Migrator } from '@mikro-orm/migrations';
|
||||||
|
|
||||||
export const getEntities = (modules: ModuleMetadata['imports'] = []) =>
|
import { MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs';
|
||||||
modules
|
import { SeedManager } from '@mikro-orm/seeder';
|
||||||
.map((m) => ('entities' in m && isFunction(m.entities) ? m.entities() : []))
|
|
||||||
.reduce((o, n) => [...o, ...n], []);
|
export const getBbOptions = <T>(options: MikroOrmModuleSyncOptions) =>
|
||||||
export const resolveConfig = () => {};
|
deepMerge(
|
||||||
|
{
|
||||||
|
// registerRequestContext: false,
|
||||||
|
extensions: [Migrator, SeedManager],
|
||||||
|
migrations: {
|
||||||
|
disableForeignKeys: false,
|
||||||
|
tableName: 'migrations',
|
||||||
|
path: Utils.detectTsNode() ? 'src/database/migrations' : 'dist/database/migrations',
|
||||||
|
},
|
||||||
|
seeder: {
|
||||||
|
path: Utils.detectTsNode() ? 'src/database/seeders' : 'dist/database/seeders',
|
||||||
|
defaultSeeder: 'DatabaseSeeder',
|
||||||
|
},
|
||||||
|
entities: ['dist/**/*.entity.js', 'node_modules/@3rapp/**/*.entity.js'],
|
||||||
|
entitiesTs: ['src/**/*.entity.ts', 'node_modules/@3rapp/**/*.entity.ts'],
|
||||||
|
|
||||||
|
// highlighter: new SqlHighlighter(),
|
||||||
|
// metadataProvider: TsMorphMetadataProvider,
|
||||||
|
debug: true,
|
||||||
|
} as MikroOrmModuleSyncOptions,
|
||||||
|
options,
|
||||||
|
'replace',
|
||||||
|
) as T;
|
||||||
|
|
||||||
|
const faker = new Faker({
|
||||||
|
locale: [zh_CN, en, base],
|
||||||
|
});
|
||||||
|
export const getFaker = () => faker;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@3rapp/utils",
|
"name": "@3rapp/common",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
1
packages/common/src/index.ts
Normal file
1
packages/common/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './utils/tools';
|
@ -1 +0,0 @@
|
|||||||
export * from './tools';
|
|
2152
pnpm-lock.yaml
2152
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -15,6 +15,7 @@
|
|||||||
"dependsOn": ["^lint"]
|
"dependsOn": ["^lint"]
|
||||||
},
|
},
|
||||||
"dev": {
|
"dev": {
|
||||||
|
"dependsOn": ["^dev"],
|
||||||
"cache": false,
|
"cache": false,
|
||||||
"persistent": true
|
"persistent": true
|
||||||
},
|
},
|
||||||
@ -22,7 +23,7 @@
|
|||||||
"dependsOn": ["^build"]
|
"dependsOn": ["^build"]
|
||||||
},
|
},
|
||||||
"@3rapp/admin#dev": {
|
"@3rapp/admin#dev": {
|
||||||
"dependsOn": ["@3rapp/utils#build"]
|
"dependsOn": ["@3rapp/common#build"]
|
||||||
},
|
},
|
||||||
"@3rapp/api#db": {
|
"@3rapp/api#db": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
|
Loading…
Reference in New Issue
Block a user