Compare commits

...

10 Commits

Author SHA1 Message Date
pincman
d70d3fb093 init 2024-05-15 20:08:35 +08:00
pincman
ebd3a66499 update 2024-05-15 09:42:19 +08:00
pincman
a9638cfa38 update 2024-05-15 06:36:45 +08:00
pincman
112b737cc4 update 2024-05-14 19:13:18 +08:00
pincman
18d79b79a7 update 2024-05-13 09:35:26 +08:00
pincman
24f7f9524e update 2024-05-12 06:07:54 +08:00
pincman
e729426370 update 2024-05-11 05:52:36 +08:00
pincman
894d296159 update 2024-05-10 06:29:02 +08:00
pincman
8cfbfa85cb update 2024-05-09 16:26:37 +08:00
pincman
2704a9ee9e update 2024-05-09 04:34:52 +08:00
67 changed files with 2284 additions and 1235 deletions

View File

@ -1,3 +1,3 @@
module.exports = {
extends: [require.resolve('@3rapp/code-config/react')],
extends: [require.resolve('@3rapp/code-config/stylelint')],
};

View File

@ -12,15 +12,23 @@
"preview": "vite preview"
},
"dependencies": {
"@3rapp/api": "workspace:*",
"@3rapp/common": "workspace:*",
"@ant-design/cssinjs": "^1.20.0",
"antd": "^5.17.0",
"axios": "^1.6.8",
"clsx": "^2.1.1",
"deepmerge": "^4.3.1",
"immer": "^10.1.1",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-use": "^17.5.0",
"zustand": "^4.5.2"
},
"devDependencies": {
"@3rapp/code-config": "workspace:*",
"@3rapp/utils": "workspace:*",
"@types/lodash": "^4.17.1",
"@types/node": "^20.12.10",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",

View File

@ -1,4 +1,4 @@
import { pathResolve } from '@3rapp/utils';
import { pathResolve } from '@3rapp/common';
import merge from 'deepmerge';
import { ConfigEnv, UserConfig } from 'vite';
@ -19,6 +19,16 @@ export const createConfig = (params: ConfigEnv, configure?: Configure): UserConf
localsConvention: 'camelCaseOnly',
},
},
server: {
proxy: {
'/api': {
target: 'http://localhost:3001/api',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
cors: true,
},
plugins: createPlugins(isBuild),
},
typeof configure === 'function' ? configure(params, isBuild) : {},

View 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>
);
};

View File

@ -1,5 +1,5 @@
.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 {
@apply tw-shadow-md tw-p-5 tw-bg-black tw-text-center tw-text-white tw-text-lg;

View File

@ -1,48 +1,63 @@
// src/app.tsx
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 zhCN from 'antd/locale/zh_CN';
import { FC } from 'react';
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 (
<ConfigProvider
locale={zhCN}
locale={locale.antd}
theme={{
algorithm: theme.defaultAlgorithm,
token: {
colorPrimary: '#00B96B',
},
components: {
Layout: {
colorBgBody: '',
},
},
algorithm,
token: {},
}}
>
<StyleProvider hashPriority="high">
<AntdApp>
<div className={$styles.app}>
<div className={$styles.container}>
3R教室<span>React课程第一节</span>
<Button
type="primary"
className="!bg-lime-400 !text-emerald-900"
href="https://pincman.com/3r"
target="_blank"
>
</Button>
</div>
{/* <StateDemo />
<EffectDemo />
<RefDemo />
<MemoDemo />
<CallbackDemo />
<ContextDemo />
<ReducerDemo />
<CustomDemo /> */}
<Setting />
</div>
</AntdApp>
</StyleProvider>
</ConfigProvider>
);
};
const App: FC = () => (
<Locale>
<Theme>
<Wrapper />
</Theme>
</Locale>
);
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 KiB

View 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>
);
};

View 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);

View 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;

View 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;

View 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;

View 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]);
};

View 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;

View 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;

View 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;

View 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;

View 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;
}
}

View 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>;
};

View File

@ -0,0 +1,3 @@
import { LangType } from './types';
export const langs: `${LangType}`[] = ['en_US', 'zh_CN'];

View 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,
},
};

View 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]);
};

View 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;

View 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',
},
);

View 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;
}

View 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;

View 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;
}
}

View 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>;
}

View File

@ -0,0 +1,6 @@
import { ThemeState } from './types';
export const defaultThemeConfig: ThemeState = {
mode: 'light',
compact: false,
};

View 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' }), []),
};
};

View 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;

View 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 }),
});

View File

@ -0,0 +1,9 @@
export enum ThemeMode {
LIGHT = 'light',
DARK = 'dark',
}
export type ThemeState = {
mode: `${ThemeMode}`;
compact: boolean;
};

View File

@ -4,3 +4,11 @@ body,
.ant-app {
@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
View File

@ -0,0 +1,4 @@
export interface Post {
title: string;
body: string;
}

View File

@ -1,4 +1,5 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
parserOptions: {

View File

@ -3,8 +3,6 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"builder": "swc",
"typeCheck": true
"deleteOutDir": true
}
}

View File

@ -3,8 +3,15 @@
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"main": "dist/main.js",
"types": "dist/main.d.ts",
"exports": {
".": {
"import": "./dist/main.d.ts"
},
"./*": "./dist/*.d.ts"
},
"scripts": {
"cli": "nest",
"db": "mikro-orm",
@ -23,15 +30,18 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@3rapp/common": "workspace:*",
"@mikro-orm/cli": "^6.2.5",
"@mikro-orm/core": "^6.2.5",
"@mikro-orm/migrations": "^6.2.5",
"@mikro-orm/nestjs": "^5.2.3",
"@mikro-orm/postgresql": "^6.2.5",
"@mikro-orm/reflection": "^6.2.5",
"@mikro-orm/seeder": "^6.2.5",
"@nestjs/common": "^10.3.8",
"@nestjs/core": "^10.3.8",
"@nestjs/platform-fastify": "^10.3.8",
"deepmerge": "^4.3.1",
"fastify": "^4.26.2",
"lodash": "^4.17.21",
"reflect-metadata": "^0.2.2",
@ -40,6 +50,8 @@
},
"devDependencies": {
"@3rapp/code-config": "workspace:*",
"@faker-js/faker": "^8.4.1",
"@mikro-orm/sql-highlighter": "^1.0.1",
"@nestjs/cli": "^10.3.2",
"@nestjs/schematics": "^10.1.1",
"@nestjs/testing": "^10.3.8",

View File

@ -1,20 +1,14 @@
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
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({
imports: [
MikroOrmModule.forRoot({
...database(),
registerRequestContext: true,
// autoLoadEntities: true,
}),
...moduleImports,
],
imports: [DatabaseModule.forRoot(database()), UserModule, ContentModule],
controllers: [AppController],
providers: [AppService],
})

View File

@ -1,26 +1,16 @@
import { Migrator } from '@mikro-orm/migrations';
import { defineConfig, Utils } from '@mikro-orm/postgresql';
import { defineConfig } from '@mikro-orm/postgresql';
import { SeedManager } from '@mikro-orm/seeder';
import { PostgreSqlOptions } from '@mikro-orm/postgresql/PostgreSqlMikroORM';
import { moduleImports } from '@/module.list';
import { getEntities } from '@/modules/database/helpers';
import { getBbOptions } from '@/modules/database/helpers';
export const database = () =>
defineConfig({
host: '127.0.0.1',
user: 'postgres',
password: '123456',
dbName: '3rapp',
entities: getEntities(moduleImports),
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',
},
});
defineConfig(
getBbOptions<PostgreSqlOptions>({
host: '127.0.0.1',
user: 'postgres',
password: '123456',
dbName: '3rapp',
}),
);
export default database();

View File

@ -1,6 +1,6 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20240508080237 extends Migration {
export class Migration20240508201511 extends Migration {
async up(): Promise<void> {
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"));',

View File

@ -1,10 +1,10 @@
import type { EntityManager } from '@mikro-orm/core';
import { Seeder } from '@mikro-orm/seeder';
import { PostSeeder } from './PostSeeder';
import { ContentPostSeeder } from './content/PostSeeder';
export class DatabaseSeeder extends Seeder {
async run(em: EntityManager): Promise<void> {
return this.call(em, [PostSeeder]);
return this.call(em, [ContentPostSeeder]);
}
}

View File

@ -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> {}
}

View 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(' '),
};
}
}

View 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);
}
}

View File

@ -14,6 +14,8 @@ async function bootstrap() {
});
// 设置全局访问前缀
app.setGlobalPrefix('api');
// 关闭nestjs进程自动关闭mikroorm等进程
app.enableShutdownHooks();
// 启动后的输出
await app.listen(3001, () => {
console.log();

View File

@ -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];

View File

@ -1,11 +1,14 @@
import { AnyEntity, EntityName } from '@mikro-orm/core';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Module } from '@nestjs/common';
import { PostController } from './controllers/post.controller';
import { PostEntity } from './entities/post.entity';
import { PostService } from './services/post.service';
@Module({})
export class ContentModule {
static entities(): EntityName<AnyEntity>[] {
return [PostEntity];
}
}
@Module({
imports: [MikroOrmModule.forFeature({ entities: [PostEntity] })],
controllers: [PostController],
providers: [PostService],
exports: [PostService],
})
export class ContentModule {}

View 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();
}
}

View File

@ -11,10 +11,10 @@ export class PostEntity {
id = v4();
@Property({ index: true })
title: string;
title!: string;
@Property({ type: 'text', lazy: true })
body: string;
body!: string;
@Property({ type: ArrayType, nullable: true })
keywords?: string[];

View File

@ -1,14 +1,12 @@
import { CreateRequestContext } from '@mikro-orm/core';
import { Injectable } from '@nestjs/common';
import { PostRepository } from '../repositories/post.repository';
@Injectable()
export class PostService {
constructor(protected postRepo: PostRepository) {}
constructor(private readonly postRepo: PostRepository) {}
@CreateRequestContext<PostService>((t) => t.postRepo)
async doSomething() {
console.log('test');
async list() {
return this.postRepo.findAll();
}
}

View 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],
};
}
}

View File

@ -1,8 +1,37 @@
import { ModuleMetadata } from '@nestjs/common';
import { isFunction } from 'lodash';
import { deepMerge } from '@3rapp/common';
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'] = []) =>
modules
.map((m) => ('entities' in m && isFunction(m.entities) ? m.entities() : []))
.reduce((o, n) => [...o, ...n], []);
export const resolveConfig = () => {};
import { MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs';
import { SeedManager } from '@mikro-orm/seeder';
export const getBbOptions = <T>(options: MikroOrmModuleSyncOptions) =>
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;

View File

@ -1,5 +1,5 @@
{
"name": "@3rapp/utils",
"name": "@3rapp/common",
"version": "0.0.0",
"files": [
"dist"

View File

@ -0,0 +1 @@
export * from './utils/tools';

View File

@ -1 +0,0 @@
export * from './tools';

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@
"dependsOn": ["^lint"]
},
"dev": {
"dependsOn": ["^dev"],
"cache": false,
"persistent": true
},
@ -22,7 +23,7 @@
"dependsOn": ["^build"]
},
"@3rapp/admin#dev": {
"dependsOn": ["@3rapp/utils#build"]
"dependsOn": ["@3rapp/common#build"]
},
"@3rapp/api#db": {
"cache": false,