[];
49 | /** 权限列表 */
50 | permissions?: Permission[];
51 | },
52 | T
53 | >;
54 | /**
55 | * 当前账户状态类型
56 | */
57 | export type AuthStoreType = {
58 | /** 是否已经初始化Token */
59 | inited: boolean;
60 | /** Token */
61 | token: null | string;
62 | /** 用户信息 */
63 | user: User | null;
64 | };
65 |
--------------------------------------------------------------------------------
/src/components/Chart/_default.config.ts:
--------------------------------------------------------------------------------
1 | import {
2 | TitleComponent,
3 | TooltipComponent,
4 | GridComponent,
5 | DatasetComponent,
6 | TransformComponent,
7 | } from 'echarts/components';
8 |
9 | import type { ChartState } from './types';
10 |
11 | export const getDefaultChartConfig = (): ChartState => ({
12 | render: 'canvas',
13 | exts: [TitleComponent, TooltipComponent, GridComponent, DatasetComponent, TransformComponent],
14 | height: '300px',
15 | width: 'auto',
16 | });
17 |
--------------------------------------------------------------------------------
/src/components/Chart/hooks.ts:
--------------------------------------------------------------------------------
1 | import { deepMerge, useStoreSetuped } from '@/utils';
2 |
3 | import { ChartSetup, ChartStore } from './store';
4 |
5 | import type { ChartConfig } from './types';
6 |
7 | export const useSetupChart = (config?: ChartConfig) => {
8 | useStoreSetuped({
9 | store: ChartSetup,
10 | callback: () => {
11 | ChartStore.setState((state) =>
12 | deepMerge(state, {
13 | ...config,
14 | exts: config?.exts?.filter((e) => !state.exts.includes(e)) ?? [],
15 | }),
16 | );
17 | },
18 | });
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/Chart/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 | export * from './hooks';
3 | export * from './chart';
4 | export * from './store';
5 | export * from './components/PercentGaugeChart';
6 |
--------------------------------------------------------------------------------
/src/components/Chart/store.ts:
--------------------------------------------------------------------------------
1 | import create from 'zustand';
2 |
3 | import { createStore } from '@/utils';
4 |
5 | import type { ChartState } from './types';
6 | import { getDefaultChartConfig } from './_default.config';
7 |
8 | export const ChartSetup = create<{ setuped?: true }>(() => ({}));
9 |
10 | export const ChartStore = createStore(() => getDefaultChartConfig());
11 |
--------------------------------------------------------------------------------
/src/components/Chart/types.ts:
--------------------------------------------------------------------------------
1 | import type * as echarts from 'echarts/core';
2 | import type { ECBasicOption } from 'echarts/types/dist/shared';
3 | import type { CSSProperties } from 'react';
4 | import type { GaugeSeriesOption } from 'echarts/charts';
5 |
6 | export type EChartExt = ArrayItem[0], Array>>;
7 | export interface ChartConfig {
8 | render?: 'svg' | 'canvas';
9 | exts?: Array;
10 | height?: string;
11 | width?: string;
12 | }
13 | export interface ChartState extends Required {}
14 | export interface ChartLoading {
15 | type?: string;
16 | show?: boolean;
17 | text?: string;
18 | color?: string;
19 | textColor?: string;
20 | maskColor?: string;
21 | zlevel?: number;
22 | fontSize?: number;
23 | showSpinner?: true;
24 | spinnerRadius?: number;
25 | lineWidth?: number;
26 | fontWeight?: string;
27 | fontStyle?: string;
28 | fontFamily?: string;
29 | }
30 | export interface ChartProps extends ChartConfig {
31 | options: T;
32 | loading?: ChartLoading;
33 | className?: string;
34 | style?: CSSProperties;
35 | }
36 | export type GaugeChartProps = {
37 | config?: Omit & {
38 | click?: (chart: echarts.ECharts) => void;
39 | };
40 | style?: CSSProperties;
41 | loading?: ChartLoading;
42 | data: NonNullable;
43 | };
44 |
--------------------------------------------------------------------------------
/src/components/Config/_default.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2021-12-29 11:55:02 +0800
6 | * @Updated_at : 2022-01-11 14:30:55 +0800
7 | * @Path : /src/components/Config/_default.config.ts
8 | * @Description : 默认配置
9 | * @LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 |
14 | import { ConfigStoreType } from './types';
15 |
16 | export const defaultConfig: ConfigStoreType['config'] = {
17 | timezone: 'UTC',
18 | isAntd: true,
19 | theme: {
20 | mode: 'light',
21 | depend: 'manual',
22 | range: {
23 | light: '07:30',
24 | dark: '18:30',
25 | },
26 | darken: {
27 | theme: {
28 | brightness: 100,
29 | contrast: 90,
30 | sepia: 10,
31 | },
32 | fixes: {
33 | invert: [],
34 | css: '',
35 | ignoreInlineStyle: [],
36 | ignoreImageAnalysis: [],
37 | disableStyleSheetsProxy: true,
38 | },
39 | },
40 | },
41 | colors: {
42 | primary: '#1890ff',
43 | info: '#00adb5',
44 | success: '#52c41a',
45 | error: '#ff4d4f',
46 | warning: '#faad14',
47 | },
48 | // layout: {
49 | // mode: 'side',
50 | // collapsed: false,
51 | // theme: {
52 | // header: 'light',
53 | // sidebar: 'dark',
54 | // embed: 'light',
55 | // },
56 | // fixed: {
57 | // header: false,
58 | // sidebar: false,
59 | // embed: false,
60 | // },
61 | // },
62 | };
63 |
--------------------------------------------------------------------------------
/src/components/Config/constants.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2022-01-02 18:30:16 +0800
6 | * @Updated_at : 2022-01-11 14:19:47 +0800
7 | * @Path : /src/components/Config/constants.ts
8 | * @Description : 配置组件常量
9 | * @LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 | /**
14 | * 主题切换依赖
15 | * 注意OS和TIME与手动切换并不冲突,但OS与TIME两者只能选择一个
16 | */
17 | export enum ThemeDepend {
18 | /** 跟随操作系统 */
19 | OS = 'os',
20 | /** 跟随时间范围 */
21 | TIME = 'time',
22 | /** 只能手动切换 */
23 | MANUAL = 'manual',
24 | }
25 | /**
26 | * 主题模式
27 | */
28 | export enum ThemeMode {
29 | /** 明亮 */
30 | LIGHT = 'light',
31 | /** 暗黑 */
32 | DARK = 'dark',
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Config/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 | export * from './setup';
3 | export * from './hooks';
4 | export * from './store';
5 | export * from './constants';
6 |
--------------------------------------------------------------------------------
/src/components/Config/store.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2021-12-29 11:55:02 +0800
6 | * @Updated_at : 2022-01-16 00:29:58 +0800
7 | * @Path : /src/components/Config/store.ts
8 | * @Description : 配置组件状态池
9 | * @LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 | import { createStore } from '@/utils';
14 |
15 | import { ConfigStoreType } from './types';
16 | import { defaultConfig } from './_default.config';
17 | /**
18 | * 配置组件初始化状态池
19 | */
20 | export const ConfigSetup = createStore<{ setuped?: true }>(() => ({}));
21 | /**
22 | * 配置组件状态池
23 | */
24 | export const ConfigStore = createStore(() => ({
25 | config: defaultConfig,
26 | watchers: {},
27 | }));
28 |
--------------------------------------------------------------------------------
/src/components/Config/types.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2021-12-29 11:55:02 +0800
6 | * @Updated_at : 2022-01-11 14:30:14 +0800
7 | * @Path : /src/components/Config/types.ts
8 | * @Description : 配置组件类型
9 | * @LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 | import DarkReader from 'darkreader';
14 |
15 | import { ThemeDepend, ThemeMode } from './constants';
16 |
17 | /**
18 | * 配置组件参数选项
19 | */
20 | export interface ConfigProps {
21 | /** 默认时区 */
22 | timezone?: string;
23 | /** 主题配置 */
24 | theme?: ThemeConfig;
25 | /** 色系配置 */
26 | colors?: ColorConfig;
27 | /**
28 | * 临时变量,是否使用antd组件
29 | * 由于antd下暂时不支持动态暗黑而采用dark-reader
30 | * 但是arco,tdesign等都支持
31 | * 有了这个变量在后面开发其它组件库的面板时切换暗黑模式时就可以做判断了
32 | */
33 | isAntd?: boolean;
34 | }
35 | /**
36 | * 配置组件状态池
37 | */
38 | export interface ConfigStoreType {
39 | /** 配置状态 */
40 | config: ReRequired> & {
41 | /** 主题配置 */
42 | theme: ReRequired> & {
43 | /** DarkReader配置 */
44 | darken?: DarkReaderConfig;
45 | };
46 | };
47 | /** 监听器 */
48 | watchers: {
49 | /** 主题切换依赖监听器 */
50 | theme?: NodeJS.Timeout;
51 | };
52 | }
53 | /**
54 | * 主题配置
55 | */
56 | export interface ThemeConfig {
57 | /** 主题模式 */
58 | mode?: `${ThemeMode}`;
59 | /** 主题依赖,注意OS和TIME与手动切换并不冲突,但OS与TIME两者只能选择一个 */
60 | depend?: `${ThemeDepend}`;
61 | /** 切换时间 */
62 | range?: Partial;
63 | /** DarkRender配置 */
64 | darken?: DarkReaderConfig;
65 | }
66 | /**
67 | * 色系配置
68 | */
69 | export interface ColorConfig {
70 | /** 主色 */
71 | primary?: string;
72 | /** 信息色 */
73 | info?: string;
74 | /** 成功色 */
75 | success?: string;
76 | /** 错误色 */
77 | error?: string;
78 | /** 警告色 */
79 | warning?: string;
80 | }
81 |
82 | /**
83 | * 主题切换时间范围
84 | */
85 | export type ThemeTimeRange = { [key in `${ThemeMode}`]: string };
86 | /**
87 | * DarkReader配置
88 | */
89 | export interface DarkReaderConfig {
90 | theme?: Partial;
91 | fixes?: Partial;
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/Fetcher/index.ts:
--------------------------------------------------------------------------------
1 | export * from './hooks';
2 | export * from './store';
3 | export * from './types';
4 | export * from './provider';
5 |
--------------------------------------------------------------------------------
/src/components/Fetcher/provider.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Author : pincman
3 | * HomePage : https://pincman.com
4 | * Support : support@pincman.com
5 | * Created_at : 2021-12-25 07:26:27 +0800
6 | * Updated_at : 2022-01-10 10:15:56 +0800
7 | * Path : /src/components/Fetcher/provider.tsx
8 | * Description : SWR包装器
9 | * LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 | import type { AxiosRequestConfig } from 'axios';
14 | import { useEffect } from 'react';
15 | import { SWRConfig } from 'swr';
16 |
17 | import { deepMerge } from '@/utils';
18 |
19 | import { useFetcher } from './hooks';
20 |
21 | import { FetcherStore } from './store';
22 | /**
23 | * SWR包装器,如果要使用swr功能请使用此组件包裹根组件
24 | * @param props
25 | */
26 | export const SWRFetcher: FC = ({ children }) => {
27 | const swr = FetcherStore((state) => state.swr);
28 | const fetcher = useFetcher();
29 | useEffect(() => {
30 | FetcherStore.setState((state) => {
31 | state.swr = {
32 | ...(state.swr ?? {}),
33 | fetcher: async (
34 | resource: string | AxiosRequestConfig,
35 | options?: AxiosRequestConfig,
36 | ) => {
37 | let config: AxiosRequestConfig = options ?? {};
38 | if (typeof resource === 'string') config.url = resource;
39 | else config = deepMerge(config, resource, 'replace');
40 | const res = await fetcher.request({ ...config, method: 'get' });
41 | await new Promise((resolve) => {
42 | setTimeout(resolve, 3000);
43 | });
44 | return res.data;
45 | },
46 | };
47 | });
48 | }, [fetcher]);
49 | return {children};
50 | };
51 |
--------------------------------------------------------------------------------
/src/components/Fetcher/store.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2021-12-25 05:01:46 +0800
6 | * @Updated_at : 2022-01-16 00:29:34 +0800
7 | * @Path : /src/components/Fetcher/store.ts
8 | * @Description : Fetcher组件状态池
9 | * @LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 | import create from 'zustand';
14 |
15 | import { createStore } from '@/utils';
16 |
17 | import { FetcherStoreType } from './types';
18 | /**
19 | * Fetcher组件初始化状态
20 | */
21 | export const FetcherSetup = create<{ setuped?: true }>(() => ({}));
22 | /**
23 | * Fetcher组件状态池
24 | */
25 | export const FetcherStore = createStore(() => ({
26 | axios: {},
27 | swr: {},
28 | }));
29 |
--------------------------------------------------------------------------------
/src/components/Fetcher/types.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2021-12-14 00:07:50 +0800
6 | * @Updated_at : 2022-01-10 10:15:17 +0800
7 | * @Path : /src/components/Fetcher/types.ts
8 | * @Description : Fetcher组件类型
9 | * @LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 | import type { AxiosInterceptorManager, AxiosRequestConfig, AxiosResponse } from 'axios';
14 | import type { BareFetcher, PublicConfiguration } from 'swr/dist/types';
15 | /**
16 | * Fetcher配置
17 | */
18 | export interface FetcherConfig extends AxiosRequestConfig, FetchOption {}
19 | /**
20 | * swrjs配置
21 | */
22 | export interface SwrConfig
23 | extends Partial>>> {}
24 |
25 | /**
26 | * Fetcher组件状态池
27 | */
28 | export interface FetcherStoreType {
29 | axios: FetcherConfig;
30 | swr: SwrConfig;
31 | }
32 | /**
33 | * 自定义选项参数
34 | */
35 | export interface FetchOption {
36 | /** 当前账户验证token */
37 | token?: string | null;
38 | /** 响应后设置token的函数 */
39 | setToken?: (token: string) => Promise;
40 | /** 响应式清除token的函数 */
41 | clearToken?: () => Promise;
42 | /** 是否禁止重复请求 */
43 | cancel_repeat?: boolean;
44 | /** 自定义axios请求和响应函数 */
45 | interceptors?: {
46 | request?: (
47 | req: AxiosInterceptorManager,
48 | ) => AxiosInterceptorManager;
49 | response?: (
50 | res: AxiosInterceptorManager,
51 | ) => AxiosInterceptorManager;
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/Icon/_default.config.ts:
--------------------------------------------------------------------------------
1 | import type { IconState } from './types';
2 |
3 | export const getDefaultIconConfig = (): IconState => ({
4 | size: 16,
5 | style: {},
6 | classes: [],
7 | prefix: { svg: 'svg', iconfont: 'icon' },
8 | });
9 |
--------------------------------------------------------------------------------
/src/components/Icon/constants.ts:
--------------------------------------------------------------------------------
1 | export enum IconPrefixType {
2 | SVG = 'svg',
3 | ICONFONT = 'if',
4 | IONIFY = 'fy',
5 | }
6 | export enum IconType {
7 | SVG = 'svg',
8 | ICONFONT = 'iconfont',
9 | IONIFY = 'iconify',
10 | COMPONENT = 'component',
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/Icon/hooks.tsx:
--------------------------------------------------------------------------------
1 | import { createFromIconfontCN } from '@ant-design/icons';
2 | import { omit } from 'lodash-es';
3 |
4 | import { useStoreSetuped, deepMerge } from '@/utils';
5 |
6 | import type { IconComputed, IconConfig, IconProps } from './types';
7 | import type { IconType } from './constants';
8 | import { IconSetup, IconStore } from './store';
9 |
10 | export const useSetupIcon = (config?: IconConfig) => {
11 | useStoreSetuped({
12 | store: IconSetup,
13 | callback: () => {
14 | const options: IconConfig = config ?? {};
15 | IconStore.setState((state) => {
16 | const newState = deepMerge(state, omit(config, ['iconfont']) as any);
17 | if (options.iconfont_urls) {
18 | newState.iconfont = createFromIconfontCN({
19 | scriptUrl: options.iconfont_urls,
20 | });
21 | }
22 | return newState;
23 | });
24 | },
25 | });
26 | };
27 | export const useIcon = (args: IconProps) => {
28 | const config = IconStore((state) => ({ ...state }));
29 | const params = omit(config, ['size', 'prefix', 'classes', 'iconfont_urls']);
30 | const csize = typeof config.size === 'number' ? `${config.size}px` : config.size;
31 | const style = { fontSize: args.style?.fontSize ?? csize, ...(args.style ?? {}) };
32 | const className = [...config.classes, args.className];
33 | if ('component' in args) {
34 | const result = deepMerge(params, {
35 | ...args,
36 | type: 'component',
37 | style,
38 | className,
39 | });
40 | return omit(result, ['iconfont']) as IconComputed;
41 | }
42 | let name: string;
43 | let type: `${IconType}` = 'svg';
44 | const [prefix, ...names] = args.name.split(':');
45 | if (prefix === 'if') {
46 | name = `${config.prefix.iconfont}-${names.join(':')}`;
47 | type = 'iconfont';
48 | } else if (prefix === 'fy') {
49 | name = names.join(':');
50 | type = 'iconify';
51 | } else {
52 | name = `${config.prefix.svg}-${names.join(':')}`;
53 | type = 'svg';
54 | }
55 | const result = deepMerge(config, {
56 | ...args,
57 | name,
58 | type,
59 | style,
60 | className,
61 | });
62 | return (prefix !== 'if' ? omit(result, ['iconfont']) : result) as IconComputed;
63 | };
64 |
--------------------------------------------------------------------------------
/src/components/Icon/icon.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import AntdIcon from '@ant-design/icons';
3 | import { Icon as Iconify } from '@iconify/react';
4 |
5 | import produce from 'immer';
6 |
7 | import classNames from 'classnames';
8 |
9 | import { IconType } from './constants';
10 | import { useIcon } from './hooks';
11 | import type { IconComputed, IconProps } from './types';
12 | import { IconSetup } from './store';
13 |
14 | const getAntdSvgIcon = ({ config }: { config: IconComputed }) => {
15 | if ('component' in config) {
16 | const { component, spin, rotate, className, ...rest } = config;
17 | return config.component({ className: classNames(className), ...rest });
18 | }
19 | const { name, iconfont, className, inline, type, spin, rotate, ...rest } = config;
20 | return type === IconType.IONIFY ? (
21 |
22 | ) : (
23 |
26 | );
27 | };
28 | const Icon = (props: IconProps) => {
29 | const config = useIcon(props);
30 | const isSetuped = IconSetup((state) => state.setuped);
31 | const [setuped, setSetuped] = useState(isSetuped);
32 | useEffect(() => {
33 | setSetuped(isSetuped);
34 | }, [isSetuped]);
35 | if (!setuped) return null;
36 | if ('type' in config && config.iconfont && config.type === IconType.ICONFONT) {
37 | const { name, iconfont: FontIcon, inline, className, type, ...rest } = config;
38 | return ;
39 | }
40 | const options = produce(config, (draft) => {
41 | if (draft.spin) draft.className.push('anticon-spin');
42 | if (draft.rotate) {
43 | draft.style.transform = draft.style.transform
44 | ? `${draft.style.transform} rotate(${draft.rotate}deg)`
45 | : `rotate(${draft.rotate}deg)`;
46 | }
47 | });
48 | return (
49 | getAntdSvgIcon({ config: options })}
52 | />
53 | );
54 | };
55 | export default Icon;
56 |
--------------------------------------------------------------------------------
/src/components/Icon/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Icon } from './icon';
2 | export * from './hooks';
3 | export * from './types';
4 | export * from './constants';
5 | export * from './store';
6 |
--------------------------------------------------------------------------------
/src/components/Icon/store.ts:
--------------------------------------------------------------------------------
1 | import create from 'zustand';
2 |
3 | import { createStore } from '@/utils';
4 |
5 | import { getDefaultIconConfig } from './_default.config';
6 | import type { IconState } from './types';
7 |
8 | export const IconSetup = create<{ setuped?: true }>(() => ({}));
9 |
10 | export const IconStore = createStore(() => getDefaultIconConfig());
11 |
--------------------------------------------------------------------------------
/src/components/Icon/types.ts:
--------------------------------------------------------------------------------
1 | import type { IconFontProps as DefaultIconFontProps } from '@ant-design/icons/lib/components/IconFont';
2 | // import type { IconProps as IconifyIconProps } from '@iconify/react';
3 | import type { CSSProperties, FC, RefAttributes, SVGProps } from 'react';
4 |
5 | import type { IconPrefixType, IconType } from './constants';
6 |
7 | export type IconName = `${IconPrefixType}:${string}`;
8 | export type IconComponent = FC;
9 | export type IconConfig = RecordScalable<
10 | {
11 | size?: number | string;
12 | classes?: string[];
13 | style?: CSSProperties;
14 | prefix?: { svg?: string; iconfont?: string };
15 | iconfont_urls?: string | string[];
16 | },
17 | T
18 | >;
19 | export type IconState = RecordScalable<
20 | Required> & {
21 | iconfont?: FC>;
22 | },
23 | T
24 | >;
25 | export type IconComputed = {
26 | spin?: boolean;
27 | rotate?: number;
28 | className: string[];
29 | style: CSSProperties;
30 | } & (
31 | | {
32 | name: string;
33 | type: `${IconType}`;
34 | inline?: boolean;
35 | iconfont?: FC>;
36 | }
37 | | {
38 | component: FC;
39 | }
40 | );
41 | export interface BaseIconProps extends Omit {
42 | className?: string;
43 | spin?: boolean;
44 | rotate?: number;
45 | }
46 | export interface SvgProps extends BaseIconProps {
47 | name: IconName;
48 | component?: never;
49 | inline?: boolean;
50 | }
51 | export interface ComponentProps extends BaseIconProps {
52 | name?: never;
53 | component: IconComponent;
54 | }
55 |
56 | export type IconProps = SvgProps | ComponentProps;
57 |
58 | type BaseElementProps = RefAttributes & SVGProps;
59 |
--------------------------------------------------------------------------------
/src/components/KeepAlive/constants.ts:
--------------------------------------------------------------------------------
1 | import { createContext, Dispatch } from 'react';
2 |
3 | import { KeepAliveAction } from './types';
4 |
5 | export enum AliveActionType {
6 | REMOVE = 'remove',
7 | REMOVE_MULTI = 'remove_multi',
8 | ADD = 'add',
9 | CLEAR = 'clear',
10 | ACTIVE = 'active',
11 | CHANGE = 'change',
12 | RESET = 'reset',
13 | }
14 | export const KeepAliveIdContext = createContext(null);
15 | export const KeepAliveDispatchContext = createContext | null>(null);
16 |
--------------------------------------------------------------------------------
/src/components/KeepAlive/hooks.tsx:
--------------------------------------------------------------------------------
1 | import { useUnmount } from 'react-use';
2 |
3 | import { useCallback } from 'react';
4 |
5 | import { isNil } from 'ramda';
6 |
7 | import { deepMerge, useStoreSetuped } from '@/utils';
8 |
9 | import { useNavigator } from '../Router';
10 |
11 | import { KeepAliveSetup, KeepAliveStore } from './store';
12 |
13 | import { KeepAliveConfig } from './types';
14 | import { AliveActionType } from './constants';
15 |
16 | export const useSetupKeepAlive = (config: KeepAliveConfig) => {
17 | useStoreSetuped({
18 | store: KeepAliveSetup,
19 | callback: () => {
20 | KeepAliveStore.setState((state) => deepMerge(state, config, 'replace'), true);
21 | },
22 | });
23 | const listenLives = KeepAliveStore.subscribe(
24 | (state) => state.lives,
25 | (lives) => {
26 | KeepAliveStore.setState((state) => {
27 | state.include = lives;
28 | });
29 | },
30 | );
31 | useUnmount(() => {
32 | listenLives();
33 | });
34 | };
35 | export const useActivedAlive = () => KeepAliveStore(useCallback((state) => state.active, []));
36 | export const useKeepAlives = () => KeepAliveStore(useCallback((state) => state.lives, []));
37 | export const useKeepAliveDispath = () => {
38 | const navigate = useNavigator();
39 | const removeAlive = useCallback(
40 | (id: string) => {
41 | KeepAliveStore.dispatch({
42 | type: AliveActionType.REMOVE,
43 | params: { id, navigate },
44 | });
45 | },
46 | [navigate],
47 | );
48 | const removeAlives = useCallback(
49 | (ids: string[]) => {
50 | KeepAliveStore.dispatch({
51 | type: AliveActionType.REMOVE_MULTI,
52 | params: { ids, navigate },
53 | });
54 | },
55 | [navigate],
56 | );
57 |
58 | const changeAlive = useCallback(
59 | (id: string) => {
60 | KeepAliveStore.dispatch({
61 | type: AliveActionType.CHANGE,
62 | params: { id, navigate },
63 | });
64 | },
65 | [navigate],
66 | );
67 | const clearAlives = useCallback(() => {
68 | KeepAliveStore.dispatch({
69 | type: AliveActionType.CLEAR,
70 | navigate,
71 | });
72 | }, [navigate]);
73 | const refreshAlive = useCallback(
74 | (id: string | null) => {
75 | KeepAliveStore.dispatch({
76 | type: AliveActionType.RESET,
77 | params: { id, navigate },
78 | });
79 | if (!isNil(id) && navigate) navigate({ id });
80 | },
81 | [navigate],
82 | );
83 | return { changeAlive, removeAlive, removeAlives, clearAlives, refreshAlive };
84 | };
85 |
--------------------------------------------------------------------------------
/src/components/KeepAlive/index.ts:
--------------------------------------------------------------------------------
1 | export * from './constants';
2 | export * from './view';
3 | export * from './hooks';
4 | export * from './store';
5 |
--------------------------------------------------------------------------------
/src/components/KeepAlive/store.ts:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 | import { equals, filter, find, findIndex, includes, not } from 'ramda';
3 | import { Reducer } from 'react';
4 |
5 | import { createReduxStore, createStore } from '@/utils';
6 |
7 | import { AliveActionType } from './constants';
8 |
9 | import { KeepAliveAction, KeepAliveStoreType } from './types';
10 |
11 | const keepAliveReducer: Reducer = produce((state, action) => {
12 | switch (action.type) {
13 | case AliveActionType.ADD: {
14 | const lives = [...state.lives];
15 | if (lives.some((item) => item === action.id && state.active === action.id)) return;
16 | const isNew = lives.filter((item) => item === action.id).length < 1;
17 | if (isNew) {
18 | if (lives.length >= state.maxLen) state.lives.shift();
19 | state.lives.push(action.id);
20 | state.active = action.id;
21 | }
22 | break;
23 | }
24 | case AliveActionType.REMOVE: {
25 | const { id, navigate } = action.params;
26 | const index = findIndex((item) => item === id, state.lives);
27 | if (equals(index, -1)) return;
28 | const toRemove = state.lives[index];
29 | state.lives.splice(index, 1);
30 | if (state.active === toRemove) {
31 | if (state.lives.length < 1) {
32 | navigate(state.path);
33 | } else {
34 | const toActiveIndex = index > 0 ? index - 1 : index;
35 | state.active = state.lives[toActiveIndex];
36 | navigate({ id: state.active });
37 | }
38 | }
39 | break;
40 | }
41 | case AliveActionType.REMOVE_MULTI: {
42 | const { ids, navigate } = action.params;
43 | state.lives = filter((item) => not(includes(item, ids)), state.lives);
44 | if (state.lives.length < 1) navigate(state.path);
45 | break;
46 | }
47 | case AliveActionType.CLEAR: {
48 | state.lives = [];
49 | action.navigate(state.path);
50 | break;
51 | }
52 | case AliveActionType.ACTIVE: {
53 | const current = find((item) => item === action.id, state.lives);
54 | if (current && state.active !== current) state.active = current;
55 | break;
56 | }
57 | case AliveActionType.CHANGE: {
58 | const { id, navigate } = action.params;
59 | const current = find((item) => item === id, state.lives);
60 | if (!current || state.active === id) return;
61 | navigate({ id });
62 | break;
63 | }
64 | case AliveActionType.RESET: {
65 | const { id, navigate } = action.params;
66 | state.reset = id;
67 | break;
68 | }
69 | default:
70 | break;
71 | }
72 | });
73 |
74 | export const KeepAliveSetup = createStore<{ setuped?: true; generated?: true }>(() => ({}));
75 |
76 | export const KeepAliveStore = createReduxStore(keepAliveReducer, {
77 | path: '/',
78 | active: null,
79 | include: [],
80 | exclude: [],
81 | maxLen: 10,
82 | notFound: '/errors/404',
83 | lives: [],
84 | reset: null,
85 | });
86 |
--------------------------------------------------------------------------------
/src/components/KeepAlive/types.ts:
--------------------------------------------------------------------------------
1 | import { RefObject } from 'react';
2 |
3 | import { RouteNavigator, RouteOption } from '../Router';
4 |
5 | import { AliveActionType } from './constants';
6 |
7 | export type KeepAliveRouteOption = RouteOption<{ id: string }>;
8 |
9 | export interface KeepAliveConfig {
10 | path?: string;
11 | active?: string | null;
12 | exclude?: Array;
13 | maxLen?: number;
14 | notFound?: string;
15 | }
16 |
17 | export interface KeepAliveStoreType extends Required {
18 | include?: Array; // 是否异步添加 Include 如果不是又填写了 true 会导致重复渲染
19 | lives: string[];
20 | reset: string | null;
21 | }
22 | export interface AlivePageProps {
23 | isActive: boolean;
24 | id: string;
25 | renderDiv: RefObject;
26 | }
27 |
28 | export type KeepAliveAction =
29 | | {
30 | type: AliveActionType.REMOVE;
31 | params: {
32 | id: string;
33 | navigate: RouteNavigator;
34 | };
35 | }
36 | | {
37 | type: AliveActionType.REMOVE_MULTI;
38 | params: {
39 | ids: string[];
40 | navigate: RouteNavigator;
41 | };
42 | }
43 | | {
44 | type: AliveActionType.ADD;
45 | id: string;
46 | }
47 | | {
48 | type: AliveActionType.ACTIVE;
49 | id: string;
50 | }
51 | | {
52 | type: AliveActionType.CHANGE;
53 | params: {
54 | id: string;
55 | navigate: RouteNavigator;
56 | };
57 | }
58 | | {
59 | type: AliveActionType.CLEAR;
60 | navigate: RouteNavigator;
61 | }
62 | | {
63 | type: AliveActionType.RESET;
64 | params: {
65 | id: string | null;
66 | navigate?: RouteNavigator;
67 | };
68 | };
69 |
--------------------------------------------------------------------------------
/src/components/Layout/constants.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 布局模式
3 | */
4 | export enum LayoutMode {
5 | /** 只有顶栏导航 */
6 | TOP = 'top',
7 | /** 侧边导航,顶栏自定义 */
8 | SIDE = 'side',
9 | /** 同side,但是LOGO在顶栏 */
10 | CONTENT = 'content',
11 | /** 内嵌双导航,侧边折叠 */
12 | EMBED = 'embed',
13 | }
14 | /**
15 | * 布局组件
16 | */
17 | export enum LayoutComponent {
18 | /** 顶栏 */
19 | HEADER = 'header',
20 | /** 侧边栏 */
21 | SIDEBAR = 'sidebar',
22 | /** 内嵌导航,只在mode为embed时生效 */
23 | EMBED = 'embed',
24 | }
25 | export enum LayoutActionType {
26 | /** 更改组件固定 */
27 | CHANGE_FIXED = 'change_fixed',
28 | /** 更改CSS变量 */
29 | CHANGE_VARS = 'change_vars',
30 | /** 更改布局模式 */
31 | CHANGE_MODE = 'change_mode',
32 | /** 重置菜单 */
33 | CHANGE_MENU = 'change_menu',
34 | /** 更改组件主题 */
35 | CHANGE_THEME = 'change_theme',
36 | /** 更改侧边缩进 */
37 | CHANGE_COLLAPSE = 'change_collapse',
38 | /** 反转侧边缩进 */
39 | TOGGLE_COLLAPSE = 'toggle_collapse',
40 | /** 更改移动模式下的侧边缩进 */
41 | CHANGE_MOBILE_SIDE = 'change_mobile_side',
42 | /** 反转移动模式下的侧边缩进 */
43 | TOGGLE_MOBILE_SIDE = 'toggle_mobile_side',
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/Layout/default.config.ts:
--------------------------------------------------------------------------------
1 | import { LayoutStorageStoreType } from './types';
2 |
3 | export const defaultConfig: LayoutStorageStoreType = {
4 | mode: 'side',
5 | collapsed: false,
6 | theme: {
7 | header: 'light',
8 | sidebar: 'dark',
9 | embed: 'light',
10 | },
11 | fixed: {
12 | header: false,
13 | sidebar: false,
14 | embed: false,
15 | },
16 | vars: {
17 | sidebarWidth: '200px',
18 | sidebarCollapseWidth: '64px',
19 | headerHeight: '48px',
20 | headerLightColor: '#fff',
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/Layout/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 | export * from './utils';
3 | export * from './hooks';
4 | export * from './provider';
5 |
--------------------------------------------------------------------------------
/src/components/Layout/provider.tsx:
--------------------------------------------------------------------------------
1 | import { useLocation } from 'react-router-dom';
2 |
3 | import { useCallback, useReducer } from 'react';
4 |
5 | import { useUpdateEffect } from 'react-use';
6 |
7 | import { useAntdCheck, useTheme } from '@/components/Config';
8 |
9 | import { useMenus } from '@/components/Menu';
10 |
11 | import { LayoutConfig } from './types';
12 | import { getMenuData, initLayoutConfig, layoutDarkTheme, layoutReducer } from './utils';
13 | import { useChangeLayoutLocalData, useLayoutLocalData, useSetupLayout } from './hooks';
14 | import { LayoutActionType } from './constants';
15 | import { LayoutContext, LayoutDispatchContext, LayoutSetup } from './store';
16 |
17 | export const LayoutStateProvider: FC = ({ children }) => {
18 | const isAntd = useAntdCheck();
19 | const systemTheme = useTheme();
20 | const config = useLayoutLocalData();
21 | const changeConfig = useChangeLayoutLocalData();
22 | const menus = useMenus();
23 | const location = useLocation();
24 | const [data, dispatch] = useReducer(
25 | layoutReducer,
26 | initLayoutConfig({
27 | config,
28 | menu: getMenuData(menus, location, config.mode),
29 | systemTheme,
30 | }),
31 | );
32 | useUpdateEffect(() => {
33 | changeConfig((state) => ({ ...state, ...data.config }));
34 | }, [data.config.vars, data.config.collapsed, data.config.mode, data.config.fixed]);
35 | useUpdateEffect(() => {
36 | if (!isAntd || systemTheme !== 'dark') {
37 | changeConfig((state) => ({ ...state, theme: data.config.theme }));
38 | }
39 | }, [data.config.theme]);
40 | useUpdateEffect(() => {
41 | if (!isAntd) return;
42 | if (systemTheme === 'dark') {
43 | dispatch({
44 | type: LayoutActionType.CHANGE_THEME,
45 | value: layoutDarkTheme,
46 | });
47 | } else {
48 | dispatch({
49 | type: LayoutActionType.CHANGE_THEME,
50 | value: config.theme,
51 | });
52 | }
53 | }, [systemTheme]);
54 | useUpdateEffect(() => {
55 | dispatch({
56 | type: LayoutActionType.CHANGE_MENU,
57 | value: getMenuData(menus, location, data.config.mode),
58 | });
59 | }, [data.config.mode, menus, location]);
60 | return (
61 |
62 |
63 | {children}
64 |
65 |
66 | );
67 | };
68 | export const LayoutProvider: FC = ({ children, ...rest }) => {
69 | const setuped = LayoutSetup(useCallback((state) => state.setuped, []));
70 | useSetupLayout(rest);
71 | return setuped ? {children} : null;
72 | };
73 |
--------------------------------------------------------------------------------
/src/components/Layout/store.ts:
--------------------------------------------------------------------------------
1 | import { createContext, Dispatch } from 'react';
2 |
3 | import { createStore } from '@/utils';
4 |
5 | import { RouteComponentProps } from '../Router';
6 |
7 | import { defaultConfig } from './default.config';
8 |
9 | import { LayoutAction, LayoutContextType, LayoutStorageStoreType } from './types';
10 |
11 | /**
12 | * 布局组件初始化状态池
13 | */
14 | export const LayoutSetup = createStore<{ setuped?: true }>(() => ({}));
15 | /**
16 | * 布局组件状态池
17 | */
18 | export const LayoutStore = createStore(() => defaultConfig);
19 | export const LayoutContext = createContext(null);
20 | export const LayoutDispatchContext = createContext | null>(null);
21 | export const LayoutRouteInfo = createContext(null);
22 |
--------------------------------------------------------------------------------
/src/components/Layout/types.ts:
--------------------------------------------------------------------------------
1 | import { ThemeMode } from '@/components/Config';
2 |
3 | import { MenuOption } from '@/components/Menu';
4 |
5 | import { LayoutActionType, LayoutComponent, LayoutMode } from './constants';
6 |
7 | /**
8 | * 布局配置
9 | */
10 | export interface LayoutConfig {
11 | /** 布局模式 */
12 | mode?: `${LayoutMode}`;
13 | /** 是否折叠边栏,如果是embed模式则折叠子变量 */
14 | collapsed?: boolean;
15 | /** 布局组件主题色 */
16 | theme?: Partial;
17 | /** 布局组件固定设置 */
18 | fixed?: Partial;
19 | /** 可用的CSS变量 */
20 | vars?: LayoutVarsConfig;
21 | }
22 | /**
23 | * 布局配置本地储存状态池
24 | */
25 | export interface LayoutStorageStoreType extends ReRequired {}
26 | /**
27 | * 布局组件状态
28 | */
29 | export interface LayoutContextType {
30 | /** 配置状态 */
31 | config: LayoutStorageStoreType;
32 | /** 是否展示移动设备下的菜单 */
33 | mobileSide: boolean;
34 | /** 菜单状态 */
35 | menu: LayoutMenuState;
36 | }
37 |
38 | /**
39 | * 布局组件主题色
40 | */
41 | export type LayoutTheme = { [key in `${LayoutComponent}`]: `${ThemeMode}` };
42 | /**
43 | * 布局组件是否固定
44 | */
45 | export type LayoutFixed = { [key in `${LayoutComponent}`]: boolean };
46 | /**
47 | * 布局组件CSS变量
48 | */
49 | export interface LayoutVarsConfig {
50 | /** 侧边栏宽度 */
51 | sidebarWidth?: string | number;
52 | /** 折叠时侧边栏宽度 */
53 | sidebarCollapseWidth?: string | number;
54 | /** 顶栏高度 */
55 | headerHeight?: string | number;
56 | /** 顶栏明亮模式下的颜色 */
57 | headerLightColor?: string;
58 | }
59 | /**
60 | * 菜单状态
61 | */
62 | export interface LayoutMenuState {
63 | /** 菜单列表 */
64 | data: MenuOption[];
65 | /** 展开的菜单 */
66 | opens: string[];
67 | /** 选中的菜单 */
68 | selects: string[];
69 | /** 拥有子菜单的顶级菜单,用于控制只有一个菜单打开 */
70 | rootSubKeys: string[];
71 | /** 分割菜单,用于top和embed模式的菜单 */
72 | split: LayoutSplitMenuState;
73 | }
74 | /**
75 | * 分割菜单的顶级菜单列表,用于top和embed模式的菜单
76 | */
77 | export interface LayoutSplitMenuState {
78 | /** 菜单数据 */
79 | data: MenuOption[];
80 | /** 选中的菜单 */
81 | selects: string[];
82 | }
83 | /**
84 | * 布局操作
85 | */
86 | export type LayoutAction =
87 | | {
88 | type: LayoutActionType.CHANGE_FIXED;
89 | key: keyof LayoutFixed;
90 | value: boolean;
91 | }
92 | | {
93 | type: LayoutActionType.CHANGE_VARS;
94 | /** css值 */
95 | vars: LayoutVarsConfig;
96 | }
97 | | {
98 | type: LayoutActionType.CHANGE_MODE;
99 | value: `${LayoutMode}`;
100 | }
101 | | {
102 | type: LayoutActionType.CHANGE_MENU;
103 | /** 菜单状态 */
104 | value: RePartial;
105 | }
106 | | {
107 | type: LayoutActionType.CHANGE_THEME;
108 | value: Partial;
109 | }
110 | | { type: LayoutActionType.CHANGE_COLLAPSE; value: boolean }
111 | | { type: LayoutActionType.TOGGLE_COLLAPSE }
112 | | { type: LayoutActionType.CHANGE_MOBILE_SIDE; value: boolean }
113 | | { type: LayoutActionType.TOGGLE_MOBILE_SIDE };
114 |
--------------------------------------------------------------------------------
/src/components/Menu/_default.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2021-12-14 00:07:50 +0800
6 | * @Updated_at : 2022-01-09 21:43:52 +0800
7 | * @Path : /src/components/Menu/_default.config.ts
8 | * @Description : 默认菜单配置
9 | * @LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 | import { MenuStoreType } from './types';
14 |
15 | export const getDefaultMenuStore = (): MenuStoreType => ({
16 | config: {
17 | type: 'router',
18 | server: null,
19 | menus: [],
20 | },
21 | data: [],
22 | });
23 |
--------------------------------------------------------------------------------
/src/components/Menu/hooks.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2021-12-16 05:55:08 +0800
6 | * @Updated_at : 2022-01-15 22:22:11 +0800
7 | * @Path : /src/components/Menu/hooks.ts
8 | * @Description : 可用的菜单组件钩子
9 | * @LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 | import { useCallback } from 'react';
14 |
15 | import { useUnmount } from 'react-use';
16 |
17 | import { createStoreHooks, deepMerge, useStoreSetuped } from '@/utils';
18 |
19 | import { AuthStore } from '../Auth';
20 |
21 | import { useFetcherGetter } from '../Fetcher';
22 |
23 | import { RouterStore } from '../Router/store';
24 |
25 | import { MenuConfig, MenuOption, MenuStatusType } from './types';
26 | import { changeMenus } from './utils';
27 | import { MenuStatus, MenuStore } from './store';
28 | /**
29 | * 动态菜单状态池
30 | */
31 | export const useMenu = createStoreHooks(MenuStore);
32 | /**
33 | * 动态菜单数据
34 | */
35 | export const useMenus = () => MenuStore(useCallback((state) => state.data, []));
36 | // export const useAntdMenus = () => MenuStore(useCallback((state) => getAntdMenus(state.data), []));
37 |
38 | /**
39 | * 初始化菜单
40 | * @param config 菜单配置
41 | */
42 | export const useSetupMenu = >(
43 | config?: MenuConfig,
44 | ) => {
45 | // fech工具用于获取远程菜单
46 | const fecher = useFetcherGetter();
47 | // 订阅next以刷新菜单数据
48 | const unMenuSub = MenuStatus.subscribe(
49 | (state) => state.next,
50 | (next) => changeMenus(next, fecher()),
51 | );
52 | // 订阅user,如果获取菜单的方式为独立配置则在user改变时刷新菜单
53 | const unAuthSub = AuthStore.subscribe(
54 | (state) => state.user,
55 | () => {
56 | const { setuped } = MenuStatus.getState();
57 | const { type } = MenuStore.getState().config;
58 | if (setuped && type !== 'router') {
59 | MenuStatus.setState((state) => {
60 | state.next = true;
61 | });
62 | }
63 | },
64 | );
65 | // 订阅路由列表,如果获取菜单的方式为通过路由携带则在路由列表改变时刷新菜单
66 | const unRouterSub = RouterStore.subscribe(
67 | (state) => state.routes,
68 | () => {
69 | const { setuped } = MenuStatus.getState();
70 | const { type } = MenuStore.getState().config;
71 | if (setuped && type === 'router') {
72 | MenuStatus.setState((state) => {
73 | state.next = true;
74 | });
75 | }
76 | },
77 | );
78 | /** 合并传入配置生成菜单状态 */
79 | useStoreSetuped({
80 | store: MenuStatus,
81 | callback: () => {
82 | MenuStore.setState((state) => {
83 | state.config = deepMerge(state.config, config ?? {});
84 | });
85 | },
86 | });
87 | useUnmount(() => {
88 | unRouterSub();
89 | unAuthSub();
90 | unMenuSub();
91 | });
92 | };
93 |
--------------------------------------------------------------------------------
/src/components/Menu/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 | export * from './hooks';
3 | export * from './store';
4 |
--------------------------------------------------------------------------------
/src/components/Menu/store.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2021-12-24 06:16:29 +0800
6 | * @Updated_at : 2022-01-16 14:02:41 +0800
7 | * @Path : /src/components/Menu/store.ts
8 | * @Description : 菜单组件状态池
9 | * @LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 |
14 | import { createStore } from '@/utils';
15 |
16 | import { getDefaultMenuStore } from './_default.config';
17 | import { MenuStoreType, MenuStatusType } from './types';
18 | /**
19 | * 菜单信号状态管理池
20 | */
21 | export const MenuStatus = createStore(() => ({ next: false }));
22 | /**
23 | * 菜单数据状态管理池
24 | */
25 | export const MenuStore = createStore(() => getDefaultMenuStore());
26 |
--------------------------------------------------------------------------------
/src/components/Menu/types.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2021-12-14 00:07:50 +0800
6 | * @Updated_at : 2022-01-12 21:02:47 +0800
7 | * @Path : /src/components/Menu/types.ts
8 | * @Description : 菜单组件类型
9 | * @LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 | import { SetupedState } from '@/utils';
14 |
15 | import { RouteMeta } from '../Router';
16 |
17 | /**
18 | * 菜单配置
19 | */
20 | export interface MenuConfig {
21 | /**
22 | * 获取菜单的方式
23 | * server: 通过服务器获取
24 | * router: 通过路由携带
25 | * configure: 独立配置
26 | */
27 | type?: 'server' | 'router' | 'configure';
28 | /** 通过服务器获取菜单的API地址 */
29 | server?: string | null;
30 | /** 独立配置的菜单列表 */
31 | menus?: MenuOption[];
32 | }
33 | /**
34 | * 菜单数据状态
35 | */
36 | export interface MenuState
37 | extends Omit>, 'menus'> {
38 | /** 菜单列表 */
39 | menus: MenuOption[];
40 | }
41 | /**
42 | * 菜单选项(继承自路由菜单元数据)
43 | */
44 | export type MenuOption = RouteMeta & {
45 | /** 菜单ID */
46 | id: string;
47 | /** 菜单文字 */
48 | text: string;
49 | /** 菜单路径 */
50 | path?: string;
51 | /** 子菜单 */
52 | children?: MenuOption[];
53 | };
54 | /**
55 | * 菜单生成信号状态管理池
56 | */
57 | export type MenuStatusType = SetupedState<{
58 | /** 是否即将开始重新生成菜单 */
59 | next: boolean;
60 | }>;
61 | /**
62 | * 菜单数据状态管理池
63 | */
64 | export interface MenuStoreType {
65 | /** 菜单配置状态 */
66 | config: MenuState;
67 | /** 最终菜单数据 */
68 | data: MenuOption[];
69 | }
70 | // export type AntdMenuOption = MenuOption<
71 | // AntdRouteMenuMeta
72 | // >;
73 |
--------------------------------------------------------------------------------
/src/components/Router/_default.config.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Author : pincman
3 | * HomePage : https://pincman.com
4 | * Support : support@pincman.com
5 | * Created_at : 2021-12-14 00:07:50 +0800
6 | * Updated_at : 2022-01-13 22:51:52 +0800
7 | * Path : /src/components/Router/_default.config.tsx
8 | * Description : 路由组件默认配置
9 | * LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 | import { Spinner } from '@/components/Spinner';
14 |
15 | import { RouterStoreType } from './types';
16 |
17 | export const getDefaultStore: () => RouterStoreType = () => ({
18 | routes: [],
19 | items: [],
20 | flats: [],
21 | renders: [],
22 | maps: {},
23 | config: {
24 | basePath: '/',
25 | hash: false,
26 | server: null,
27 | loading: () => ,
28 | auth: {
29 | enabled: true,
30 | login_redirect: '/auth/login',
31 | white_list: [],
32 | role_column: 'name',
33 | permission_column: 'name',
34 | redirect: 'login',
35 | },
36 | // permission: {
37 | // enabled: true,
38 | // column: 'name',
39 | // },
40 | routes: {
41 | constants: [],
42 | dynamic: [],
43 | },
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/src/components/Router/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 | export * from './store';
3 | export * from './provider';
4 | export * from './hooks';
5 | export * from './utils';
6 |
--------------------------------------------------------------------------------
/src/components/Router/provider.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Author : pincman
3 | * HomePage : https://pincman.com
4 | * Support : support@pincman.com
5 | * Created_at : 2021-12-14 00:07:50 +0800
6 | * Updated_at : 2022-01-09 14:29:57 +0800
7 | * Path : /src/components/Router/provider.tsx
8 | * Description : 路由组件包装器
9 | * LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 | import {
14 | BrowserRouter,
15 | HashRouter,
16 | matchRoutes,
17 | renderMatches,
18 | useLocation,
19 | RouteObject,
20 | } from 'react-router-dom';
21 |
22 | import { useRouter, useRouterStatus } from './hooks';
23 |
24 | /**
25 | * 根据路由渲染列表生成react router路由表
26 | * 也可以直接使用内置的`useRoutes`来替代
27 | * @param props
28 | */
29 | const RoutesList: FC<{ routes: RouteObject[]; basename: string }> = ({ routes, basename }) => {
30 | const location = useLocation();
31 | return renderMatches(matchRoutes(routes, location, basename));
32 | };
33 | /**
34 | * 路由渲染组件,用于渲染最终路由
35 | */
36 | const RouterRender: FC = () => {
37 | const { basePath: basename, hash, window } = useRouter.useConfig();
38 | const renders = useRouter.useRenders();
39 | return hash ? (
40 |
41 |
42 |
43 | ) : (
44 |
45 |
46 |
47 | );
48 | };
49 | /**
50 | * 路由组件包装器,在路由渲染列表生成后立即渲染路由
51 | */
52 | export const Router = () => {
53 | const success = useRouterStatus.useSuccess();
54 | return success ? : null;
55 | };
56 |
--------------------------------------------------------------------------------
/src/components/Router/store.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2021-12-16 17:08:42 +0800
6 | * @Updated_at : 2022-01-16 00:26:19 +0800
7 | * @Path : /src/components/Router/store.ts
8 | * @Description : 路由组件状态池
9 | * @LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 |
14 | import { createStore } from '@/utils';
15 |
16 | import { getDefaultStore } from './_default.config';
17 | import { RouterStatusType, RouterStoreType } from './types';
18 |
19 | /**
20 | * 路由初始化信号状态池
21 | */
22 | export const RouterStatus = createStore(() => ({
23 | next: false,
24 | success: false,
25 | }));
26 | /**
27 | * 路由状态池
28 | */
29 | export const RouterStore = createStore(() => getDefaultStore());
30 |
--------------------------------------------------------------------------------
/src/components/Router/utils/factory/index.ts:
--------------------------------------------------------------------------------
1 | import { AxiosInstance } from 'axios';
2 |
3 | import { isArray } from 'lodash-es';
4 |
5 | import { getUser } from '@/components/Auth';
6 |
7 | import { RouterStatus, RouterStore } from '../../store';
8 |
9 | import { RouteOption } from '../../types';
10 |
11 | import { filteAccessRoutes, filteWhiteList } from './filter';
12 | import { generateRoutes } from './generate';
13 |
14 | /**
15 | * 构建用户生成路由渲染的路由列表
16 | * @param fetcher 远程Request对象
17 | */
18 | export const factoryRoutes = async (fetcher: AxiosInstance) => {
19 | const user = getUser();
20 | const { config } = RouterStore.getState();
21 | RouterStatus.setState((state) => ({ ...state, next: false, success: false }));
22 | // 如果没有启用auth功能则使用配置中路由直接开始生成
23 | if (!config.auth.enabled) {
24 | RouterStore.setState((state) => {
25 | state.routes = [...state.config.routes.constants, ...state.config.routes.dynamic];
26 | });
27 | generateRoutes();
28 | } else if (user) {
29 | // 如果用户已登录,首先过滤精通路由
30 | RouterStore.setState((state) => {
31 | state.routes = filteAccessRoutes(user, state.config.routes.constants, config.auth, {
32 | basePath: config.basePath,
33 | });
34 | });
35 | if (!config.server) {
36 | // 如果路由通过配置生成则直接过滤动态路由并合并已过滤的静态路由
37 | RouterStore.setState((state) => {
38 | state.routes = filteAccessRoutes(
39 | user,
40 | [...state.routes, ...state.config.routes.dynamic],
41 | config.auth,
42 | {
43 | basePath: config.basePath,
44 | },
45 | );
46 | });
47 | generateRoutes();
48 | } else {
49 | try {
50 | // 如果路由通过服务器生成则直接合并已过滤的动态路由(权限过滤由服务端搞定)
51 | const { data } = await fetcher.get(config.server);
52 | if (isArray(data)) {
53 | RouterStore.setState((state) => {
54 | state.routes = [...state.routes, ...data];
55 | });
56 | }
57 | } catch (error) {
58 | console.log(error);
59 | }
60 | }
61 | } else {
62 | // 如果没有登录用户则根据白名单和路由项中的access为false来生成路由
63 | RouterStore.setState((state) => {
64 | state.routes = [
65 | ...filteWhiteList(state.config.routes.constants, config.auth, {
66 | basePath: config.basePath,
67 | }),
68 | ...filteWhiteList(state.config.routes.dynamic, config.auth, {
69 | basePath: config.basePath,
70 | }),
71 | ];
72 | });
73 | generateRoutes();
74 | RouterStatus.setState((state) => ({ ...state, next: false }));
75 | }
76 | };
77 |
--------------------------------------------------------------------------------
/src/components/Router/utils/helpers.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2021-12-14 00:07:50 +0800
6 | * @Updated_at : 2022-01-18 06:00:37 +0800
7 | * @Path : /src/components/Router/utils/helpers.ts
8 | * @Description : 工具函数
9 | * @LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 | import { trim } from 'lodash-es';
14 | import { isNil } from 'ramda';
15 |
16 | import { isUrl } from '@/utils';
17 |
18 | import { IndexRouteOption, PathRouteOption, RouteOption } from '../types';
19 |
20 | /**
21 | * 组装并格式化路由路径以获取完整路径
22 | * @param item 路由配置
23 | * @param basePath 基础路径
24 | * @param parentPath 父路径
25 | */
26 | export const mergeRoutePath = (
27 | item: RouteOption,
28 | basePath: string,
29 | parentPath?: string,
30 | ): string => {
31 | const currentPath = 'path' in item && typeof item.path === 'string' ? item.path : '';
32 | // 如果没有传入父路径则使用basePath作为路由前缀
33 | let prefix = !parentPath ? basePath : `/${trim(parentPath, '/')}`;
34 | // 如果是父路径下的根路径则直接父路径
35 | if (trim(currentPath, '/') === '') return prefix;
36 | // 如果是顶级根路径并且当前路径以通配符"*"开头则直接返回当前路径
37 | if (prefix === '/' && currentPath.startsWith('*')) return currentPath;
38 | // 如果前缀不是"/",则为在前缀后添加"/"作为与当前路径的连接符
39 | if (prefix !== '/') prefix = `${prefix}/`;
40 | // 生成最终路径
41 | return `${prefix}${trim(currentPath, '/')}`;
42 | };
43 |
44 | export const checkRoute = (option: RouteOption): option is PathRouteOption | IndexRouteOption => {
45 | if ('index' in option) return option.index;
46 | if ('path' in option) {
47 | return !isNil(option.path) && option.path.length > 0 && !isUrl(option.path);
48 | }
49 | return false;
50 | };
51 |
--------------------------------------------------------------------------------
/src/components/Router/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './factory';
2 | export * from './helpers';
3 |
--------------------------------------------------------------------------------
/src/components/Router/utils/views.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Author : pincman
3 | * HomePage : https://pincman.com
4 | * Support : support@pincman.com
5 | * Created_at : 2021-12-14 00:07:50 +0800
6 | * Updated_at : 2022-01-14 00:46:36 +0800
7 | * Path : /src/components/Router/utils/views.tsx
8 | * Description : 页面和视图组件
9 | * LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 | import loadable from '@loadable/component';
14 | import pMinDelay from 'p-min-delay';
15 | import { FC, FunctionComponent } from 'react';
16 | import { Navigate, useLocation } from 'react-router-dom';
17 | import { timeout } from 'promise-timeout';
18 | import { has } from 'lodash-es';
19 |
20 | import { RoutePage } from '../types';
21 |
22 | /**
23 | * 根据正则和glob递归获取所有动态页面导入映射
24 | * [key:bar/foo]: () => import('{起始目录: 如page}/bar/foo.blade.tsx')
25 | * @param imports 需要遍历的路径规则,支持glob
26 | * @param reg 用于匹配出key的正则表达式
27 | */
28 | const getAsyncImports = (imports: Record Promise>, reg: RegExp) => {
29 | return Object.keys(imports)
30 | .map((key) => {
31 | const names = reg.exec(key);
32 | return Array.isArray(names) && names.length >= 2
33 | ? { [names[1]]: imports[key] }
34 | : undefined;
35 | })
36 | .filter((m) => !!m)
37 | .reduce((o, n) => ({ ...o, ...n }), []) as unknown as Record Promise>;
38 | };
39 | /**
40 | * 所有动态页面映射
41 | */
42 | export const pages = getAsyncImports(
43 | import.meta.glob('../../../views/**/*.blade.{tsx,jsx}'),
44 | /..\/..\/\..\/views\/([\w+.?/?]+)(.blade.tsx)|(.blade.jsx)/i,
45 | );
46 |
47 | /**
48 | * 未登录跳转页面组件
49 | * @param props
50 | */
51 | export const AuthRedirect: FC<{
52 | /** 登录跳转地址 */
53 | loginPath?: string;
54 | }> = ({ loginPath }) => {
55 | const location = useLocation();
56 | let redirect = `?redirect=${location.pathname}`;
57 | if (location.search) redirect = `${redirect}${location.search}`;
58 | return ;
59 | };
60 | /**
61 | * 异步页面组件
62 | * @param props
63 | */
64 | export const getAsyncPage = (props: {
65 | /** 缓存key */
66 | cacheKey: string;
67 | /** loading组件 */
68 | loading: FunctionComponent | false;
69 | /** 页面路径 */
70 | page: string;
71 | }) => {
72 | const { cacheKey, page } = props;
73 | const fallback: JSX.Element | undefined = props.loading ? : undefined;
74 | if (!has(pages, page)) throw new Error(`Page ${page} not exits in 'views' dir!`);
75 | return loadable(() => timeout(pMinDelay(pages[page](), 50), 220000), {
76 | cacheKey: () => cacheKey,
77 | fallback,
78 | });
79 | };
80 |
81 | export const IFramePage: RoutePage<{ to: string }> = ({ route, to }) => {
82 | return ;
83 | };
84 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/babel/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SpinnerOption } from '../../types';
2 |
3 | import classes from './style.module.css';
4 |
5 | export const Babel = (props: SpinnerOption) => {
6 | const style = useSpinnerStyle(props);
7 | return ;
8 | };
9 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/babel/style.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | --size: 30px;
3 | --speed: 2s;
4 | --color: #00adb5;
5 | width: var(--size);
6 | height: var(--size);
7 | background: var(--color);
8 | border-radius: calc(var(--size) / 2);
9 | animation: var(--speed) animate linear infinite;
10 | }
11 |
12 | @keyframes animate {
13 | 0% {
14 | opacity: 1;
15 | transform: scale(0.1);
16 | }
17 |
18 | 50% {
19 | opacity: 0.5;
20 | transform: scale(1);
21 | }
22 |
23 | 100% {
24 | opacity: 0.1;
25 | transform: scale(1.5);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/block-reserve/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SpinnerOption } from '../../types';
2 |
3 | import classes from './style.module.css';
4 |
5 | export const Block = (props: SpinnerOption) => {
6 | const style = useSpinnerStyle(props);
7 | return (
8 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/block-reserve/style.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | --size: 30px;
3 | --speed: 2s;
4 | --color: #00adb5;
5 | position: relative;
6 | width: calc(var(--size) * 4);
7 | height: calc(var(--size) * 4);
8 | perspective: calc(var(--size) * 4);
9 |
10 | & > div {
11 | position: absolute;
12 | top: 0;
13 | right: 0;
14 | bottom: 0;
15 | left: 0;
16 | width: var(--size);
17 | height: var(--size);
18 | margin: auto;
19 | background: var(--color);
20 | transform: rotate(0);
21 | animation: var(--speed) animate infinite;
22 | }
23 | }
24 |
25 | @keyframes animate {
26 | 50% {
27 | transform: rotateY(-180deg);
28 | }
29 |
30 | 100% {
31 | transform: rotateY(-180deg) rotateX(-180deg);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/box/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SpinnerOption } from '../../types';
2 | import { useSpinnerStyle } from '../../utils';
3 |
4 | import classes from './style.module.css';
5 |
6 | export const Box = (props: SpinnerOption) => {
7 | const style = useSpinnerStyle(props);
8 | return ;
9 | };
10 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/box/style.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | --size: 30px;
3 | --speed: 1.5s;
4 | --color: #00adb5;
5 | width: var(--size);
6 | height: var(--size);
7 |
8 | &::before {
9 | position: absolute;
10 | top: calc(var(--size) + 10px);
11 | left: 0;
12 | width: var(--size);
13 | height: calc(var(--size) / 6);
14 | content: '';
15 | background: #000;
16 | border-radius: 50%;
17 | opacity: 0.1;
18 | animation: var(--speed) animate linear infinite;
19 | }
20 |
21 | &::after {
22 | position: absolute;
23 | top: 0;
24 | left: 0;
25 | width: var(--size);
26 | height: var(--size);
27 | content: '';
28 | background: var(--color);
29 | border-radius: calc(var(--size) / 10);
30 | animation: var(--speed) shadow linear infinite;
31 | }
32 | }
33 |
34 | @keyframes shadow {
35 | 17% {
36 | border-bottom-right-radius: calc(var(--size) / 10);
37 | }
38 |
39 | 25% {
40 | transform: translateY(calc(var(--size) / 10 * 3)) rotate(22.5deg);
41 | }
42 |
43 | 50% {
44 | border-bottom-right-radius: calc(var(--size) / 3 * 4);
45 | transform: translateY(calc(var(--size) / 5 * 3)) scale(1, 0.9) rotate(45deg);
46 | }
47 |
48 | 75% {
49 | transform: translateY(9px) rotate(67.5deg);
50 | }
51 |
52 | 100% {
53 | transform: translateY(0) rotate(90deg);
54 | }
55 | }
56 |
57 | @keyframes animate {
58 | 0%,
59 | 100% {
60 | transform: scale(1, 1);
61 | }
62 |
63 | 50% {
64 | transform: scale(1.2, 1);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/coffee/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SpinnerOption } from '../../types';
2 | import { useSpinnerStyle } from '../../utils';
3 |
4 | import classes from './style.module.css';
5 |
6 | export const Coffee = (props: SpinnerOption) => {
7 | const style = useSpinnerStyle(props);
8 | return ;
9 | };
10 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/coffee/style.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | --size: 30px;
3 | --speed: 2s;
4 | --color: #00adb5;
5 | position: relative;
6 | width: var(--size);
7 | height: var(--size);
8 | border: calc(var(--size) / 30) var(--color) solid;
9 | border-radius: 0 0 calc(var(--size) / 6) calc(var(--size) / 6);
10 |
11 | &::before {
12 | position: absolute;
13 | top: calc(var(--size) / 3 * -1);
14 | left: calc(var(--size) / 15 * 4);
15 | width: calc(var(--size) / 30);
16 | height: calc(var(--size) / 5);
17 | content: '';
18 | background-color: var(--color);
19 | box-shadow: calc(var(--size) / 6) 0 0 0 var(--color),
20 | calc(var(--size) / 6) calc(var(--size) / 6 * -1) 0 0 var(--color),
21 | calc(var(--size) / 3) 0 0 0 var(--color);
22 | animation: var(--speed) animate linear infinite alternate;
23 | }
24 |
25 | &::after {
26 | position: absolute;
27 | top: calc(var(--size) / 15 * 2);
28 | left: var(--size);
29 | width: calc(var(--size) / 6);
30 | height: calc(var(--size) / 5 * 2);
31 | content: '';
32 | border: calc(var(--size) / 30) solid var(--color);
33 | border-left: none;
34 | border-radius: 0 var(--size) var(--size) 0;
35 | }
36 | }
37 |
38 | @keyframes animate {
39 | 0% {
40 | height: 0;
41 | }
42 |
43 | 100% {
44 | height: calc(var(--size) / 5);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/disappeared/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SpinnerOption } from '../../types';
2 | import { useSpinnerStyle } from '../../utils';
3 |
4 | import classes from './style.module.css';
5 |
6 | export const Disappeared = (props: SpinnerOption) => {
7 | const style = useSpinnerStyle(props);
8 | return (
9 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/disappeared/style.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | --size: 30px;
3 | --speed: 0.8s;
4 | --color: #00adb5;
5 | display: flex;
6 | flex-flow: nowrap;
7 | align-items: center;
8 | justify-content: space-between;
9 | width: calc(var(--size) * 2);
10 | height: calc(var(--size) * 2);
11 |
12 | & > div:nth-of-type(1) {
13 | animation-delay: calc(var(--speed) / 2 * -1);
14 | }
15 |
16 | & > div:nth-of-type(2) {
17 | animation-delay: calc(var(--speed) / 4 * -1);
18 | }
19 |
20 | & > div {
21 | width: var(--size);
22 | height: var(--size);
23 | background: var(--color);
24 | border-radius: 50%;
25 | animation: var(--speed) animate ease-in-out alternate infinite;
26 | }
27 | }
28 |
29 | @keyframes animate {
30 | from {
31 | opacity: 1;
32 | }
33 |
34 | to {
35 | opacity: 0;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/eat/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SpinnerOption } from '../../types';
2 | import { useSpinnerStyle } from '../../utils';
3 |
4 | import classes from './style.module.css';
5 |
6 | export const Eat = (props: SpinnerOption<{ itemColor?: string }>) => {
7 | const style = {
8 | ...useSpinnerStyle(props),
9 | '--item-color': props.itemColor,
10 | };
11 | return (
12 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/eat/style.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | --size: 30px;
3 | --speed: 1s;
4 | --color: #f8b26a;
5 | --item-color: #e15b64;
6 | position: absolute;
7 | display: flex;
8 | align-items: center;
9 | justify-content: center;
10 | width: calc(var(--size) * 2);
11 | height: calc(var(--size) * 2);
12 |
13 | & > div:first-child {
14 | display: block;
15 |
16 | & > div {
17 | position: absolute;
18 | top: calc(50% - var(--size) * 1 / 5);
19 | width: calc(var(--size) * 2 / 5);
20 | height: calc(var(--size) * 2 / 5);
21 | background: var(--item-color);
22 | border-radius: 50%;
23 | animation: var(--speed) animate3 linear infinite;
24 | }
25 |
26 | & > div:nth-child(1) {
27 | animation-delay: -0.67s;
28 | }
29 |
30 | & > div:nth-child(2) {
31 | animation-delay: -0.33s;
32 | }
33 |
34 | & > div:nth-child(3) {
35 | animation-delay: 0s;
36 | }
37 | }
38 |
39 | & > div:last-child {
40 | display: flex;
41 | align-items: center;
42 | justify-content: center;
43 | width: 100%;
44 | height: 100%;
45 |
46 | & > div {
47 | position: absolute;
48 | top: calc(50% - var(--size));
49 | width: calc(var(--size) * 2);
50 | height: var(--size);
51 | background: var(--color);
52 | border-radius: calc(var(--size) * 2) calc(var(--size) * 2) 0 0;
53 | transform-origin: 50% 100%;
54 | }
55 |
56 | & > div:first-child {
57 | animation: var(--speed, 1s) animate1 linear infinite;
58 | }
59 |
60 | & > div:nth-child(2) {
61 | animation: var(--speed, 1s) animate2 linear infinite;
62 | }
63 |
64 | & > div:last-child {
65 | transform: rotate(-90deg);
66 | animation: none;
67 | }
68 | }
69 | }
70 |
71 | @keyframes animate1 {
72 | 0% {
73 | transform: rotate(0deg);
74 | }
75 |
76 | 50% {
77 | transform: rotate(-45deg);
78 | }
79 |
80 | 100% {
81 | transform: rotate(0deg);
82 | }
83 | }
84 |
85 | @keyframes animate2 {
86 | 0% {
87 | transform: rotate(180deg);
88 | }
89 |
90 | 50% {
91 | transform: rotate(225deg);
92 | }
93 |
94 | 100% {
95 | transform: rotate(180deg);
96 | }
97 | }
98 |
99 | @keyframes animate3 {
100 | 0% {
101 | opacity: 0;
102 | transform: translate(calc(var(--size) * 3), 0);
103 | }
104 |
105 | 20% {
106 | opacity: 1;
107 | }
108 |
109 | 100% {
110 | opacity: 1;
111 | transform: translate(calc(var(--size) * 2 / 3), 0);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/index.ts:
--------------------------------------------------------------------------------
1 | export * from './coffee';
2 | export * from './box';
3 | export * from './rain';
4 | export * from './babel';
5 | export * from './block-reserve';
6 | export * from './disappeared';
7 | export * from './wave';
8 | export * from './wind-mill';
9 | export * from './loop-circle';
10 | export * from './rotate-circle';
11 | export * from './jump-circle';
12 | export * from './eat';
13 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/jump-circle/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SpinnerOption } from '../../types';
2 |
3 | import classes from './style.module.css';
4 |
5 | export const JumpCircle = (props: SpinnerOption<{ circleColor?: string }>) => {
6 | const style = {
7 | ...useSpinnerStyle(props),
8 | '--circle-color': props.circleColor,
9 | };
10 | return (
11 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/jump-circle/style.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | --size: 30px;
3 | --speed: 0.5s;
4 | --color: #00adb5;
5 | --circle-color: #f9c094;
6 | position: relative;
7 | display: flex;
8 | align-items: flex-end;
9 | justify-content: center;
10 | width: var(--size);
11 | height: var(--size);
12 |
13 | & > div:first-child {
14 | width: calc(var(--size) / 2);
15 | height: calc(var(--size) / 2);
16 | background-color: var(--color);
17 | border-radius: 50%;
18 | animation: var(--speed) animate ease-out infinite alternate;
19 | }
20 |
21 | & > div:last-child {
22 | position: absolute;
23 | bottom: 0;
24 | left: 0;
25 | width: var(--size);
26 | height: calc(var(--size) * 2 / 15);
27 | background-color: var(--color);
28 | }
29 | }
30 |
31 | @keyframes animate {
32 | 0% {
33 | transform: translateY(0) scaleX(1.2) scaleY(0.8);
34 | }
35 |
36 | 25% {
37 | transform: translateY(calc(var(--size) / 3 * -1)) scaleX(1) scaleY(1);
38 | }
39 |
40 | 100% {
41 | background-color: var(--circle-color);
42 | transform: translateY(calc(var(--size) / 3 * -4));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/loop-circle/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SpinnerOption } from '../../types';
2 | import { useSpinnerStyle } from '../../utils';
3 |
4 | import classes from './style.module.css';
5 |
6 | export const LoopCircle = (props: SpinnerOption) => {
7 | const style = useSpinnerStyle(props);
8 | return (
9 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/loop-circle/style.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | --size: 12px;
3 | --color: #00adb5;
4 | --speed: 1.2s;
5 | position: absolute;
6 | width: calc(var(--size) * 4);
7 | height: calc(var(--size) * 4);
8 |
9 | & > div {
10 | position: absolute;
11 | width: 100%;
12 | height: 100%;
13 |
14 | & > div {
15 | position: absolute;
16 | width: var(--size);
17 | height: var(--size);
18 | background-color: var(--color);
19 | border-radius: 100%;
20 | animation: var(--speed) animate infinite ease-in-out;
21 | animation-fill-mode: both;
22 | }
23 | }
24 |
25 | & > div:nth-of-type(1) {
26 | & > div:nth-of-type(1) {
27 | top: 0;
28 | left: 0;
29 | }
30 |
31 | & > div:nth-of-type(2) {
32 | top: 0;
33 | right: 0;
34 | animation-delay: -0.9s;
35 | }
36 |
37 | & > div:nth-of-type(3) {
38 | right: 0;
39 | bottom: 0;
40 | animation-delay: -0.6s;
41 | }
42 |
43 | & > div:nth-of-type(4) {
44 | bottom: 0;
45 | left: 0;
46 | animation-delay: -0.3s;
47 | }
48 | }
49 |
50 | & > div:nth-of-type(2) {
51 | transform: rotateZ(45deg);
52 |
53 | & > div:nth-of-type(1) {
54 | top: 0;
55 | left: 0;
56 | animation-delay: -1.1s;
57 | }
58 |
59 | & > div:nth-of-type(2) {
60 | top: 0;
61 | right: 0;
62 | animation-delay: -0.8s;
63 | }
64 |
65 | & > div:nth-of-type(3) {
66 | right: 0;
67 | bottom: 0;
68 | animation-delay: -0.5s;
69 | }
70 |
71 | & > div:nth-of-type(4) {
72 | bottom: 0;
73 | left: 0;
74 | animation-delay: -0.2s;
75 | }
76 | }
77 |
78 | & > div:nth-of-type(3) {
79 | transform: rotateZ(90deg);
80 |
81 | & > div:nth-of-type(1) {
82 | top: 0;
83 | left: 0;
84 | animation-delay: -1s;
85 | }
86 |
87 | & > div:nth-of-type(2) {
88 | top: 0;
89 | right: 0;
90 | animation-delay: -0.7s;
91 | }
92 |
93 | & > div:nth-of-type(3) {
94 | right: 0;
95 | bottom: 0;
96 | animation-delay: -0.4s;
97 | }
98 |
99 | & > div:nth-of-type(4) {
100 | bottom: 0;
101 | left: 0;
102 | animation-delay: -0.1s;
103 | }
104 | }
105 | }
106 |
107 | @keyframes animate {
108 | 0%,
109 | 80%,
110 | 100% {
111 | transform: scale(0);
112 | }
113 |
114 | 40% {
115 | transform: scale(1);
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/rain/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SpinnerOption } from '../../types';
2 | import { useSpinnerStyle } from '../../utils';
3 |
4 | import classes from './style.module.css';
5 |
6 | export const Rain = (props: SpinnerOption) => {
7 | const style = useSpinnerStyle(props);
8 | return (
9 |
10 | {Array.from(Array(9)).map((_, index) => (
11 |
12 | ))}
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/rain/style.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | --size: 80px;
3 | --color: #00adb5;
4 | --speed: 3s;
5 | position: relative;
6 | width: var(--size);
7 | height: var(--size);
8 | transform: rotateZ(45deg);
9 |
10 | & > div {
11 | position: absolute;
12 | height: calc(var(--size) / 40);
13 | background: linear-gradient(-45deg, var(--color), rgb(0 0 255 / 0%));
14 | border-radius: 50%;
15 | animation: var(--speed) scaling ease-in-out infinite,
16 | var(--speed) move-to ease-in-out infinite;
17 | }
18 |
19 | & > div:nth-of-type(1) {
20 | top: 30%;
21 | left: 25%;
22 | animation-delay: 0s;
23 | }
24 |
25 | & > div:nth-of-type(2) {
26 | top: 10%;
27 | left: 0%;
28 | animation-delay: 0.8s;
29 | }
30 |
31 | & > div:nth-of-type(3) {
32 | top: 15%;
33 | left: 10%;
34 | animation-delay: 0.5s;
35 | }
36 |
37 | & > div:nth-of-type(4) {
38 | top: 25%;
39 | left: 30%;
40 | animation-delay: 1.6s;
41 | }
42 |
43 | & > div:nth-of-type(5) {
44 | top: 40%;
45 | left: 4%;
46 | animation-delay: 3.2s;
47 | }
48 |
49 | & > div:nth-of-type(6) {
50 | top: 55%;
51 | left: 18%;
52 | animation-delay: 1.2s;
53 | }
54 |
55 | & > div:nth-of-type(7) {
56 | top: 66%;
57 | left: 3%;
58 | animation-delay: 0.4s;
59 | }
60 |
61 | & > div:nth-of-type(8) {
62 | top: 77%;
63 | left: 24%;
64 | animation-delay: 2s;
65 | }
66 |
67 | & > div:nth-of-type(9) {
68 | top: 83%;
69 | left: 30%;
70 | animation-delay: 1s;
71 | }
72 | }
73 |
74 | @keyframes scaling {
75 | 0% {
76 | width: 0;
77 | }
78 |
79 | 50% {
80 | width: calc(var(--size) / 2);
81 | }
82 |
83 | 100% {
84 | width: 0;
85 | }
86 | }
87 |
88 | @keyframes move-to {
89 | 0% {
90 | transform: translateX(0);
91 | }
92 |
93 | 100% {
94 | transform: translateX(calc(var(--size) / 4 * 3));
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/rotate-circle/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SpinnerOption } from '../../types';
2 | import { useSpinnerStyle } from '../../utils';
3 |
4 | import classes from './style.module.css';
5 |
6 | export const RotateCircle = (props: SpinnerOption) => {
7 | const style = useSpinnerStyle(props);
8 | return (
9 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/rotate-circle/style.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | --size: 30px;
3 | --color: #00adb5;
4 | --speed: 2.4s;
5 | position: relative;
6 | width: var(--size);
7 | height: var(--size);
8 | overflow: hidden;
9 | animation: var(--speed) rotate linear infinite;
10 |
11 | & > div {
12 | position: absolute;
13 | width: calc(var(--size) / 2);
14 | height: calc(var(--size) / 2);
15 | background-color: var(--color);
16 | border-radius: 50%;
17 | animation: var(--speed) opacity linear infinite alternate;
18 | }
19 |
20 | & > div:nth-of-type(1) {
21 | top: 0;
22 | left: 0;
23 | animation-delay: 0s;
24 | }
25 |
26 | & > div:nth-of-type(2) {
27 | top: 0;
28 | right: 0;
29 | animation-delay: var(--speed) / 3;
30 | }
31 |
32 | & > div:nth-of-type(3) {
33 | bottom: 0;
34 | left: 0;
35 | animation-delay: var(--speed) / 3 * 2;
36 | }
37 |
38 | & > div:nth-of-type(4) {
39 | right: 0;
40 | bottom: 0;
41 | animation-delay: var(--speed);
42 | }
43 | }
44 |
45 | @keyframes rotate {
46 | 0% {
47 | transform: rotate(0);
48 | }
49 |
50 | 100% {
51 | transform: rotate(360deg);
52 | }
53 | }
54 |
55 | @keyframes opacity {
56 | from {
57 | opacity: 1;
58 | }
59 |
60 | to {
61 | opacity: 0.3;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/wave/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SpinnerOption } from '../../types';
2 | import { useSpinnerStyle } from '../../utils';
3 |
4 | import classes from './style.module.css';
5 |
6 | export const Wave = (props: SpinnerOption) => {
7 | const style = useSpinnerStyle(props);
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/wave/style.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | --size: 30px;
3 | --color: #00adb5;
4 | --speed: 1.2s;
5 | width: calc(var(--size) / 3) * 10;
6 | height: var(--size);
7 | font-size: calc(var(--size) / 3);
8 | text-align: center;
9 |
10 | & > div {
11 | display: inline-block;
12 | width: calc(var(--size) / 5);
13 | height: 100%;
14 | margin-left: 5px;
15 | background-color: var(--color);
16 | animation: animate var(--speed) infinite ease-in-out;
17 | }
18 |
19 | & > div:nth-of-type(1) {
20 | animation-delay: calc(var(--speed) * -1);
21 | }
22 |
23 | & > div:nth-of-type(2) {
24 | animation-delay: calc(var(--speed) * -1 + 0.1s);
25 | }
26 |
27 | & > div:nth-of-type(3) {
28 | animation-delay: calc(var(--speed) * -1 + 0.2s);
29 | }
30 |
31 | & > div:nth-of-type(4) {
32 | animation-delay: calc(var(--speed) * -1 + 0.3s);
33 | }
34 |
35 | & > div:nth-of-type(5) {
36 | animation-delay: calc(var(--speed) * -1 + 0.4s);
37 | }
38 | }
39 |
40 | @keyframes animate {
41 | 0%,
42 | 40%,
43 | 100% {
44 | transform: scaleY(0.4);
45 | }
46 |
47 | 20% {
48 | transform: scaleY(1);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/wind-mill/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SpinnerOption } from '../../types';
2 | import { useSpinnerStyle } from '../../utils';
3 |
4 | import classes from './style.module.css';
5 |
6 | export const WaveMill = (props: SpinnerOption) => {
7 | const style = useSpinnerStyle(props);
8 | return (
9 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/Spinner/collection/wind-mill/style.module.css:
--------------------------------------------------------------------------------
1 | @keyframes animate {
2 | 0% {
3 | transform: rotate(360deg);
4 | }
5 |
6 | 50% {
7 | transform: rotate(180deg);
8 | }
9 |
10 | 100% {
11 | transform: rotate(0deg);
12 | }
13 | }
14 |
15 | .container {
16 | --size: 50px;
17 | --color: #00adb5;
18 | position: relative;
19 | width: 4px;
20 | height: 0;
21 | padding-top: calc(var(--size) / 2);
22 | border-color: transparent transparent var(--color);
23 | border-style: none solid solid;
24 | border-width: 0 4px var(--size) 4px;
25 |
26 | & > div:first-child {
27 | position: absolute;
28 | width: 4px;
29 | height: 4px;
30 | background: #fff;
31 | border: 3px var(--color) solid;
32 | border-radius: 5px;
33 | transform: translateX(-3px) translateY(-4px);
34 | }
35 |
36 | & > div:last-child {
37 | position: relative;
38 | animation: animate var(--speed, 5s) infinite linear;
39 |
40 | & > div {
41 | position: absolute;
42 | width: 2px;
43 | height: 0;
44 | border-color: var(--color) transparent transparent;
45 | border-style: solid solid none;
46 | border-width: calc(var(--size) / 5 * 3) 2px 0 2px;
47 | }
48 |
49 | & > div:nth-of-type(1) {
50 | transform: rotate(60deg);
51 | transform-origin: 0 -2px;
52 | }
53 |
54 | & > div:nth-of-type(2) {
55 | transform: rotate(180deg);
56 | transform-origin: 2px -1px;
57 | }
58 |
59 | & > div:nth-of-type(3) {
60 | transform: rotate(300deg);
61 | transform-origin: 5px 0;
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/Spinner/constants.ts:
--------------------------------------------------------------------------------
1 | import type { CSSProperties } from 'react';
2 |
3 | export const defaultStyle: CSSProperties = {
4 | margin: 'auto',
5 | position: 'absolute',
6 | left: 0,
7 | right: 0,
8 | top: 0,
9 | bottom: 0,
10 | };
11 |
--------------------------------------------------------------------------------
/src/components/Spinner/index.ts:
--------------------------------------------------------------------------------
1 | export * from './spinner';
2 | export * from './loading';
3 |
--------------------------------------------------------------------------------
/src/components/Spinner/loading.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 |
3 | import type { LoadingProps } from './types';
4 |
5 | export const Loading: FC = ({ className, style, component }) => {
6 | // 'fixed w-full h-full top-0 left-0 dark:bg-white bg-gray-800 bg-opacity-25 flex items-center justify-center';
7 | const defaultClassName = classNames([
8 | 'h-full',
9 | 'w-full',
10 | 'flex',
11 | 'items-center',
12 | 'justify-center',
13 | ]);
14 | const classes = className ? `${defaultClassName} ${className}` : defaultClassName;
15 | return (
16 |
17 | {component}
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/Spinner/spinner.tsx:
--------------------------------------------------------------------------------
1 | import type { SpinnerProps } from './types';
2 | import * as spinners from './collection';
3 |
4 | export const Spinner: FC = ({ name, ...rest }) => {
5 | const Icon = spinners[name];
6 | return Icon ? : null;
7 | };
8 |
--------------------------------------------------------------------------------
/src/components/Spinner/types.ts:
--------------------------------------------------------------------------------
1 | import type { CSSProperties } from 'react';
2 |
3 | import type * as spinners from './collection';
4 |
5 | export type SpinnerName = keyof typeof spinners;
6 | export type SpinnerOption = RecordScalable<
7 | {
8 | center?: boolean;
9 | style?: CSSProperties;
10 | speed?: number;
11 | color?: string;
12 | darkColor?: string;
13 | size?: string;
14 | },
15 | T
16 | >;
17 | export type SpinnerProps = SpinnerOption & {
18 | name: keyof typeof spinners;
19 | };
20 | export interface LoadingProps {
21 | component: JSX.Element;
22 | className?: string;
23 | style?: CSSProperties;
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/Spinner/utils.ts:
--------------------------------------------------------------------------------
1 | import { useUpdateEffect } from 'ahooks';
2 | import { omit } from 'lodash-es';
3 | import { CSSProperties, useCallback, useState } from 'react';
4 |
5 | import { ThemeMode, useColors, useTheme } from '../Config';
6 |
7 | import { defaultStyle } from './constants';
8 | import type { SpinnerOption } from './types';
9 |
10 | export const useSpinnerStyle = (props: SpinnerOption) => {
11 | const { center, style, size, darkColor, speed } = props;
12 | const colors = useColors();
13 | const theme = useTheme();
14 | const [color, setColor] = useState(props.color ?? colors.info);
15 | const getStyle = useCallback(
16 | (t: `${ThemeMode}`) => ({
17 | ...(center ? defaultStyle : {}),
18 | ...omit(style ?? {}, ['className']),
19 | '--size': size,
20 | '--color': t === 'dark' ? darkColor ?? color : color,
21 | '--speed': speed ? `${speed}s` : undefined,
22 | '--darkreader-bg--color': darkColor ?? color,
23 | '--darkreader-border--color': darkColor ?? color,
24 | }),
25 | [],
26 | );
27 | const [styles, setStyles] = useState(getStyle(theme));
28 | useUpdateEffect(() => {
29 | setColor(props.color ?? colors.info);
30 | }, [colors.info, props.color]);
31 | useUpdateEffect(() => {
32 | setStyles(getStyle(theme));
33 | }, [theme]);
34 | return styles as CSSProperties;
35 | };
36 |
--------------------------------------------------------------------------------
/src/components/Storage/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 | export * from './store';
3 | export * from './hooks';
4 |
--------------------------------------------------------------------------------
/src/components/Storage/store.ts:
--------------------------------------------------------------------------------
1 | import { redux } from 'zustand/middleware';
2 | import create from 'zustand';
3 |
4 | import { createStore, SetupedState } from '@/utils';
5 |
6 | import { fixStorage, storageReducer } from './utils';
7 |
8 | /**
9 | * Storage初始化状态的store
10 | */
11 | export const StorageSetup = createStore(() => ({}));
12 | /**
13 | * Storage状态管理store
14 | */
15 | export const StorageStore = create(redux(storageReducer, { config: fixStorage({}) }));
16 |
--------------------------------------------------------------------------------
/src/config/app.ts:
--------------------------------------------------------------------------------
1 | import type { ConfigProps } from '@/components/Config';
2 |
3 | export const config: ConfigProps = {
4 | timezone: 'UTC',
5 | theme: {
6 | mode: 'light',
7 | depend: 'manual',
8 | // range: {
9 | // light: timer({ date: '07:30', format: 'HH:mm' }).format(),
10 | // dark: timer({ date: '18:30', format: 'HH:mm' }).format(),
11 | // },
12 | },
13 | colors: {
14 | primary: '#1890ff',
15 | info: '#00adb5',
16 | success: '#52c41a',
17 | error: '#ff4d4f',
18 | warning: '#faad14',
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | export * from './app';
2 | export * from './router';
3 | export * from './layout';
4 |
--------------------------------------------------------------------------------
/src/config/layout.ts:
--------------------------------------------------------------------------------
1 | import { LayoutConfig } from '@/components/Layout';
2 |
3 | export const layout: LayoutConfig = {
4 | // mode: 'side',
5 | // collapsed: false,
6 | // theme: {
7 | // header: 'light',
8 | // sidebar: 'dark',
9 | // embed: 'light',
10 | // },
11 | // fixed: {
12 | // header: false,
13 | // sidebar: false,
14 | // embed: false,
15 | // },
16 | // vars: {
17 | // sidebarWidth: '200px',
18 | // sidebarCollapseWidth: '64px',
19 | // headerHeight: '48px',
20 | // headerLightColor: '#fff',
21 | // },
22 | };
23 |
--------------------------------------------------------------------------------
/src/config/router.tsx:
--------------------------------------------------------------------------------
1 | import type { RouterConfig } from '@/components/Router';
2 |
3 | import { constantsRoutes } from './routes/constants';
4 |
5 | import { dynamicRoutes } from './routes/dynamic';
6 |
7 | export const router: RouterConfig = {
8 | basePath: import.meta.env.BASE_URL,
9 | window: undefined,
10 | hash: false,
11 | routes: {
12 | constants: constantsRoutes(),
13 | dynamic: dynamicRoutes(),
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/src/config/routes/constants.tsx:
--------------------------------------------------------------------------------
1 | import type { RouteOption } from '@/components/Router';
2 |
3 | export const constantsRoutes = () => [...auth, ...errors];
4 | const auth: RouteOption[] = [
5 | {
6 | path: '/auth',
7 | children: [
8 | {
9 | name: 'auth.redirect',
10 | index: true,
11 | to: '/auth/login',
12 | meta: { hide: true },
13 | },
14 | {
15 | name: 'auth.login',
16 | path: 'login',
17 | page: 'auth/login',
18 | meta: { hide: true },
19 | },
20 | {
21 | name: 'auth.singup',
22 | path: 'signup',
23 | page: 'auth/signup',
24 | meta: { hide: true },
25 | },
26 | ],
27 | },
28 | ];
29 |
30 | const errors: RouteOption[] = [
31 | {
32 | name: '404',
33 | path: '*',
34 | page: 'errors/404',
35 | meta: { hide: true },
36 | },
37 | ];
38 |
--------------------------------------------------------------------------------
/src/config/routes/loading.tsx:
--------------------------------------------------------------------------------
1 | import { RouteOption } from '@/components/Router';
2 | import { Loading, Spinner } from '@/components/Spinner';
3 |
4 | const RouteLoading = () => {
5 | return (
6 | }
9 | />
10 | );
11 | };
12 | export const addLoading = (routes: RouteOption[]): RouteOption[] => {
13 | return routes.map((item) => {
14 | const data = {
15 | ...item,
16 | loading: RouteLoading,
17 | };
18 | if (data.children) data.children = addLoading([...data.children]);
19 | return data;
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/src/favicon.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
4 | 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
5 | -webkit-font-smoothing: antialiased;
6 | -moz-osx-font-smoothing: grayscale;
7 | }
8 |
9 | code {
10 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
11 | }
12 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2021-12-14 00:07:50 +0800
6 | * @Updated_at : 2021-12-16 14:56:38 +0800
7 | * @Path : /src/main.tsx
8 | * @Description : 入口文件,在此启动项目
9 | * @LastEditors : pincman
10 | * Copyright 2021 pincman, All Rights Reserved.
11 | *
12 | */
13 | import { enableMapSet } from 'immer';
14 | import ReactDOM from 'react-dom';
15 | // import 'virtual:windi-base.css';
16 | // import 'virtual:windi-components.css';
17 | // import 'virtual:windi-utilities.css';
18 | // import 'virtual:windi-devtools';
19 | import 'virtual:svg-icons-register';
20 |
21 | import '@/styles/index.css';
22 |
23 | import App from './App';
24 |
25 | // if (import.meta.env.DEV) {
26 | // import('antd/dist/antd.less');
27 | // }
28 |
29 | enableMapSet();
30 | ReactDOM.render(, document.getElementById('root'));
31 |
--------------------------------------------------------------------------------
/src/styles/antd.less:
--------------------------------------------------------------------------------
1 | @root-entry-name: 'variable';
2 | @layout-header-height: 48px;
3 | // =================================
4 | // ==============屏幕断点============
5 | // =================================
6 |
7 | // Extra small screen / phone
8 | @screen-xs: 480px;
9 | @screen-xs-min: @screen-xs;
10 |
11 | // Small screen / tablet
12 | @screen-sm: 576px;
13 | @screen-sm-min: @screen-sm;
14 |
15 | // Medium screen / desktop
16 | @screen-md: 768px;
17 | @screen-md-min: @screen-md;
18 |
19 | // Large screen / wide desktop
20 | @screen-lg: 992px;
21 | @screen-lg-min: @screen-lg;
22 |
23 | // Extra large screen / full hd
24 | @screen-xl: 1200px;
25 | @screen-xl-min: @screen-xl;
26 |
27 | // Extra extra large screen / large desktop
28 | @screen-2xl: 1400px;
29 | @screen-2xl-min: @screen-2xl;
30 |
31 | @screen-xs-max: (@screen-sm-min - 1px);
32 | @screen-sm-max: (@screen-md-min - 1px);
33 | @screen-md-max: (@screen-lg-min - 1px);
34 | @screen-lg-max: (@screen-xl-min - 1px);
35 |
--------------------------------------------------------------------------------
/src/styles/index.css:
--------------------------------------------------------------------------------
1 | /* stylelint-disable selector-max-id */
2 | /* stylelint-disable selector-id-pattern */
3 | @import 'tailwindcss/base';
4 | @import './tailwind/base';
5 | @import 'tailwindcss/components';
6 | @import './tailwind/components';
7 | @import 'tailwindcss/utilities';
8 | @import './tailwind/utilities';
9 |
10 | html,
11 | body,
12 | #root {
13 | width: 100%;
14 | height: 100vh !important;
15 | }
16 |
17 | *::-webkit-scrollbar {
18 | width: 6px;
19 | height: 6px;
20 | }
21 |
22 | *::-webkit-scrollbar-thumb {
23 | background-color: rgb(170 170 170 / 60%);
24 | border-radius: 4px;
25 | }
26 |
27 | *::-webkit-scrollbar-track-piece {
28 | background: #fff;
29 | }
30 |
31 | *::-webkit-scrollbar-thumb:horizontal:hover,
32 | *::-webkit-scrollbar-thumb:vertical:hover {
33 | background-color: var(--ant-info-color);
34 | }
35 | .ant-tabs-tab-remove {
36 | display: flex;
37 | }
--------------------------------------------------------------------------------
/src/styles/tailwind/base.css:
--------------------------------------------------------------------------------
1 | /* 添加自定义tailwind基础层样式,一般用于覆盖一些tailwind中默认的基础样式 */
2 |
3 | /* 如果要引用tailwind自带的值或tailwind.config.js的theme中配置的值,可以通过 "@apply"指令或"theme"函数获取 */
4 |
5 | /* 在"@layer"中添加的样式如果在程序中没有用到会在编译后被清除,如果需要强制存在于编译后的样式表,请在"@layer"外定义 */
6 |
7 | /* 示例:
8 | h1 {
9 | @apply text-2xl;
10 | } */
11 |
12 | @layer base {
13 | }
14 |
--------------------------------------------------------------------------------
/src/styles/tailwind/components.css:
--------------------------------------------------------------------------------
1 | /* 添加自定义tailwind组件层样式,一般无特殊需求可以用react组件抽象而不是在这里定义css类 */
2 |
3 | /* 如果要引用tailwind自带的值或tailwind.config.js的theme中配置的值,可以通过 "@apply"指令或"theme"函数获取 */
4 |
5 | /* 在"@layer"中添加的样式如果在程序中没有用到会在编译后被清除,如果需要强制存在于编译后的样式表,请在"@layer"外定义 */
6 |
7 | /* 示例:
8 | .btn-primary {
9 | @apply py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75;
10 | } */
11 |
12 | @layer components {
13 | }
14 |
--------------------------------------------------------------------------------
/src/styles/tailwind/utilities.css:
--------------------------------------------------------------------------------
1 | /* 添加自定义tailwindg工具层样式,可以在这里添加一些tailwind中不存在的一些样式类 */
2 |
3 | /* 如果要引用tailwind自带的值或tailwind.config.js的theme中配置的值,可以通过 "@apply"指令或"theme"函数获取 */
4 |
5 | /* 在"@layer"中添加的样式如果在程序中没有用到会在编译后被清除,如果需要强制存在于编译后的样式表,请在"@layer"外定义 */
6 |
7 | /* 示例:
8 | .content-auto {
9 | content-visibility: auto;
10 | } */
11 |
12 | @layer utilities {
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2022-01-11 20:28:30 +0800
6 | * @Updated_at : 2022-01-11 20:31:21 +0800
7 | * @Path : /src/utils/constants.ts
8 | * @Description : 常量和enum
9 | * @LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 |
14 | /**
15 | * 屏幕尺寸类型
16 | */
17 | export enum ScreenSizeType {
18 | XS = 'xs',
19 | SM = 'sm',
20 | MD = 'md',
21 | LG = 'lg',
22 | XL = 'xl',
23 | DoubleXL = '2xl',
24 | }
25 | export const screenSize: { [key in `${ScreenSizeType}`]: number } = {
26 | xs: 480,
27 | sm: 576,
28 | md: 768,
29 | lg: 992,
30 | xl: 1200,
31 | '2xl': 1400,
32 | };
33 |
--------------------------------------------------------------------------------
/src/utils/helpers.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2021-12-16 05:55:08 +0800
6 | * @Updated_at : 2022-01-08 12:58:01 +0800
7 | * @Path : /src/utils/helpers.ts
8 | * @Description : 辅助函数
9 | * @LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 | import deepmerge from 'deepmerge';
14 |
15 | /**
16 | * 检测当前值是否为Promise对象
17 | * @param promise 待检测的值
18 | */
19 | export function isPromise(promise: any): promise is PromiseLike {
20 | return !!promise && promise instanceof Promise;
21 | }
22 | /**
23 | * 检测当前函数是否为异步函数
24 | * @param callback 待检测函数
25 | */
26 | export function isAsyncFn>(
27 | callback: (...asgs: A) => Promise | R,
28 | ): callback is (...asgs: A) => Promise {
29 | const AsyncFunction = (async () => {}).constructor;
30 | return callback instanceof AsyncFunction === true;
31 | }
32 | /**
33 | * 深度合并对象
34 | * @param x 初始值
35 | * @param y 新值
36 | * @param arrayMode 对于数组采取的策略,`replace`为直接替换,`merge`为合并数组
37 | */
38 | export const deepMerge = (
39 | x: Partial,
40 | y: Partial,
41 | arrayMode: 'replace' | 'merge' = 'merge',
42 | ) => {
43 | const options: deepmerge.Options = {};
44 | if (arrayMode === 'replace') {
45 | options.arrayMerge = (_d, s, _o) => s;
46 | } else if (arrayMode === 'merge') {
47 | options.arrayMerge = (_d, s, _o) => Array.from(new Set([..._d, ...s]));
48 | }
49 | return deepmerge(x, y, options) as T2 extends T1 ? T1 : T1 & T2;
50 | };
51 | /**
52 | * 检测当前路径是否为一个URL
53 | * @param path 路径(字符串)
54 | */
55 | export const isUrl = (path: string): boolean => {
56 | if (!path.startsWith('http')) {
57 | return false;
58 | }
59 | try {
60 | const url = new URL(path);
61 | return !!url;
62 | } catch (error) {
63 | return false;
64 | }
65 | };
66 |
67 | /**
68 | * 生成一个随机浮点数
69 | */
70 | export const random = () => +(Math.random() * 60).toFixed(2);
71 | /**
72 | * 生成一个区间之间的随机数(含最大值,含最小值)
73 | * @param min 最小值
74 | * @param max 最大值
75 | */
76 | export const randomIntFrom = (min: number, max: number) => {
77 | const minc = Math.ceil(min);
78 | const maxc = Math.floor(max);
79 | return Math.floor(Math.random() * (maxc - minc + 1)) + minc;
80 | };
81 | /**
82 | * 从一个数组中随机取一个值
83 | * @param some 待取值的数组
84 | */
85 | export const randomArray = (...some: number[]) => some[randomIntFrom(0, some.length - 1)];
86 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './constants';
2 | export * from './store';
3 | export * from './helpers';
4 | export * from './hooks';
5 |
--------------------------------------------------------------------------------
/src/utils/store/helpers.ts:
--------------------------------------------------------------------------------
1 | import { capitalize } from 'lodash-es';
2 |
3 | import { DependencyList } from 'react';
4 | import { useDebounce } from 'react-use';
5 |
6 | import { isAsyncFn } from '../helpers';
7 | import { useDeepCompareMemoize } from '../hooks';
8 |
9 | import {
10 | SetupedEffectProps,
11 | SetupedState,
12 | StoreHookSelectorCreator,
13 | StoreSelectorCreator,
14 | } from './types';
15 |
16 | /**
17 | * 用于初始化自定义组件,具有避免重复设置和防抖作用
18 | * @param 选项
19 | * @param deps 依赖项(尽量别写而使用zustantd的subscribe代替,否则会导致多次渲染)
20 | */
21 | export const useStoreSetuped = (
22 | { store, callback, clear, wait }: SetupedEffectProps,
23 | deps: DependencyList = [],
24 | ) => {
25 | const depends = useDeepCompareMemoize(deps);
26 | useDebounce(
27 | () => {
28 | if (
29 | depends.every((d) => !!d) &&
30 | !store.getState().created &&
31 | !store.getState().setuped
32 | ) {
33 | store.setState((state: any) => ({ ...state, created: true }));
34 | if (isAsyncFn(callback)) {
35 | callback().then(() => {
36 | store.setState((state: any) => ({ ...state, setuped: true }));
37 | });
38 | } else {
39 | callback();
40 | store.setState((state: any) => ({ ...state, setuped: true }));
41 | }
42 | }
43 | return () => {
44 | if (clear) clear();
45 | };
46 | },
47 | wait ?? 10,
48 | depends,
49 | );
50 | };
51 |
52 | /**
53 | * 创建一个store的响应式取值器,可以通过store.getters.xxx来获取状态
54 | * @param store 需要取值的store
55 | */
56 | export const createStoreSelectors: StoreSelectorCreator = (store: any) => {
57 | store.getters = {};
58 |
59 | Object.keys(store.getState()).forEach((key) => {
60 | const selector = (state: any) => state[key];
61 | store.getters[key] = () => store(selector);
62 | });
63 |
64 | return store;
65 | };
66 | /**
67 | * 创建一个store的hooks模式的响应式取值器,可以通过store.useXxx来获取状态
68 | * @param store 需要取值的store
69 | */
70 | export const createStoreHooks: StoreHookSelectorCreator = (store: any) => {
71 | (store as any).use = {};
72 |
73 | Object.keys(store.getState()).forEach((key) => {
74 | const selector = (state: any) => state[key];
75 | store[`use${capitalize(key)}`] = () => store(selector);
76 | });
77 | return store;
78 | };
79 |
--------------------------------------------------------------------------------
/src/utils/store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 | export * from './store';
3 | export * from './helpers';
4 |
--------------------------------------------------------------------------------
/src/utils/store/store.ts:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 | import create from 'zustand';
3 | import { redux, subscribeWithSelector, devtools as zsdevtools } from 'zustand/middleware';
4 |
5 | import {
6 | ImmerStateSetterWithGet,
7 | ImmerStoreMiddlewareType,
8 | ReduxStoreCreator,
9 | SetImmberState,
10 | StoreCreator,
11 | } from './types';
12 |
13 | /**
14 | * 创建一个可供订阅的immber store
15 | * @param createState store创建函数
16 | */
17 | export const createStore: StoreCreator = (createState: any, devtools: any) => {
18 | let store: any = devtools ? zsdevtools(createState) : createState;
19 | store = create(subscribeWithSelector(ImmerMiddleware(store)));
20 | store.setState = setImmerState(store.setState) as any;
21 | return store as any;
22 | };
23 | export const createReduxStore: ReduxStoreCreator = (reducer: any, initial: any, devtools: any) => {
24 | let store: any = devtools ? zsdevtools(redux(reducer, initial)) : redux(reducer, initial);
25 | store = create(subscribeWithSelector(ImmerMiddleware(store as any)));
26 | store.setState = setImmerState(store.setState) as any;
27 | return store as any;
28 | };
29 | /**
30 | * 扩展默认的状态设置函数为immber设置函数
31 | * @param set 默认的状态设置函数
32 | */
33 | const setImmerState: SetImmberState = (set) => (partial, replace) => {
34 | const nextState = typeof partial === 'function' ? produce(partial) : partial;
35 | return set(nextState, replace);
36 | };
37 |
38 | /**
39 | * 在Immer中间件中重置set函数,使其支持immber
40 | * @param set
41 | * @param get
42 | */
43 | const setImmerStateWithGet: ImmerStateSetterWithGet = (set: any, get: any) => setImmerState(set);
44 |
45 | /**
46 | * 扩展zustand使其支持immber(同时支持`set`和`setState`)
47 | * @param config 传入中间件的creator
48 | */
49 | const ImmerMiddleware: ImmerStoreMiddlewareType = (config: any) => (set, get, api) => {
50 | api.setState = setImmerState(api.setState) as any;
51 | return config(setImmerStateWithGet(set, get), get, api);
52 | };
53 |
--------------------------------------------------------------------------------
/src/utils/timer.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author : pincman
3 | * @HomePage : https://pincman.com
4 | * @Support : support@pincman.com
5 | * @Created_at : 2021-12-29 09:13:53 +0800
6 | * @Updated_at : 2022-01-08 14:11:18 +0800
7 | * @Path : /src/utils/timer.ts
8 | * @Description : 时间函数
9 | * @LastEditors : pincman
10 | * Copyright 2022 pincman, All Rights Reserved.
11 | *
12 | */
13 | import dayjs from 'dayjs';
14 | import 'dayjs/locale/en';
15 | import 'dayjs/locale/zh-cn';
16 | import 'dayjs/locale/zh-tw';
17 | import advancedFormat from 'dayjs/plugin/advancedFormat';
18 | import customParseFormat from 'dayjs/plugin/customParseFormat';
19 | import dayOfYear from 'dayjs/plugin/dayOfYear';
20 | import localeData from 'dayjs/plugin/localeData';
21 | import timezone from 'dayjs/plugin/timezone';
22 | import utc from 'dayjs/plugin/utc';
23 | import isBetween from 'dayjs/plugin/isBetween';
24 | import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
25 | import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
26 | import objectSupport from 'dayjs/plugin/objectSupport';
27 |
28 | import { ConfigStore } from '@/components/Config/store';
29 | /**
30 | * 时间生成器参数接口
31 | */
32 | export interface TimeOptions {
33 | /** 时间对象,可以是字符串,dayjs对象,date对象等一切dayjs支持的格式 */
34 | date?: dayjs.ConfigType;
35 | /** 格式化选项 */
36 | format?: dayjs.OptionType;
37 | /** 本地化语言 */
38 | locale?: string;
39 | /** 是否为严格模式 */
40 | strict?: boolean;
41 | /** 时区 */
42 | zonetime?: string;
43 | }
44 |
45 | dayjs.extend(localeData);
46 | dayjs.extend(utc);
47 | dayjs.extend(timezone);
48 | dayjs.extend(advancedFormat);
49 | dayjs.extend(customParseFormat);
50 | dayjs.extend(dayOfYear);
51 | dayjs.extend(objectSupport);
52 | dayjs.extend(isSameOrAfter);
53 | dayjs.extend(isBetween);
54 | dayjs.extend(isSameOrBefore);
55 |
56 | /**
57 | * 根据参数和应用配置生成一个dayjs对象
58 | * @param options 配置
59 | */
60 | export function timer(options?: TimeOptions): dayjs.Dayjs {
61 | if (!options) return dayjs();
62 | options.date;
63 | const { date, format, locale, strict, zonetime } = options;
64 | const { config } = ConfigStore.getState();
65 | // 如果没有传入local或timezone则使用应用配置
66 | return dayjs(date, format, locale, strict).tz(zonetime ?? config.timezone ?? 'UTC');
67 | }
68 |
--------------------------------------------------------------------------------
/src/views/account/center/index.blade.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 |
3 | const AccountCenter: FC = () => {
4 | return 账户中心
;
5 | };
6 | export default AccountCenter;
7 |
--------------------------------------------------------------------------------
/src/views/account/setting/index.blade.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 |
3 | const AccountSetting: FC = () => {
4 | return 账户设置
;
5 | };
6 | export default AccountSetting;
7 |
--------------------------------------------------------------------------------
/src/views/auth/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './credential.form';
2 |
--------------------------------------------------------------------------------
/src/views/auth/login.blade.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 |
3 | import bgimg from '../../assets/svg/login-box-bg.svg';
4 |
5 | import { CredentialForm } from './components';
6 | // import styles from './login.module.less';
7 |
8 | const Login: FC = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |

18 |
19 |
20 | 基于Vite+React的管理面板
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 | );
34 | };
35 | export default Login;
36 |
--------------------------------------------------------------------------------
/src/views/auth/signup.blade.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 |
3 | // import type { AntdMenuOption } from '@/components/Menu';
4 |
5 | const SingUp: FC = () => {
6 | return User SingUdp
;
7 | };
8 | export default SingUp;
9 |
--------------------------------------------------------------------------------
/src/views/charts/line.blade.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 |
3 | const LineChart: FC = () => {
4 | return 折线图表
;
5 | };
6 | export default LineChart;
7 |
--------------------------------------------------------------------------------
/src/views/charts/percent.blade.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 |
3 | const PercentChart: FC = () => {
4 | return 百分比图表
;
5 | };
6 | export default PercentChart;
7 |
--------------------------------------------------------------------------------
/src/views/charts/wave.blade.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 |
3 | const WaveChart: FC = () => {
4 | return 水波图表
;
5 | };
6 | export default WaveChart;
7 |
--------------------------------------------------------------------------------
/src/views/content/articles/create.blade.tsx:
--------------------------------------------------------------------------------
1 | export default () => 新增文章
;
2 |
--------------------------------------------------------------------------------
/src/views/content/articles/list.blade.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 |
3 | import { Button } from 'antd';
4 |
5 | import { useThemeDispatch } from '@/components/Config';
6 |
7 | const ArticlesList: FC = () => {
8 | const { toggleTheme } = useThemeDispatch();
9 | return (
10 |
11 |
14 |
15 | );
16 | };
17 | export default ArticlesList;
18 |
--------------------------------------------------------------------------------
/src/views/content/categories/list.blade.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 |
3 | const CategoriesList: FC = () => {
4 | return 分类管理
;
5 | };
6 | export default CategoriesList;
7 |
--------------------------------------------------------------------------------
/src/views/content/comments/list.blade.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 |
3 | const CommentsList: FC = () => {
4 | return 评论管理
;
5 | };
6 | export default CommentsList;
7 |
--------------------------------------------------------------------------------
/src/views/content/tags/list.blade.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 |
3 | const TagsList: FC = () => {
4 | return 标签管理
;
5 | };
6 | export default TagsList;
7 |
--------------------------------------------------------------------------------
/src/views/dashboard/anlysis/index.blade.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 |
3 | const AnlysisDashboard = () => {
4 | const [ddd, setDdd] = useState('anlysis');
5 | const changeDdd = useCallback(
6 | (e: React.ChangeEvent) => setDdd(e.target.value),
7 | [],
8 | );
9 | return 分析页
;
10 | // return (
11 | //
12 | //
13 | //
14 | //
15 | //
16 | //
17 | //
18 | // );
19 | };
20 | export default AnlysisDashboard;
21 |
--------------------------------------------------------------------------------
/src/views/dashboard/monitor/components/app.tsx:
--------------------------------------------------------------------------------
1 | import { Card } from 'antd';
2 |
3 | import { useEffect, useState } from 'react';
4 |
5 | import classNames from 'classnames';
6 |
7 | import { Icon } from '@/components/Icon';
8 |
9 | import type { AppItem } from '../types';
10 |
11 | import { apps } from './data';
12 |
13 | import Trash from '~icons/bi/trash';
14 |
15 | export const ServerApp = () => {
16 | const [data, setData] = useState>([]);
17 | useEffect(() => {
18 | const clear = setTimeout(() => {
19 | setData(apps);
20 | }, 500);
21 | return () => clearTimeout(clear);
22 | }, []);
23 | return (
24 |
25 | {data.map((app, index) => (
26 |
31 | {'icon' in app ? (
32 | <>
33 |
34 |
44 | >
45 | ) : (
46 |
47 |
52 |
71 |
75 |
76 |
77 | )}
78 |
79 | ))}
80 |
81 | );
82 | };
83 |
--------------------------------------------------------------------------------
/src/views/dashboard/monitor/components/chart.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useState } from 'react';
2 |
3 | import { PercentGaugeChart } from '@/components/Chart';
4 |
5 | import { random } from '@/utils';
6 |
7 | import { useServer } from './data';
8 |
9 | export const CpuMonitor = () => {
10 | const { server, loading } = useServer('1');
11 | const num = useMemo(() => server?.cpu, [server?.cpu]);
12 | const [value, setValue] = useState(0);
13 | useEffect(() => {
14 | if (server) setValue(random());
15 | const clear = setInterval(() => {
16 | if (server) setValue(random());
17 | }, 5000);
18 | return () => clearInterval(clear);
19 | }, [server]);
20 | return (
21 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/src/views/dashboard/monitor/components/data.tsx:
--------------------------------------------------------------------------------
1 | import { useSwrFetcher } from '@/components/Fetcher';
2 |
3 | import type { AppItem, ServerItem } from '../types';
4 |
5 | import Nginx from '~icons/logos/nginx';
6 | import Mysql from '~icons/logos/mysql';
7 | import Postgresql from '~icons/logos/postgresql';
8 | import Mongodb from '~icons/logos/mongodb';
9 | import Docker from '~icons/logos/docker';
10 | import Redis from '~icons/logos/redis';
11 | import Rabbitmq from '~icons/logos/rabbitmq';
12 | import Kafka from '~icons/mdi/apache-kafka';
13 | import Node from '~icons/logos/nodejs-icon';
14 | import PHP from '~icons/logos/php';
15 | import Python from '~icons/logos/python';
16 | import Nestjs from '~icons/logos/nestjs';
17 | import Laravel from '~icons/logos/laravel';
18 | import Symfony from '~icons/logos/symfony';
19 | import Wordpress from '~icons/logos/wordpress';
20 |
21 | export const useServer = (id: string) => {
22 | const { data, error } = useSwrFetcher({ url: '/servers', params: { id } });
23 | return {
24 | server: data,
25 | loading: !error && !data,
26 | error,
27 | };
28 | };
29 |
30 | export const apps: AppItem[] = [
31 | {
32 | name: 'mysql',
33 | component: Mysql,
34 | },
35 | {
36 | name: 'nginx',
37 | component: Nginx,
38 | },
39 | {
40 | name: 'nginx',
41 | component: Node,
42 | },
43 | {
44 | name: 'nginx',
45 | component: PHP,
46 | },
47 | {
48 | name: 'nginx',
49 | component: Laravel,
50 | },
51 | {
52 | name: 'nginx',
53 | component: Symfony,
54 | },
55 | {
56 | name: 'nginx',
57 | component: Wordpress,
58 | },
59 | {
60 | name: 'nginx',
61 | component: Nestjs,
62 | },
63 | {
64 | name: 'nginx',
65 | component: Redis,
66 | },
67 | {
68 | name: 'nginx',
69 | component: Rabbitmq,
70 | },
71 | {
72 | name: 'nginx',
73 | component: Docker,
74 | },
75 | {
76 | name: 'nginx',
77 | component: Mongodb,
78 | },
79 | {
80 | name: 'nginx',
81 | component: Postgresql,
82 | },
83 | {
84 | name: 'nginx',
85 | component: Kafka,
86 | },
87 | {
88 | name: 'nginx',
89 | component: Python,
90 | },
91 | ];
92 |
--------------------------------------------------------------------------------
/src/views/dashboard/monitor/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './chart';
2 | export * from './app';
3 | export * from './info';
4 |
--------------------------------------------------------------------------------
/src/views/dashboard/monitor/components/info.tsx:
--------------------------------------------------------------------------------
1 | import ProDescriptions from '@ant-design/pro-descriptions';
2 | import { Badge, Typography } from 'antd';
3 | import { memo, useCallback } from 'react';
4 |
5 | import { serverStatus } from '../constants';
6 |
7 | import { useServer } from './data';
8 |
9 | const Label: FC<{ text: string }> = memo(({ text }) => (
10 | {text}
11 | ));
12 | export const ServerInfo: FC = () => {
13 | const { server, loading } = useServer('1');
14 | const mbToGb = useCallback(
15 | (value: number) => (value / 1024).toFixed(2).replace(/\.00$/, ''),
16 | [],
17 | );
18 | return (
19 |
25 | }>
26 |
27 | {server?.os}
28 |
29 |
30 | }>
31 | {server?.cpu && `${server.cpu}核`}
32 |
33 | }>
34 | {server?.memory && `${mbToGb(server.memory)}G`}
35 |
36 | {server?.disk &&
37 | server.disk.map((i, index) => (
38 | }>
39 | {`${mbToGb(i.value)}G (${i.path})`}
40 |
41 | ))}
42 | }>
43 | {server?.status && (
44 |
48 | )}
49 |
50 | }>
51 | {server?.insetIp && (
52 |
53 | {server.insetIp}
54 |
55 | )}
56 |
57 | }>
58 | {server?.publicIp && (
59 | {server.publicIp}
60 | )}
61 |
62 |
63 | );
64 | };
65 |
--------------------------------------------------------------------------------
/src/views/dashboard/monitor/constants.ts:
--------------------------------------------------------------------------------
1 | import type { ServerStatusOption } from './types';
2 |
3 | export enum ServerStatus {
4 | RUNNING = 'running',
5 | RESTARTING = 'restarting',
6 | SHUTDOWN = 'shutdown',
7 | }
8 | export const serverStatus: ServerStatusOption = {
9 | running: { text: '运行中', badge: 'success' },
10 | restarting: { text: '重启中', badge: 'processing' },
11 | shutdown: { text: '已关机', badge: 'default' },
12 | };
13 |
--------------------------------------------------------------------------------
/src/views/dashboard/monitor/index.blade.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, Col, Input, Row } from 'antd';
2 | import { useCallback, useState } from 'react';
3 |
4 | import { CpuMonitor, ServerApp, ServerInfo } from './components';
5 |
6 | const analysisTabs = [
7 | {
8 | key: 'network',
9 | tab: '网络',
10 | },
11 | {
12 | key: 'load',
13 | tab: '负载',
14 | },
15 | {
16 | key: 'io',
17 | tab: '读写io',
18 | },
19 | ];
20 | const analysisPanel = {
21 | network: load content
,
22 | // network: ,
23 | load: load content
,
24 | io: sdsd
,
25 | };
26 | const Dashboard = () => {
27 | const [analysis, setAnalysis] = useState('network');
28 | const changeAnalysis = useCallback((tab: string) => {
29 | setAnalysis(tab);
30 | }, []);
31 | const [ddd, setDdd] = useState('keepAlive测试');
32 | const changeDdd = useCallback(
33 | (e: React.ChangeEvent) => setDdd(e.target.value),
34 | [],
35 | );
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
53 |
56 |
57 |
58 |
59 | >
60 | }
61 | >
62 |
63 |
64 |
65 |
66 | {/* */}
67 | {/* */}
68 | {/* */}
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
84 | {analysisPanel[analysis]}
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | );
93 | };
94 | export default Dashboard;
95 |
--------------------------------------------------------------------------------
/src/views/dashboard/monitor/index.blade.tsx.back:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 |
3 | const MonitorDashboard = () => {
4 | const [ddd, setDdd] = useState('monitor');
5 | const changeDdd = useCallback(
6 | (e: React.ChangeEvent) => setDdd(e.target.value),
7 | [],
8 | );
9 | return keeplive测试
;
10 | // return (
11 | //
12 | //
13 | //
14 | //
15 | //
16 | //
17 | //
18 | //
19 | //
20 | //
21 | //
22 | //
23 | // );
24 | };
25 | export default MonitorDashboard;
26 |
--------------------------------------------------------------------------------
/src/views/dashboard/monitor/index.module.less:
--------------------------------------------------------------------------------
1 | .navTabs {
2 | &:global(.ant-tabs > .ant-tabs-nav) {
3 | height: 2.25rem !important;
4 | }
5 |
6 | &:global(.ant-tabs > .ant-tabs-nav .ant-tabs-nav-list) {
7 | min-width: var(--tab-container-width) !important;
8 | }
9 | &:global(.ant-tabs-card.ant-tabs-small > .ant-tabs-nav .ant-tabs-tab) {
10 | width: 5rem;
11 | justify-content: space-between;
12 | padding: 0.35rem 0.5rem;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/views/dashboard/monitor/types.ts:
--------------------------------------------------------------------------------
1 | import type { CSSProperties } from 'react';
2 |
3 | import type { PresetStatusColorType } from 'antd/lib/_util/colors';
4 |
5 | import type { IconComponent, IconName } from '@/components/Icon';
6 |
7 | import type { ServerStatus } from './constants';
8 |
9 | export interface ServerItem {
10 | id: string;
11 | os: string;
12 | cpu: number;
13 | memory: number;
14 | disk: Array<{ path: string; value: number }>;
15 | status: `${ServerStatus}`;
16 | insetIp: string;
17 | publicIp: string;
18 | }
19 | export type ServerStatusOption = {
20 | [key in `${ServerStatus}`]: {
21 | text: string;
22 | badge: PresetStatusColorType;
23 | };
24 | };
25 | export type AppItem = { name: string; style?: CSSProperties; className?: string } & (
26 | | {
27 | icon: IconName;
28 | }
29 | | {
30 | component: IconComponent;
31 | }
32 | );
33 |
--------------------------------------------------------------------------------
/src/views/dashboard/utils.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 | import type { TopLevelFormatterParams } from 'echarts/types/dist/shared';
3 |
4 | export const random = () => +(Math.random() * 60).toFixed(2);
5 | export const randomIntFrom = (min: number, max: number) => {
6 | const minc = Math.ceil(min);
7 | const maxc = Math.floor(max);
8 | return Math.floor(Math.random() * (maxc - minc + 1)) + minc; // 含最大值,含最小值
9 | };
10 | export const randomArray = (...some: number[]) => some[randomIntFrom(0, some.length - 1)];
11 |
12 | export const getTooltipFomatter = (
13 | params: T extends Array ? C : T,
14 | ) => `
15 |
${dayjs(
16 | params.value[0],
17 | ).format('M-D HH:mm:ss')}
18 |
19 |
20 |
21 | ${params.marker}
22 |
${
23 | params.seriesName
24 | }
25 |
${
26 | params.value[1]
27 | }mbps
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
`;
36 |
--------------------------------------------------------------------------------
/src/views/dashboard/workspace/index.blade.tsx:
--------------------------------------------------------------------------------
1 | import { Col, Input, Row } from 'antd';
2 | import { useCallback, useState } from 'react';
3 |
4 | const WorkspaceDashboard = () => {
5 | const [ddd, setDdd] = useState('workspace');
6 | const changeDdd = useCallback(
7 | (e: React.ChangeEvent) => setDdd(e.target.value),
8 | [],
9 | );
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 | export default WorkspaceDashboard;
21 |
--------------------------------------------------------------------------------
/src/views/errors/403.blade.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 |
3 | const Forbidden: FC = () => {
4 | return 403 Forbidden!
;
5 | };
6 | export default Forbidden;
7 |
--------------------------------------------------------------------------------
/src/views/errors/404.blade.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 |
3 | const NotFound: FC = () => {
4 | return 404 NOT FOUND!
;
5 | };
6 | export default NotFound;
7 |
--------------------------------------------------------------------------------
/src/views/errors/500.blade.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 |
3 | const GateWay: FC = () => {
4 | return 500 gateway error!
;
5 | };
6 | export default GateWay;
7 |
--------------------------------------------------------------------------------
/src/views/layouts/components/drawer/constants.ts:
--------------------------------------------------------------------------------
1 | import { ColorConfig } from '@/components/Config';
2 | import { LayoutMode } from '@/components/Layout/constants';
3 |
4 | export enum LayoutTheme {
5 | DarkLight = 'dark-light',
6 | LightDark = 'light-dark',
7 | LIGHTLIGHT = 'light-light',
8 | DARKDARK = 'dark-dark',
9 | }
10 | export const LayoutModeList: Array<{ title: string; type: `${LayoutMode}` }> = [
11 | {
12 | title: '左侧菜单',
13 | type: LayoutMode.SIDE,
14 | },
15 | {
16 | title: '左侧菜单+顶部LOGO',
17 | type: LayoutMode.CONTENT,
18 | },
19 |
20 | {
21 | title: '顶部菜单',
22 | type: LayoutMode.TOP,
23 | },
24 | {
25 | title: '嵌入双菜单',
26 | type: LayoutMode.EMBED,
27 | },
28 | ];
29 |
30 | export const LayoutThemeList: Array<{ title: string; type: `${LayoutTheme}` }> = [
31 | {
32 | title: '左侧暗-顶部亮',
33 | type: LayoutTheme.DarkLight,
34 | },
35 | {
36 | title: '左侧亮-顶部暗',
37 | type: LayoutTheme.LightDark,
38 | },
39 |
40 | {
41 | title: '左侧亮-顶部亮',
42 | type: LayoutTheme.LIGHTLIGHT,
43 | },
44 | {
45 | title: '左侧暗-顶部暗',
46 | type: LayoutTheme.DARKDARK,
47 | },
48 | ];
49 | export const ColorList: Array<{ title: string; type: keyof ColorConfig }> = [
50 | {
51 | title: '主色',
52 | type: 'primary',
53 | },
54 | {
55 | title: '信息',
56 | type: 'info',
57 | },
58 | {
59 | title: '成功',
60 | type: 'success',
61 | },
62 | {
63 | title: '错误',
64 | type: 'error',
65 | },
66 | {
67 | title: '警告',
68 | type: 'warning',
69 | },
70 | ];
71 |
--------------------------------------------------------------------------------
/src/views/layouts/components/drawer/hooks.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 |
3 | export const DrawerContext = createContext(false);
4 | export const ChangeDrawerContext = createContext<(show: boolean) => void>((show: boolean) => {});
5 | export const useDrawer = () => useContext(DrawerContext);
6 | export const useDrawerChange = () => useContext(ChangeDrawerContext);
7 |
--------------------------------------------------------------------------------
/src/views/layouts/components/header/index.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | width: 100%;
3 |
4 | :global {
5 | & .logo {
6 | width: var(--sidebar-width);
7 | }
8 | }
9 | }
10 |
11 | .header-fixed {
12 | position: fixed;
13 | right: 0;
14 | z-index: 1;
15 | width: calc(100% - var(--sidebar-width));
16 | }
17 |
--------------------------------------------------------------------------------
/src/views/layouts/components/sidebar/logo.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties } from 'react';
2 |
3 | export const Logo: FC<{ style?: CSSProperties }> = ({ style }) => {
4 | return ;
5 | };
6 |
--------------------------------------------------------------------------------
/src/views/layouts/components/tabs/index.module.less:
--------------------------------------------------------------------------------
1 | .container {
2 | & > :global(.ant-tabs .ant-tabs-nav-list > .ant-tabs-tab) {
3 | border-radius: 4px;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/views/layouts/master.blade.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 |
3 | import { Outlet } from 'react-router-dom';
4 |
5 | import { layout } from '@/config';
6 |
7 | import { RouteComponentProps } from '@/components/Router';
8 |
9 | import { BasicLayout } from './components';
10 |
11 | const MasterLayout: FC<{ route: RouteComponentProps }> = ({ route }) => {
12 | return (
13 |
14 |
15 |
16 | );
17 | };
18 | export default MasterLayout;
19 |
--------------------------------------------------------------------------------
/src/views/layouts/styles/mixins.less:
--------------------------------------------------------------------------------
1 | #fixed() {
2 | .fixed-container {
3 | height: 100vh;
4 | overflow: hidden;
5 | }
6 |
7 | .fixed-sidebar() {
8 | position: fixed;
9 | left: 0;
10 | z-index: 99;
11 | height: 100vh;
12 | overflow: hidden;
13 |
14 | & > :global(.ant-layout-sider-children) {
15 | display: flex;
16 | flex-direction: column;
17 |
18 | & > :global(.fixed-sidebar-content) {
19 | flex: auto;
20 | overflow: auto;
21 |
22 | &::-webkit-scrollbar {
23 | width: 3px;
24 | height: 3px;
25 | }
26 | }
27 | }
28 | }
29 |
30 | .fixed-header(@width: 100%) {
31 | position: fixed;
32 | right: 0;
33 | z-index: 1;
34 | width: @width;
35 | }
36 |
37 | .layout-container(@left: 0, @top: 0) {
38 | margin-top: @top;
39 | margin-left: @left;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/views/layouts/styles/variables.less:
--------------------------------------------------------------------------------
1 | @side-sidebar-fixed: layout-side-sidebar-fixed;
2 | @side-header-fixed: layout-side-header-fixed;
3 | @content-sidebar-fixed: layout-content-sidebar-fixed;
4 | @content-header-fixed: layout-content-header-fixed;
5 | @top-header-fixed: layout-top-header-fixed;
6 | @embed-sidebar-fixed: layout-embed-sidebar-fixed;
7 | @embed-header-fixed: layout-embed-header-fixed;
8 | @embed-embed-fixed: layout-embed-embed-fixed;
9 |
10 | @value side-header: :global(.ant-layout > .ant-layout-header);
11 | @value side-content: :global(.ant-layout > .ant-layout-content);
12 | @value side-collapsed-header: :global(.ant-layout-sider-collapsed + .ant-layout > .ant-layout-header);
13 | @value side-collapsed-content: :global(.ant-layout-sider-collapsed+.ant-layout>.ant-layout-content);
14 | @value content-main: :global(.layout-main);
15 | @value content-layout: :global(.layout-main > .ant-layout);
16 | @value content-sidebar: :global(.layout-main > .ant-layout > .ant-layout-sider);
17 | @value content-content: :global(.layout-main > .ant-layout > .ant-layout-content);
18 | @value content-collapsed-content: :global(.layout-main > .ant-layout > .ant-layout-sider-collapsed + .ant-layout-content);
19 | @value embed-main: :global(.layout-main);
20 | @value embed-wrapper: :global(.layout-main > .ant-layout);
21 | @value embed-embedbar: :global(.layout-main > .ant-layout > .ant-layout-sider);
22 | @value embed-inner: :global(.layout-main > .ant-layout > .ant-layout);
23 | @value embed-header: :global(.layout-main > .ant-layout > .ant-layout > .ant-layout-header);
24 | @value embed-content: :global(.layout-main > .ant-layout > .ant-layout > .ant-layout-content);
25 | @value embed-collapsed-inner: :global(.layout-main > .ant-layout > .ant-layout-sider-collapsed + .ant-layout);
26 | @value embed-collapsed-header: :global(.layout-main > .ant-layout > .ant-layout-sider-collapsed + .ant-layout> .ant-layout-header);
27 | @value embed-collapsed-content :global(.layout-main > .ant-layout > .ant-layout-sider-collapsed + .ant-layout > .ant-layout-content);
28 |
--------------------------------------------------------------------------------
/src/views/media/index.blade.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 |
3 | const MediaManage: FC = () => {
4 | return 文件管理
;
5 | };
6 | export default MediaManage;
7 |
--------------------------------------------------------------------------------
/src/views/setting/index.blade.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 |
3 | const Setting: FC = () => {
4 | return 系统设置
;
5 | };
6 | export default Setting;
7 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/stylelint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | customSyntax: 'postcss-less',
3 | extends: [
4 | // 'stylelint-config-recommended-scss',
5 | 'stylelint-config-standard',
6 | 'stylelint-config-css-modules',
7 | 'stylelint-config-recess-order',
8 | 'stylelint-prettier/recommended',
9 | ],
10 | rules: {
11 | 'selector-type-no-unknown': null,
12 | 'selector-class-pattern': null,
13 | 'custom-property-pattern': null,
14 | 'no-duplicate-selectors': null, // 取消禁止重复定义,这样可以在css module中单独定义变量
15 | 'block-no-empty': true, // 禁止出现空块
16 | 'declaration-empty-line-before': 'never',
17 | 'declaration-block-no-duplicate-properties': true, // 在声明的块中中禁止出现重复的属性
18 | 'declaration-block-no-redundant-longhand-properties': true, // 禁止使用可以缩写却不缩写的属性
19 | 'shorthand-property-no-redundant-values': true, // 禁止在简写属性中使用冗余值
20 | 'color-hex-length': 'short', // 指定十六进制颜色是否使用缩写
21 | 'comment-no-empty': true, // 禁止空注释
22 | 'font-family-name-quotes': 'always-unless-keyword', // 指定字体名称是否需要使用引号引起来 | 期待每一个不是关键字的字体名都使用引号引起来
23 | 'font-weight-notation': 'numeric', // 要求使用数字或命名的 (可能的情况下) font-weight 值
24 | 'function-url-quotes': 'always', // 要求或禁止 url 使用引号
25 | 'property-no-vendor-prefix': true, // 禁止属性使用浏览器引擎前缀
26 | 'value-no-vendor-prefix': true, // 禁止给值添加浏览器引擎前缀
27 | 'selector-no-vendor-prefix': true, // 禁止使用浏览器引擎前缀
28 | 'no-descending-specificity': null, // 禁止低优先级的选择器出现在高优先级的选择器之后
29 | 'property-no-unknown': [
30 | true,
31 | {
32 | ignoreProperties: [
33 | // CSS Modules composition
34 | // https://github.com/css-modules/css-modules#composition
35 | 'composes',
36 | ],
37 | },
38 | ],
39 |
40 | 'selector-pseudo-class-no-unknown': [
41 | true,
42 | {
43 | ignorePseudoClasses: [
44 | // CSS Modules :global scope
45 | // https://github.com/css-modules/css-modules#exceptions
46 | 'global',
47 | 'local',
48 | ],
49 | },
50 | ],
51 | 'rule-empty-line-before': [
52 | // 要求或禁止在规则声明之前有空行
53 | 'always-multi-line',
54 | {
55 | except: ['first-nested'],
56 | ignore: ['after-comment'],
57 | },
58 | ],
59 | 'at-rule-empty-line-before': [
60 | // 要求或禁止在 at 规则之前有空行
61 | 'always',
62 | {
63 | except: ['blockless-after-same-name-blockless', 'first-nested'],
64 | ignore: ['after-comment'],
65 | },
66 | ],
67 | 'comment-empty-line-before': [
68 | // 要求或禁止在注释之前有空行
69 | 'always',
70 | {
71 | except: ['first-nested'],
72 | ignore: ['stylelint-commands'],
73 | },
74 | ],
75 | },
76 | };
77 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | darkMode: 'class',
3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4 | theme: {
5 | screens: {
6 | xs: '480px',
7 | sm: '576px',
8 | md: '768px',
9 | lg: '992px',
10 | xl: '1200px',
11 | '2xl': '1400px',
12 | },
13 | extend: {},
14 | },
15 | // 自定义样式请通过 src/styles/tailwind中的样式实现,不建议通过插件添加
16 | plugins: [],
17 | };
18 |
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["./src", "./test", "./typings", "./build", "./mock", "**.js", "**.ts"],
4 | "exclude": ["back", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "jsx": "react-jsx",
5 | "target": "ESNext",
6 | "module": "esnext",
7 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
8 | "moduleResolution": "Node",
9 | "skipLibCheck": true,
10 | "allowSyntheticDefaultImports": true,
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "noEmit": true,
14 | "declaration": true,
15 | "removeComments": true,
16 | "experimentalDecorators": true,
17 | "alwaysStrict": true,
18 | "sourceMap": true,
19 | "incremental": true,
20 | "forceConsistentCasingInFileNames": true,
21 | "esModuleInterop": true,
22 | "noUnusedLocals": true,
23 | "noImplicitReturns": true,
24 | "noFallthroughCasesInSwitch": true,
25 | "pretty": true,
26 | "noImplicitAny": true,
27 | "allowJs": true,
28 | "suppressImplicitAnyIndexErrors": true,
29 | "importsNotUsedAsValues": "remove",
30 | "outDir": "./dist",
31 | "baseUrl": "./",
32 | "paths": {
33 | "@/*": ["src/*"]
34 | },
35 | "types": ["unplugin-icons/types/react"]
36 | },
37 | "include": ["./src", "./mock", "./typings/**/*.d.ts"]
38 | }
39 |
--------------------------------------------------------------------------------
/typings/global.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/ban-types */
2 | /**
3 | * 获取数组中元素的类型
4 | */
5 | declare type ArrayItem = A extends readonly (infer T)[] ? T : never;
6 | /**
7 | * 过滤类型,去除U中T不包含的类型
8 | */
9 | declare type Filter = T extends U ? T : never;
10 |
11 | /**
12 | * 反向过滤类型,去除U中T包含的类型
13 | */
14 | declare type Diff = T extends U ? never : T;
15 |
16 | /**
17 | * 获取一个对象的值类型
18 | */
19 | declare type ValueOf = T[keyof T];
20 | /**
21 | * React组件简写
22 | */
23 | declare type FC = React.FunctionComponent
;
24 | /**
25 | * 获取一个React组件的props类型
26 | */
27 | declare type ReactProps = T extends FC ? C : never;
28 |
29 | declare type RePartial = {
30 | [P in keyof T]?: T[P] extends (infer U)[] | undefined
31 | ? RePartial[]
32 | : T[P] extends object | undefined
33 | ? T[P] extends ((...args: any[]) => any) | ClassType | undefined
34 | ? T[P]
35 | : RePartial
36 | : T[P];
37 | };
38 | declare type ReRequired = {
39 | [P in keyof T]-?: T[P] extends (infer U)[] | undefined
40 | ? ReRequired[]
41 | : T[P] extends object | undefined
42 | ? T[P] extends ((...args: any[]) => any) | ClassType | undefined
43 | ? T[P]
44 | : ReRequired
45 | : T[P];
46 | };
47 | declare type RecordAny = Record;
48 | declare type RecordNever = Record;
49 | declare type RecordAnyOrNever = RecordAny | RecordNever;
50 | declare type RecordScalable = T &
51 | (U extends Record ? RecordNever : U);
52 | /**
53 | * 一个类的类型
54 | */
55 | declare type ClassInstanceType any> =
56 | T extends abstract new (...args: any) => infer R ? R : never;
57 | declare type ClassType = { new (...args: any[]): T };
58 | declare type ObjectType = ClassType | ((...args: any[]) => any);
59 | // type ClassInstanceType = T extends { new (...args: any[]): infer U } ? U : never;
60 |
--------------------------------------------------------------------------------
/typings/module.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'antd/dist/default-theme';
2 | declare module 'antd/dist/theme';
3 | declare module 'dequal' {
4 | function dequal(foo: any, bar: any): boolean;
5 | }
6 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { UserConfig, ConfigEnv, defineConfig } from 'vite';
2 |
3 | import { getConfig } from './build';
4 |
5 | // https://vitejs.dev/config/
6 |
7 | export default defineConfig((params: ConfigEnv): UserConfig => getConfig(params));
8 |
--------------------------------------------------------------------------------