新项目, antd6, react19
This commit is contained in:
72
src/App.tsx
Normal file
72
src/App.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { App as AntdApp, ConfigProvider, notification } from 'antd';
|
||||
import type { ArgsProps } from 'antd/es/notification';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import { useEffect } from 'react';
|
||||
import { headerHeight } from './configs/config';
|
||||
import { routes } from './configs/routes';
|
||||
import Router from './router/Router';
|
||||
import { getDevice } from './utils/common';
|
||||
import { notificationEventBus } from './utils/EventBus';
|
||||
|
||||
function App() {
|
||||
const [api, contextHolder] = notification.useNotification({
|
||||
stack: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
window.dfConfig = {
|
||||
isPhone: getDevice() == 'phone',
|
||||
tableStickyOffsetHeader: headerHeight,
|
||||
language: 'zh-cn',
|
||||
vhUnit: CSS.supports('height', '100dvh') ? 'dvh' : 'vh',
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
notificationEventBus.onOpen((data) => {
|
||||
api.open(data as ArgsProps);
|
||||
});
|
||||
notificationEventBus.onClose((key: string) => {
|
||||
api.destroy(key);
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
locale={zhCN}
|
||||
componentSize='middle'
|
||||
button={{
|
||||
autoInsertSpace: false,
|
||||
}}
|
||||
theme={{
|
||||
token: {
|
||||
// borderRadius: 4,
|
||||
colorText: '#000',
|
||||
colorBorder: '#ddd',
|
||||
},
|
||||
components: {
|
||||
Table: {
|
||||
borderColor: '#ddd',
|
||||
//! 禁用动效
|
||||
motion: false,
|
||||
algorithm: true,
|
||||
},
|
||||
Input: {
|
||||
motion: false,
|
||||
algorithm: true,
|
||||
},
|
||||
Form: {
|
||||
itemMarginBottom: 16,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{contextHolder}
|
||||
<AntdApp notification={{ stack: false }}>
|
||||
<Router routes={routes} />
|
||||
</AntdApp>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
BIN
src/assets/loginBg.jpg
Normal file
BIN
src/assets/loginBg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
1
src/assets/react.svg
Normal file
1
src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
73
src/components/Footer/MonitorUpdate.tsx
Normal file
73
src/components/Footer/MonitorUpdate.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Button } from 'antd';
|
||||
import { type FC, useEffect, useRef } from 'react';
|
||||
import { Colors } from '@/configs/config';
|
||||
import { notificationEventBus } from '@/utils/EventBus';
|
||||
import { GapBox } from '../GapBox';
|
||||
|
||||
/** 监听网站版本更新 */
|
||||
const MonitorUpdate: FC = () => {
|
||||
const timerRef = useRef<any>(null);
|
||||
const version = useRef('');
|
||||
|
||||
useEffect(() => {
|
||||
timerRef.current = setInterval(() => {
|
||||
if (import.meta.env.DEV) {
|
||||
return;
|
||||
}
|
||||
fetch('/ver.txt')
|
||||
.then((res) => res.text())
|
||||
.then(async (res) => {
|
||||
if (version.current == '') {
|
||||
version.current = res;
|
||||
}
|
||||
if (res != version.current) {
|
||||
clearInterval(timerRef.current);
|
||||
notificationEventBus.emit({
|
||||
key: 'MonitorUpdate',
|
||||
title: '有新版本',
|
||||
description: (
|
||||
<div>
|
||||
<div>发现系统版本更新,请刷新页面</div>
|
||||
<div
|
||||
style={{
|
||||
color: Colors.error,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
刷新前,请先保存页面数据!!!
|
||||
</div>
|
||||
<GapBox style={{ marginTop: 12, justifyContent: 'center' }}>
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={() => {
|
||||
location.reload();
|
||||
}}
|
||||
>
|
||||
确认刷新页面
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
notificationEventBus.emitClose('MonitorUpdate');
|
||||
version.current = res;
|
||||
}}
|
||||
>
|
||||
我知道了
|
||||
</Button>
|
||||
</GapBox>
|
||||
</div>
|
||||
),
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
return () => {
|
||||
clearInterval(timerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
export default MonitorUpdate;
|
||||
13
src/components/Footer/index.module.css
Normal file
13
src/components/Footer/index.module.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
text-decoration: none;
|
||||
color: #1677ff;
|
||||
}
|
||||
34
src/components/Footer/index.tsx
Normal file
34
src/components/Footer/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { CSSProperties, FC } from 'react';
|
||||
import { DefaultERPName } from '@/configs/config';
|
||||
import styles from './index.module.css';
|
||||
import MonitorUpdate from './MonitorUpdate';
|
||||
|
||||
type IProps = {
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
const filings: any = {
|
||||
'd.com': '闽ICP备14007219号-9',
|
||||
};
|
||||
|
||||
const Footer: FC<IProps> = (props) => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<div className={styles.footer} style={{ ...props.style }}>
|
||||
<div style={{ position: 'fixed', top: -220, left: -220 }}>
|
||||
<input type='text' style={{ width: 0, height: 0 }} />
|
||||
<input type='password' style={{ width: 0, height: 0 }} />
|
||||
</div>
|
||||
<div style={{ color: '#666', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<span>© {`${currentYear} ${DefaultERPName}`}</span>
|
||||
<a href='https://beian.miit.gov.cn' target='_blank' rel='noreferrer'>
|
||||
{filings[location.hostname] || '闽ICP备14007219号-15'}
|
||||
</a>
|
||||
</div>
|
||||
<MonitorUpdate />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
39
src/components/FormPlugin/index.module.css
Normal file
39
src/components/FormPlugin/index.module.css
Normal file
@@ -0,0 +1,39 @@
|
||||
.item {
|
||||
margin-bottom: var(--item-margin-bottom, 12px);
|
||||
}
|
||||
|
||||
.itemLabel {
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
width: var(--labelWidth, 84px);
|
||||
}
|
||||
|
||||
.itemLabel.required::before {
|
||||
color: #ff4d4f;
|
||||
width: 12px;
|
||||
font-family: SimSun, sans-serif;
|
||||
content: "*";
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.itemLabel.colon::after {
|
||||
content: ":";
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.itemControl {
|
||||
display: flex;
|
||||
min-height: 32px;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.itemControl > div {
|
||||
width: 100%;
|
||||
}
|
||||
114
src/components/FormPlugin/index.tsx
Normal file
114
src/components/FormPlugin/index.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { Col, Row } from 'antd';
|
||||
import type React from 'react';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import { Colors } from '@/configs/config';
|
||||
import { isNumber } from '@/utils/common.ts';
|
||||
import styles from './index.module.css';
|
||||
|
||||
type ISearchFormPluginProps = {
|
||||
style?: React.CSSProperties & {
|
||||
'--labelWidth'?: string;
|
||||
'--item-margin-bottom'?: string;
|
||||
};
|
||||
children?: React.ReactNode;
|
||||
labelWidth?: number | string;
|
||||
itemMarginBottom?: number | string;
|
||||
gutter?: number;
|
||||
};
|
||||
|
||||
/** 响应式栅格 FormItemPlugin默认值 xs = 24, sm = 24, md = 12, lg = 8, xl = 6, xxl = 6*/
|
||||
export type ICol = {
|
||||
/** 窗口宽度 < 576px */
|
||||
xs?: number;
|
||||
/** 窗口宽度 ≥ 576px */
|
||||
sm?: number;
|
||||
/** 窗口宽度 ≥ 768px */
|
||||
md?: number;
|
||||
/** 窗口宽度 ≥ 992px */
|
||||
lg?: number;
|
||||
/** 窗口宽度 ≥ 1200px */
|
||||
xl?: number;
|
||||
/** 窗口宽度 ≥ 1600px */
|
||||
xxl?: number;
|
||||
};
|
||||
|
||||
/** 单纯表单样式简化版插件 */
|
||||
export const FormPlugin: FC<PropsWithChildren<ISearchFormPluginProps>> = (props) => {
|
||||
const { style, children, labelWidth, gutter = 0, itemMarginBottom } = props;
|
||||
|
||||
return (
|
||||
<Row
|
||||
style={{
|
||||
'--labelWidth': isNumber(labelWidth) ? `${labelWidth}px` : labelWidth,
|
||||
'--item-margin-bottom': isNumber(itemMarginBottom) ? `${itemMarginBottom}px` : itemMarginBottom,
|
||||
...style,
|
||||
}}
|
||||
gutter={[gutter, 0]}
|
||||
>
|
||||
{children}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
type SearchFormPluginItemProps = {
|
||||
col?: ICol;
|
||||
/** 统一设置 col */
|
||||
allCol?: number;
|
||||
style?: React.CSSProperties;
|
||||
/** 是否显示冒号, 默认为 true */
|
||||
colon?: boolean;
|
||||
label?: React.ReactNode;
|
||||
labelStyle?: React.CSSProperties;
|
||||
controlStyle?: React.CSSProperties;
|
||||
labelWidth?: number | string;
|
||||
/** 是否必填显示星号 */
|
||||
required?: boolean;
|
||||
errorMsg?: React.ReactNode;
|
||||
// name?: string;
|
||||
};
|
||||
|
||||
export const FormItemPlugin: FC<PropsWithChildren<SearchFormPluginItemProps>> = (props) => {
|
||||
const {
|
||||
col = {},
|
||||
style,
|
||||
colon = true,
|
||||
label,
|
||||
labelStyle,
|
||||
labelWidth,
|
||||
children,
|
||||
required,
|
||||
allCol,
|
||||
controlStyle,
|
||||
} = props;
|
||||
const { xs = 24, sm = 24, md = 12, lg = 8, xl = 6, xxl = 6 } = col;
|
||||
|
||||
return (
|
||||
<Col
|
||||
className={styles.item}
|
||||
style={{ display: 'flex', alignItems: 'flex-start', ...style }}
|
||||
xs={allCol || xs}
|
||||
sm={allCol || sm}
|
||||
md={allCol || md}
|
||||
lg={allCol || lg}
|
||||
xl={allCol || xl}
|
||||
xxl={allCol || xxl}
|
||||
>
|
||||
{label ? (
|
||||
<label
|
||||
htmlFor=''
|
||||
className={`${styles.itemLabel} ${required ? styles.required : ''} ${colon ? styles.colon : ''}`}
|
||||
// htmlFor={name}
|
||||
style={{ width: labelWidth, ...labelStyle }}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
) : null}
|
||||
<div style={{ flex: 1, minWidth: 0, position: 'relative', ...controlStyle }} className={styles.itemControl}>
|
||||
{children}
|
||||
<div style={{ position: 'absolute', top: '100%', left: 0, color: Colors.error, lineHeight: 1.1 }}>
|
||||
{props.errorMsg}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
);
|
||||
};
|
||||
28
src/components/GapBox.tsx
Normal file
28
src/components/GapBox.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type React from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
type IProps = {
|
||||
style?: React.CSSProperties;
|
||||
gap?: number; // 间距
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export const GapBox: React.FC<PropsWithChildren<IProps>> = ({ children, onClick, style, title, gap }) => {
|
||||
return (
|
||||
<div
|
||||
title={title}
|
||||
style={{
|
||||
display: 'flex',
|
||||
columnGap: gap ?? 8,
|
||||
rowGap: gap ?? 8,
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
...style,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
23
src/components/Header/HeaderUserInfo.tsx
Normal file
23
src/components/Header/HeaderUserInfo.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Button, Popconfirm } from 'antd';
|
||||
import { useUserStore } from '@/store/UserStore';
|
||||
import { GapBox } from '../GapBox';
|
||||
|
||||
export const HeaderUserInfo: React.FC = () => {
|
||||
const userInfo = useUserStore().user;
|
||||
|
||||
return (
|
||||
<GapBox>
|
||||
<div>{userInfo.login_name}</div>
|
||||
<Popconfirm
|
||||
title='确定要退出登录吗?'
|
||||
onConfirm={() => {
|
||||
location.hash = '#/login';
|
||||
}}
|
||||
>
|
||||
<Button size='small' variant='text' color='primary'>
|
||||
退出登录
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</GapBox>
|
||||
);
|
||||
};
|
||||
52
src/components/ModalPlugin.tsx
Normal file
52
src/components/ModalPlugin.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { App, Modal } from 'antd';
|
||||
import type { ModalProps } from 'antd/es/modal/interface';
|
||||
import type React from 'react';
|
||||
|
||||
type IProps = ModalProps & {
|
||||
/** 关闭是否需要确认弹框 boolean */
|
||||
confirm?: boolean;
|
||||
/** 关闭确认弹框内容 React.ReactNode */
|
||||
confirmContent?: React.ReactNode;
|
||||
};
|
||||
|
||||
/** Modal对话框简单封装, 支持确认退出
|
||||
*
|
||||
* @注意 destroyOnHidden 默认为true与antd文档相反
|
||||
* @注意 centered 默认为true与antd文档相反
|
||||
* @注意 maskClosable 默认为true与antd文档相反
|
||||
* @param confirm 关闭是否需要确认弹框 boolean
|
||||
* @param confirmContent 关闭确认弹框内容 React.ReactNode
|
||||
*/
|
||||
const ModalPlugin: React.FC<IProps> = (props) => {
|
||||
const { modal } = App.useApp();
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
transitionName='ant-fade'
|
||||
centered={props.centered ?? true}
|
||||
destroyOnHidden={props.destroyOnHidden ?? true}
|
||||
maskClosable={props.maskClosable ?? true}
|
||||
onCancel={(event) => {
|
||||
if (props.confirm) {
|
||||
modal.confirm({
|
||||
transitionName: 'ant-fade',
|
||||
title: '系统提示',
|
||||
content: <>{props.confirmContent || '确认关闭?'}</>,
|
||||
width: 300,
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
if (props.onCancel) {
|
||||
await props.onCancel(event);
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
props.onCancel?.(event);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default ModalPlugin;
|
||||
77
src/components/PageContainer/BreadcrumbPlugin.tsx
Normal file
77
src/components/PageContainer/BreadcrumbPlugin.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Breadcrumb } from 'antd';
|
||||
import type React from 'react';
|
||||
import { GapBox } from '../GapBox';
|
||||
|
||||
type IProps = {
|
||||
/** 面包屑内容 */
|
||||
items?: string[];
|
||||
/** 容器样式 */
|
||||
contentStyle?: React.CSSProperties;
|
||||
/** 面包屑样式 */
|
||||
breadcrumbStyle?: React.CSSProperties;
|
||||
};
|
||||
|
||||
const BreadcrumbPlugin: React.FC<IProps> = (props) => {
|
||||
// const [src, setSrc] = useState('');
|
||||
// const user_id = localStorage.getItem('user_id');
|
||||
// const company_id = localStorage.getItem('company_id');
|
||||
// const storageKey = `likeMenus_c${company_id}_u${user_id}`;
|
||||
// const [likeMenus, setLikeMenus] = useState<string[]>(toArray(jsonParse(localStorage.getItem(storageKey))));
|
||||
// useEffect(() => {
|
||||
// aa: for (const el of asideMenuConfig) {
|
||||
// for (const ell of toArray(el.children)) {
|
||||
// if (ell.path == lo.pathname) {
|
||||
// // ! 打开文档跳转
|
||||
// setSrc(ell.docUrl);
|
||||
// break aa;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
// const save = (type: 'add' | 'remove') => {
|
||||
// if (type == 'add') {
|
||||
// if (!likeMenus.includes(lo.pathname)) {
|
||||
// likeMenus.push(lo.pathname);
|
||||
// }
|
||||
// } else {
|
||||
// likeMenus.splice(likeMenus.indexOf(lo.pathname), 1);
|
||||
// }
|
||||
// localStorage.setItem(storageKey, JSON.stringify(likeMenus));
|
||||
// setLikeMenus([...likeMenus]);
|
||||
// };
|
||||
|
||||
return Array.isArray(props.items) ? (
|
||||
<GapBox style={{ alignItems: 'center', ...props.contentStyle }}>
|
||||
<Breadcrumb
|
||||
items={props.items.map((item) => ({ title: item }))}
|
||||
style={{ minHeight: 22, ...props.breadcrumbStyle }}
|
||||
/>
|
||||
{/* {!!src && (
|
||||
<Tooltip title={'操作手册'}>
|
||||
<Button type='link' size='small' href={src} target='_blank' icon={<QuestionCircleFilled />} />
|
||||
</Tooltip>
|
||||
)} */}
|
||||
{/* {likeMenus.includes(lo.pathname) ? (
|
||||
<Tooltip title={'取消收藏'}>
|
||||
<StarFilled
|
||||
style={{ color: '#1677FF' }}
|
||||
onClick={() => {
|
||||
save('remove');
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title={'添加收藏'}>
|
||||
<StarOutlined
|
||||
style={{ color: '#1677FF' }}
|
||||
onClick={() => {
|
||||
save('add');
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)} */}
|
||||
</GapBox>
|
||||
) : null;
|
||||
};
|
||||
export default BreadcrumbPlugin;
|
||||
74
src/components/PageContainer/PageContainerPlugin.tsx
Normal file
74
src/components/PageContainer/PageContainerPlugin.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Button, Result } from 'antd';
|
||||
import type React from 'react';
|
||||
import { type PropsWithChildren, useEffect, useState } from 'react';
|
||||
import { asideMenuConfig } from '@/configs/menuConfig';
|
||||
import { getHash } from '@/router/routerUtils';
|
||||
import { useAuthStore } from '@/store/AuthStore';
|
||||
import { useRefreshStore } from '@/store/RefreshStore';
|
||||
import { getDevice, toArray } from '@/utils/common';
|
||||
import BreadcrumbPlugin from './BreadcrumbPlugin';
|
||||
|
||||
interface IProps {
|
||||
breadcrumb?: string[];
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
/** PageContainer的简单封装 */
|
||||
const PageContainerPlugin: React.FC<PropsWithChildren<IProps>> = (props) => {
|
||||
const isPhone = getDevice() == 'phone';
|
||||
const auth = useAuthStore().auth;
|
||||
// const company = useCompanyStore().company;
|
||||
// const user = useUserStore().user;
|
||||
const refresh = useRefreshStore().refresh;
|
||||
const [authStr, setAuthStr] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// const hash = window.location.hash;
|
||||
// const end = hash.indexOf('?') > 1 ? hash.indexOf('?') : hash.length;
|
||||
// const pathname = hash.substring(1, end);
|
||||
// todo 写入当前页面路径到lastRouter 还要判断是否权限
|
||||
// if (!pathname.includes('/login') && !pathname.includes('/query/')) {
|
||||
// localStorage.setItem(`u${user.user_id}_c${company.company_id}_lastRouter`, pathname);
|
||||
// }
|
||||
window.scrollTo(0, 0);
|
||||
const hashStr = getHash();
|
||||
|
||||
b: for (const el of asideMenuConfig) {
|
||||
for (const ell of toArray(el.children)) {
|
||||
if (ell.path == hashStr) {
|
||||
setAuthStr(ell.auth);
|
||||
break b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ padding: isPhone ? 12 : 16, ...props.style }} key={refresh}>
|
||||
<BreadcrumbPlugin items={props.breadcrumb} contentStyle={{ marginBottom: 12 }} />
|
||||
{/* {props.children} */}
|
||||
{!authStr || authStr.split(',').some((key) => auth?.[key.trim()]) ? (
|
||||
props.children
|
||||
) : (
|
||||
<Result
|
||||
status='403'
|
||||
title=''
|
||||
subTitle={
|
||||
<div style={{ color: '#000' }}>
|
||||
<div>{'抱歉!您当前没有权限访问该页面'}</div>
|
||||
<div>{'如果您是主账号,请购买相应的增值包'}</div>
|
||||
<div>{'如果您是子账号,请联系管理员进行相应的授权'}</div>
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<Button type='primary' href='/'>
|
||||
返回首页
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageContainerPlugin;
|
||||
64
src/components/PaginationPlugin.tsx
Normal file
64
src/components/PaginationPlugin.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Pagination } from 'antd';
|
||||
import type React from 'react';
|
||||
|
||||
interface IProps {
|
||||
current: number | undefined;
|
||||
pageSize: number | undefined;
|
||||
total: number | undefined;
|
||||
onChange: (page: number) => void;
|
||||
style?: React.CSSProperties;
|
||||
size?: 'small' | 'default';
|
||||
hideOnSinglePage?: boolean;
|
||||
}
|
||||
|
||||
export const HeaderPagination: React.FC<IProps> = (props) => {
|
||||
return (
|
||||
<Pagination
|
||||
style={{ display: 'flex', justifyContent: 'flex-end', ...props.style }}
|
||||
current={props.current}
|
||||
pageSize={props.pageSize}
|
||||
total={props.total}
|
||||
onChange={(page) => {
|
||||
props.onChange(page);
|
||||
}}
|
||||
hideOnSinglePage={props.hideOnSinglePage}
|
||||
simple
|
||||
showSizeChanger={false}
|
||||
size={props.size ?? 'small'}
|
||||
showTotal={(total) => `共 ${total} 条`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface IProps2 {
|
||||
current: number | undefined;
|
||||
pageSize: number | undefined;
|
||||
total: number | undefined;
|
||||
onChange: (page: number, pageSize: number) => void;
|
||||
// onShowSizeChange: (pageSize: number) => void;
|
||||
size?: 'small' | 'default';
|
||||
showSizeChanger?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const FooterPagination: React.FC<IProps2> = (props) => {
|
||||
return (
|
||||
<Pagination
|
||||
className='center'
|
||||
size={props.size}
|
||||
style={{ justifyContent: 'center', ...props.style }}
|
||||
showSizeChanger={props.showSizeChanger ?? !window.dfConfig.isPhone}
|
||||
current={props.current}
|
||||
pageSize={props.pageSize}
|
||||
total={props.total}
|
||||
onChange={(page, pageSize) => {
|
||||
props.onChange(pageSize != props.pageSize ? 1 : page, pageSize);
|
||||
}}
|
||||
// onShowSizeChange={(_current, size) => {
|
||||
// props.onShowSizeChange(size);
|
||||
// }}
|
||||
// size={!isPhone ? 'small' : 'default'}
|
||||
showTotal={(total) => `共 ${total} 条`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
89
src/components/SearchButton.tsx
Normal file
89
src/components/SearchButton.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import { Button, Input } from 'antd';
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type IProps = {
|
||||
style?: React.CSSProperties;
|
||||
onClick?: () => void;
|
||||
loading?: boolean;
|
||||
onlyIcon?: boolean;
|
||||
type?: 'link' | 'text' | 'primary' | 'default' | 'dashed';
|
||||
};
|
||||
|
||||
export const SearchButton: React.FC<IProps> = (props) => {
|
||||
const title = '搜索' as string;
|
||||
return (
|
||||
<Button
|
||||
title={title}
|
||||
type={props.type || 'primary'}
|
||||
style={props.style}
|
||||
loading={props.loading}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.onlyIcon !== true && title}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ResetButton: React.FC<IProps> = (props) => {
|
||||
const title = '重置' as string;
|
||||
return (
|
||||
<Button title={title} type={props.type} style={props.style} loading={props.loading} onClick={props.onClick}>
|
||||
{props.onlyIcon !== true && title}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const MoreSearchButton: React.FC<IProps & { show: boolean }> = (props) => {
|
||||
const title = (props.show ? '收起' : '展开') as string;
|
||||
return (
|
||||
<Button
|
||||
title={title}
|
||||
type={props.type || 'text'}
|
||||
style={props.style}
|
||||
loading={props.loading}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{title}
|
||||
{props.show ? <UpOutlined /> : <DownOutlined />}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
interface ISearchInput {
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onEnd?: (newValue?: string, oldValue?: string) => void;
|
||||
onPressEnter?: (newValue?: string, oldValue?: string) => void;
|
||||
}
|
||||
|
||||
export const SearchInputPlugin: React.FC<ISearchInput> = (props) => {
|
||||
const { placeholder, value, onPressEnter, onEnd } = props;
|
||||
const [val, setVal] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
setVal(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Input
|
||||
allowClear
|
||||
value={val}
|
||||
placeholder={placeholder}
|
||||
onChange={(v) => {
|
||||
setVal(v.target.value);
|
||||
}}
|
||||
// onClear={() => {
|
||||
// onEnd?.('', value);
|
||||
// }}
|
||||
onBlur={() => {
|
||||
onEnd?.(val, value);
|
||||
}}
|
||||
onPressEnter={() => {
|
||||
onEnd?.(val, value);
|
||||
onPressEnter?.(val, value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
113
src/components/SiderMenu/NavMenu.tsx
Normal file
113
src/components/SiderMenu/NavMenu.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Menu, type MenuProps } from 'antd';
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { asideMenuConfig } from '@/configs/menuConfig';
|
||||
import { getHash, navigate } from '@/router/routerUtils';
|
||||
import { useAuthStore } from '@/store/AuthStore';
|
||||
import { useCompanyStore } from '@/store/CompanyStore';
|
||||
import { isArray, toArray } from '@/utils/common';
|
||||
|
||||
interface IProps {
|
||||
onCallback?: () => void;
|
||||
}
|
||||
|
||||
const NavMenu: React.FC<IProps> = (props) => {
|
||||
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||
const auth = useAuthStore().auth;
|
||||
const company = useCompanyStore().company;
|
||||
// 新窗口打开的链接
|
||||
const [newWindowUrl] = useState<{ [key: string]: { target: string } }>({});
|
||||
const [hash, setHash] = useState(getHash());
|
||||
type MenuItem = Required<MenuProps>['items'][number];
|
||||
/** 导航菜单 数据处理 */
|
||||
const [menuOptions, setMenuOptions] = useState<MenuItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const arr: MenuItem[] = [];
|
||||
asideMenuConfig.forEach((item) => {
|
||||
const itemName = item.name;
|
||||
const obj: any = {
|
||||
key: item.path || itemName,
|
||||
icon: item.icon,
|
||||
label: itemName,
|
||||
title: itemName,
|
||||
};
|
||||
if (isArray(item.children)) {
|
||||
obj.children = [];
|
||||
item.children?.forEach((el: any) => {
|
||||
// ! 添加权限判断
|
||||
if (!el.hideInMenu && (!el.auth || (auth && el.auth.split(',').some((key: string) => auth?.[key.trim()])))) {
|
||||
if (!el.auth && (company.staff_type == '3' || company.staff_type == '4')) {
|
||||
//
|
||||
} else {
|
||||
const elName = el.name;
|
||||
if (el.target && el.path) {
|
||||
newWindowUrl[el.path] = { target: el.target };
|
||||
}
|
||||
obj.children.push({ key: el.path, icon: el.icon, label: elName, title: elName });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (obj.children.length) {
|
||||
arr.push(obj);
|
||||
}
|
||||
});
|
||||
// console.log(arr);
|
||||
setMenuOptions(arr);
|
||||
|
||||
const hashChange = () => {
|
||||
const hashStr = getHash() || '/home/index';
|
||||
let name = '';
|
||||
let title = '';
|
||||
asideMenuConfig.forEach((item) => {
|
||||
const itemName = item.name as string;
|
||||
if (item.path == hashStr) {
|
||||
name = itemName;
|
||||
title = itemName;
|
||||
} else {
|
||||
toArray(item.children).forEach((el) => {
|
||||
if (el.path == hashStr) {
|
||||
name = itemName;
|
||||
title = el.name as string;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
setOpenKeys([name]);
|
||||
document.title = title;
|
||||
setHash(hashStr);
|
||||
};
|
||||
|
||||
hashChange();
|
||||
|
||||
window.addEventListener('hashchange', hashChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('hashchange', hashChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
onClick={(info) => {
|
||||
if (newWindowUrl[info.key]) {
|
||||
window.open(`#${info.key}`, newWindowUrl[info.key].target);
|
||||
} else {
|
||||
navigate(info.key);
|
||||
}
|
||||
props.onCallback?.();
|
||||
}}
|
||||
onOpenChange={(openKeys) => {
|
||||
setOpenKeys(openKeys);
|
||||
}}
|
||||
// style={{ width: '100%' }}
|
||||
selectedKeys={[hash]}
|
||||
openKeys={openKeys}
|
||||
// key={`${openKeys[0]}_${lo.pathname}`}
|
||||
mode='inline'
|
||||
items={menuOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default NavMenu;
|
||||
29
src/components/SiderMenu/NavigateMenuDrawer.tsx
Normal file
29
src/components/SiderMenu/NavigateMenuDrawer.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { MenuUnfoldOutlined } from '@ant-design/icons';
|
||||
import { Button, Drawer } from 'antd';
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { getDevice } from '@/utils/common';
|
||||
import NavMenu from './NavMenu';
|
||||
|
||||
export const NavigateMenuDrawer: React.FC = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
if (getDevice() === 'phone') {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setOpen(true)} icon={<MenuUnfoldOutlined />} type='primary' size='small' />
|
||||
<Drawer
|
||||
open={open}
|
||||
placement='left'
|
||||
size={240}
|
||||
styles={{ header: { display: 'none' }, body: { padding: 0 } }}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<NavMenu onCallback={() => setOpen(false)} />
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
28
src/components/TabNavPlugin/TabNavSaveCheckBoxPlugin.tsx
Normal file
28
src/components/TabNavPlugin/TabNavSaveCheckBoxPlugin.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Checkbox } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCompanyStore } from '@/store/CompanyStore';
|
||||
import { useUserStore } from '@/store/UserStore';
|
||||
|
||||
const TabNavSaveCheckBoxPlugin = () => {
|
||||
const [value, setValue] = useState(false);
|
||||
const user = useUserStore().user;
|
||||
const company = useCompanyStore().company;
|
||||
const storageKey = `u${user.user_id}_c${company.company_id}_tabNavSave`;
|
||||
useEffect(() => {
|
||||
setValue(localStorage.getItem(storageKey) == '1');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
checked={value}
|
||||
onChange={(e) => {
|
||||
console.log(e);
|
||||
setValue(e.target.checked);
|
||||
localStorage.setItem(storageKey, `${e.target.checked ? 1 : 0}`);
|
||||
}}>
|
||||
保留标签
|
||||
</Checkbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabNavSaveCheckBoxPlugin;
|
||||
58
src/components/TabNavPlugin/index.module.css
Normal file
58
src/components/TabNavPlugin/index.module.css
Normal file
@@ -0,0 +1,58 @@
|
||||
.box {
|
||||
height: 32px;
|
||||
background-color: #fff;
|
||||
backdrop-filter: blur(8px);
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 3px 5px #ddd;
|
||||
overflow: hidden;
|
||||
margin-left: -12px;
|
||||
margin-right: -12px;
|
||||
margin-top: -16px;
|
||||
/* box-sizing: border-box; */
|
||||
}
|
||||
|
||||
.boxScroll {
|
||||
height: 50px;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
border-right: 1px solid #ddd;
|
||||
padding: 0 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tagActive {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.tagActive::before {
|
||||
content: "";
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #1677ff;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.title {
|
||||
min-width: 40px;
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
padding-right: 4px;
|
||||
}
|
||||
263
src/components/TabNavPlugin/index.tsx
Normal file
263
src/components/TabNavPlugin/index.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import { Menu } from 'antd';
|
||||
import type React from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Colors, DefaultERPName, headerHeight } from '@/configs/config';
|
||||
import { getHash } from '@/router/routerUtils';
|
||||
import { useCompanyStore } from '@/store/CompanyStore';
|
||||
import { useRefreshStore } from '@/store/RefreshStore';
|
||||
import { getDevice } from '@/utils/common';
|
||||
import styles from './index.module.css';
|
||||
|
||||
interface IUrl {
|
||||
url: string;
|
||||
title: string;
|
||||
search: string;
|
||||
}
|
||||
|
||||
const TabNavPlugin: React.FC = () => {
|
||||
const [urlList, setUrlList] = useState<IUrl[]>([]);
|
||||
const urlListRef = useRef<IUrl[]>([]);
|
||||
const isPhone = getDevice() == 'phone';
|
||||
const refreshStore = useRefreshStore();
|
||||
const [pathname, setPathname] = useState('');
|
||||
|
||||
const boxScrollRef = useRef<any>(null);
|
||||
// 删除模式 不进行滚动
|
||||
const modeRef = useRef('');
|
||||
const company = useCompanyStore().company;
|
||||
|
||||
const callback = () => {
|
||||
const span = document.querySelector('.urlList-active');
|
||||
if (urlListRef.current && span && modeRef.current != 'del') {
|
||||
// urlListRef.current
|
||||
const boxCenterWidth = boxScrollRef.current.parentNode.clientWidth / 2;
|
||||
const scrollWidth = (span as HTMLElement).offsetLeft + span.clientWidth / 2;
|
||||
let scrollLeft = 0;
|
||||
if (scrollWidth >= boxCenterWidth) {
|
||||
scrollLeft = scrollWidth - boxCenterWidth;
|
||||
}
|
||||
// console.log((span as HTMLElement).offsetLeft);
|
||||
setTimeout(() => {
|
||||
boxScrollRef.current?.scroll(scrollLeft, 0);
|
||||
}, 17);
|
||||
}
|
||||
};
|
||||
const observer = new MutationObserver(callback);
|
||||
|
||||
const [menuStyle, setMenuStyle] = useState({
|
||||
left: 0,
|
||||
top: 0,
|
||||
display: 'none',
|
||||
});
|
||||
const menu = [
|
||||
{ key: '1', label: '刷新页面' },
|
||||
{ key: '2', label: '关闭当前' },
|
||||
{ key: '3', label: '关闭其他' },
|
||||
{ key: '4', label: '关闭所有' },
|
||||
];
|
||||
const menu2 = [
|
||||
{ key: '1', label: '刷新页面' },
|
||||
{ key: '3', label: '关闭其他' },
|
||||
{ key: '4', label: '关闭所有' },
|
||||
];
|
||||
|
||||
const selectRecordRef = useRef<IUrl>({ url: '', title: '', search: '' });
|
||||
const isActiveTag = useRef<boolean>(false);
|
||||
|
||||
function hideMenu() {
|
||||
menuStyle.display = 'none';
|
||||
setMenuStyle({ ...menuStyle });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPhone) {
|
||||
boxScrollRef.current.addEventListener(
|
||||
'wheel',
|
||||
(event: WheelEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
boxScrollRef.current.scrollLeft += event.deltaY;
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
observer.observe(boxScrollRef.current, {
|
||||
childList: true, // 观察目标子节点的变化,是否有添加或者删除
|
||||
attributes: true, // 观察属性变动
|
||||
subtree: true, // 观察后代节点,默认为 false
|
||||
});
|
||||
}
|
||||
document.addEventListener('click', hideMenu);
|
||||
window.addEventListener('scroll', hideMenu);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', hideMenu);
|
||||
window.removeEventListener('scroll', hideMenu);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
modeRef.current = '';
|
||||
const handle = () => {
|
||||
const pathname = getHash();
|
||||
setPathname(pathname);
|
||||
|
||||
let flag = true;
|
||||
if ((document.title == '404' || pathname == '/temp' || pathname == '/') && !isPhone) {
|
||||
return;
|
||||
}
|
||||
for (const el of urlListRef.current) {
|
||||
if (el.url === pathname) {
|
||||
flag = false;
|
||||
// el.search = lo.search;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (flag) {
|
||||
urlListRef.current.push({
|
||||
url: pathname,
|
||||
title: `${document.title}`.replace(` - ${DefaultERPName}`, ''),
|
||||
search: '',
|
||||
});
|
||||
setUrlList([...urlListRef.current]);
|
||||
}
|
||||
};
|
||||
handle();
|
||||
window.addEventListener('hashchange', handle);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('hashchange', handle);
|
||||
};
|
||||
}, []);
|
||||
// if (!urlListRef.current.includes(lo.pathname)) {
|
||||
// urlListRef.current.push(lo.pathname);
|
||||
// setUrlList([...urlListRef.current]);
|
||||
// }
|
||||
return (
|
||||
<>
|
||||
{isPhone ? null : (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
zIndex: 101,
|
||||
left: menuStyle.left,
|
||||
top: menuStyle.top,
|
||||
display: menuStyle.display,
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
selectedKeys={[]}
|
||||
items={selectRecordRef.current && selectRecordRef.current.url == '/' ? menu2 : menu}
|
||||
onClick={(item) => {
|
||||
if (item.key == '1') {
|
||||
if (selectRecordRef.current) {
|
||||
if (isActiveTag.current) {
|
||||
refreshStore.updateRefresh(refreshStore.refresh + 1);
|
||||
} else {
|
||||
location.hash = selectRecordRef.current.url;
|
||||
}
|
||||
}
|
||||
} else if (item.key == '2') {
|
||||
if (selectRecordRef.current) {
|
||||
for (let index = 0; index < urlListRef.current.length; index++) {
|
||||
if (urlListRef.current[index].url == selectRecordRef.current.url) {
|
||||
urlListRef.current.splice(index, 1);
|
||||
if (selectRecordRef.current.url == pathname) {
|
||||
if (urlListRef.current.length) {
|
||||
location.hash = urlListRef.current[urlListRef.current.length - 1].url;
|
||||
} else {
|
||||
location.hash = '/';
|
||||
}
|
||||
}
|
||||
setUrlList([...urlListRef.current]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (item.key == '3') {
|
||||
if (selectRecordRef.current) {
|
||||
urlListRef.current = [{ ...selectRecordRef.current }];
|
||||
location.hash = selectRecordRef.current.url;
|
||||
|
||||
setUrlList([...urlListRef.current]);
|
||||
}
|
||||
} else if (item.key == '4') {
|
||||
urlListRef.current = [];
|
||||
location.hash = '/';
|
||||
setUrlList([...urlListRef.current]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.box} style={{ top: headerHeight }}>
|
||||
<div className={styles.boxScroll} ref={boxScrollRef}>
|
||||
{urlList.map((el) => (
|
||||
<span
|
||||
title={el.title}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
key={el.url}
|
||||
onClick={() => {
|
||||
location.hash = el.url;
|
||||
}}
|
||||
onMouseDown={(event: any) => {
|
||||
if (event.button == 2) {
|
||||
menuStyle.top = event.pageY - document.documentElement.scrollTop + 2;
|
||||
menuStyle.left = event.pageX + 2;
|
||||
menuStyle.display = 'block';
|
||||
isActiveTag.current = pathname == el.url;
|
||||
selectRecordRef.current = el;
|
||||
setMenuStyle({ ...menuStyle });
|
||||
}
|
||||
}}
|
||||
className={`${styles.tag} ${pathname == el.url ? styles.tagActive : ''} urlList-${
|
||||
pathname == el.url ? 'active' : ''
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={styles.title}
|
||||
style={{
|
||||
color: pathname == el.url ? Colors.primary : '#000',
|
||||
}}
|
||||
>
|
||||
{el.title}
|
||||
</span>
|
||||
<CloseOutlined
|
||||
style={{ fontSize: 12, height: 32 }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
modeRef.current = 'del';
|
||||
for (let index = 0; index < urlListRef.current.length; index++) {
|
||||
if (urlListRef.current[index].url == el.url) {
|
||||
if (urlListRef.current.length == 1 && company.staff_type == 3) {
|
||||
break;
|
||||
}
|
||||
urlListRef.current.splice(index, 1);
|
||||
if (el.url == pathname) {
|
||||
if (urlListRef.current.length) {
|
||||
location.hash = urlListRef.current[urlListRef.current.length - 1].url;
|
||||
} else {
|
||||
location.hash = '/';
|
||||
}
|
||||
}
|
||||
setUrlList([...urlListRef.current]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabNavPlugin;
|
||||
87
src/components/TableColumnsFilterPlugin.tsx
Normal file
87
src/components/TableColumnsFilterPlugin.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { ColumnType } from 'antd/es/table';
|
||||
import { isArray, jsonParse } from '@/utils/common';
|
||||
|
||||
/** 表格数据类型扩展 ColumnsType */
|
||||
export type ColumnsTypeUltra<RecordType = unknown> = (ColumnType<RecordType> & {
|
||||
/** true 则不会出现在配置列中 */
|
||||
// unset?: boolean;
|
||||
/** 前端排序拖拽key, 等同与title, title是React.ReactNode时需要设置 webKey */
|
||||
// webKey?: string;
|
||||
// hide?: boolean;
|
||||
})[];
|
||||
|
||||
/**
|
||||
* 获取存储中隐藏的配置列数据
|
||||
*
|
||||
* @param storageKey {string} 存储的key
|
||||
* @returns 数组 {string[]}
|
||||
*/
|
||||
export const TableColumnsStorage = (storageKey: string): string[] => {
|
||||
const str = localStorage.getItem(storageKey);
|
||||
const temp = jsonParse(str);
|
||||
return isArray(temp) ? (temp as string[]) : [];
|
||||
};
|
||||
|
||||
// type IProps = {
|
||||
// /** 表格列数据 */
|
||||
// tableColumns: ColumnsType<any>;
|
||||
// /** 存储的key */
|
||||
// storageKey: string;
|
||||
// /** 事件 */
|
||||
// onChange?: () => void;
|
||||
// placement?: TooltipPlacement;
|
||||
// };
|
||||
|
||||
// /** 表格栏筛选组件
|
||||
// *
|
||||
// * @param tableColumns 表格列数据
|
||||
// * @param storageKey 存储的key
|
||||
// * @param onChange 事件
|
||||
// */
|
||||
// export const TableColumnsFilter: React.FC<IProps> = (props) => {
|
||||
// const [hideColumns, setHideColumns] = useState<string[]>([]);
|
||||
// useEffect(() => {
|
||||
// setHideColumns(TableColumnsStorage(props.storageKey));
|
||||
// }, []);
|
||||
// return (
|
||||
// <Popover
|
||||
// content={
|
||||
// <div style={{ width: 400, padding: '8px 0 0 8px' }}>
|
||||
// <div style={{ fontSize: 16, fontWeight: 'bold', marginBottom: 12 }}>{('配置列')}</div>
|
||||
// {props.tableColumns.map((item: any) => {
|
||||
// if (item.unset || typeof item.title != 'string') return null;
|
||||
// return (
|
||||
// <Checkbox
|
||||
// style={{ marginBottom: 8, marginRight: 8 }}
|
||||
// key={item.title}
|
||||
// checked={!hideColumns.includes(item.title)}
|
||||
// onChange={(event) => {
|
||||
// if (event.target.checked) {
|
||||
// hideColumns.splice(hideColumns.indexOf(item.title), 1);
|
||||
// } else {
|
||||
// hideColumns.push(item.title);
|
||||
// }
|
||||
// setHideColumns([...hideColumns]);
|
||||
// localStorage.setItem(props.storageKey, JSON.stringify(hideColumns));
|
||||
// props.onChange && props.onChange();
|
||||
// }}>
|
||||
// {item.title}
|
||||
// </Checkbox>
|
||||
// );
|
||||
// })}
|
||||
// </div>
|
||||
// }
|
||||
// trigger={['click']}
|
||||
// placement={props.placement || 'bottomRight'}>
|
||||
// <Button
|
||||
// title={t('配置列') as string}
|
||||
// style={{
|
||||
// display: 'inline-flex',
|
||||
// alignItems: 'center',
|
||||
// justifyContent: 'center',
|
||||
// }}
|
||||
// icon={<FilterOutlined style={{ fontSize: 16 }} />}
|
||||
// />
|
||||
// </Popover>
|
||||
// );
|
||||
// };
|
||||
332
src/components/TableColumnsFilterPlugin2.tsx
Normal file
332
src/components/TableColumnsFilterPlugin2.tsx
Normal file
@@ -0,0 +1,332 @@
|
||||
import type { ColumnType } from 'antd/es/table';
|
||||
|
||||
// interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
||||
// 'data-row-key': string;
|
||||
// }
|
||||
|
||||
/** 表格数据类型扩展 ColumnsType */
|
||||
export type ColumnsTypeUltra2<RecordType = unknown> = (ColumnType<RecordType> & {
|
||||
/** 前端排序拖拽唯一columnName, 展示列的名称 */
|
||||
columnName: string;
|
||||
fixed?: 'left' | 'right';
|
||||
})[];
|
||||
|
||||
// type IProps = {
|
||||
// /** 表格列数据 */
|
||||
// tableColumns: ColumnsTypeUltra2<any>;
|
||||
// usersConfigKey: string;
|
||||
// placement?: TooltipPlacement;
|
||||
// };
|
||||
|
||||
/** 表格栏筛选组件 */
|
||||
// export const TableColumnsFilter2: React.FC<IProps> = (props) => {
|
||||
// const [dataSource, setDataSource] = useState<any[]>([]);
|
||||
// const [open, setOpen] = useState(false);
|
||||
// const usersConfigStore = useUsersConfigStore();
|
||||
// const { notification, modal } = App.useApp();
|
||||
// const modeRef = useRef<'save' | 'reset'>('save');
|
||||
// const isPhone = window.dfConfig.isPhone;
|
||||
// // console.log(usersConfigStore.config);
|
||||
|
||||
// const { loading, request: setUsersConfigRequest } = useRequest(UsersConfigServices.setUsersConfig, {
|
||||
// onSuccess(res) {
|
||||
// if (res.err_code == 0) {
|
||||
// notification.success({
|
||||
// message: modeRef.current == 'save' ? '表格配置保存成功' : '表格配置还原成功',
|
||||
// });
|
||||
|
||||
// getUsersConfigList().then((res) => {
|
||||
// usersConfigStore.updateConfig(res);
|
||||
// setOpen(false);
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
|
||||
// const TableRow = ({ children, ...props }: RowProps) => {
|
||||
// const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging } = useSortable({
|
||||
// id: props['data-row-key'],
|
||||
// });
|
||||
|
||||
// const style: React.CSSProperties = {
|
||||
// ...props.style,
|
||||
// transform: CSS.Transform.toString(transform && { ...transform, scaleY: 1 }),
|
||||
// transition,
|
||||
// cursor: 'default',
|
||||
// ...(isDragging ? { position: 'relative', zIndex: 100 } : {}),
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <tr {...props} ref={setNodeRef} style={style} {...attributes}>
|
||||
// {React.Children.map(children, (child) => {
|
||||
// if ((child as React.ReactElement).key === 'sort') {
|
||||
// return React.cloneElement(child as React.ReactElement, {
|
||||
// children: (
|
||||
// <div style={{ textAlign: 'center' }}>
|
||||
// <Button
|
||||
// title='拖拽排序'
|
||||
// ref={setActivatorNodeRef}
|
||||
// size='small'
|
||||
// style={{
|
||||
// cursor: 'move',
|
||||
// }}
|
||||
// icon={
|
||||
// <SwapOutlined
|
||||
// style={{
|
||||
// touchAction: 'none',
|
||||
// cursor: 'move',
|
||||
// transform: 'rotate(90deg)',
|
||||
// }}
|
||||
// />
|
||||
// }
|
||||
// {...listeners}
|
||||
// />
|
||||
// </div>
|
||||
// ),
|
||||
// });
|
||||
// }
|
||||
// return child;
|
||||
// })}
|
||||
// </tr>
|
||||
// );
|
||||
// };
|
||||
|
||||
// const sensors = useSensors(
|
||||
// useSensor(PointerSensor, {
|
||||
// activationConstraint: {
|
||||
// distance: 1,
|
||||
// },
|
||||
// }),
|
||||
// );
|
||||
|
||||
// const dragEndEvent = (props: any) => {
|
||||
// const { active, over } = props;
|
||||
// if (active?.id && over?.id && active.id !== over?.id) {
|
||||
// setDataSource((prevState) => {
|
||||
// const activeIndex = prevState.findIndex((record) => record.columnName === active?.id);
|
||||
// const overIndex = prevState.findIndex((record) => record.columnName === over?.id);
|
||||
// return arrayMove(prevState, activeIndex, overIndex);
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
// const columns: ColumnsType<any> = [
|
||||
// { title: '排序', key: 'sort', width: 48 },
|
||||
// {
|
||||
// title: '显示列',
|
||||
// width: 64,
|
||||
// align: 'center',
|
||||
// dataIndex: 'show',
|
||||
// render(value, record) {
|
||||
// return (
|
||||
// <Switch
|
||||
// defaultChecked={value}
|
||||
// key={value}
|
||||
// onChange={(e) => {
|
||||
// record.show = e;
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// { title: '列名', dataIndex: 'columnName', width: 140 },
|
||||
// {
|
||||
// title: (
|
||||
// <GapBox>
|
||||
// <span>宽度</span>
|
||||
// <div style={{ color: Colors.error, fontWeight: 'normal' }}>1个字约14px</div>
|
||||
// </GapBox>
|
||||
// ),
|
||||
// dataIndex: 'width',
|
||||
// width: 140,
|
||||
// render(val, record) {
|
||||
// return (
|
||||
// <InputNumber
|
||||
// size='small'
|
||||
// defaultValue={val}
|
||||
// key={val}
|
||||
// min={44}
|
||||
// max={600}
|
||||
// onChange={(val) => {
|
||||
// record.width = val;
|
||||
// }}
|
||||
// onBlur={() => {
|
||||
// setTimeout(() => {
|
||||
// if (!record.width) {
|
||||
// record.width = 44;
|
||||
// setDataSource([...dataSource]);
|
||||
// }
|
||||
// }, 0);
|
||||
// }}
|
||||
// addonAfter='px'
|
||||
// />
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// title: (
|
||||
// <GapBox>
|
||||
// <span>固定列</span>
|
||||
// <div style={{ color: Colors.error, fontWeight: 'normal' }}>请勿在中间列锁列, 会导致表格错位</div>
|
||||
// </GapBox>
|
||||
// ),
|
||||
// dataIndex: 'fixed',
|
||||
// width: 290,
|
||||
// render(val, record) {
|
||||
// return (
|
||||
// <Radio.Group
|
||||
// onChange={(e) => {
|
||||
// record.fixed = e.target.value;
|
||||
// }}
|
||||
// defaultValue={val || ''}
|
||||
// key={val}>
|
||||
// <Radio value={''}>无</Radio>
|
||||
// <Radio value={'left'}>左边</Radio>
|
||||
// <Radio value={'right'}>右边</Radio>
|
||||
// </Radio.Group>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// ];
|
||||
|
||||
// const show = () => {
|
||||
// const arr = toArray(jsonParse(usersConfigStore.config![props.usersConfigKey]?.config_value));
|
||||
// const configObj: any = {};
|
||||
// arr.forEach((el, i) => {
|
||||
// configObj[el.columnName] = {
|
||||
// index: i,
|
||||
// ...el,
|
||||
// };
|
||||
// });
|
||||
// // console.log(configObj);
|
||||
// const repeatKey: string[] = [];
|
||||
// const keys: string[] = [];
|
||||
// dataSource.length = 0;
|
||||
// props.tableColumns
|
||||
// .filter((el) => el.columnName)
|
||||
// .forEach((el) => {
|
||||
// const obj: any = {
|
||||
// columnName: el.columnName,
|
||||
// show: !el.hidden,
|
||||
// width: el.width,
|
||||
// fixed: el.fixed,
|
||||
// };
|
||||
// if (keys.includes(el.columnName)) {
|
||||
// repeatKey.push(el.columnName);
|
||||
// }
|
||||
// keys.push(el.columnName);
|
||||
|
||||
// if (arr.length && configObj[el.columnName]) {
|
||||
// obj.show = configObj[el.columnName].show;
|
||||
// obj.width = configObj[el.columnName].width;
|
||||
// obj.fixed = configObj[el.columnName].fixed;
|
||||
// }
|
||||
// dataSource.push(obj);
|
||||
// });
|
||||
// if (repeatKey.length) {
|
||||
// console.warn(`表格列配置重复: ${repeatKey.join()}`);
|
||||
// if (import.meta.env.DEV) {
|
||||
// notification.error({ message: `表格列配置重复: ${repeatKey.join()}` });
|
||||
// }
|
||||
// }
|
||||
// if (arr.length) {
|
||||
// dataSource.sort((a, b) => toNumber(configObj[a.columnName]?.index) - toNumber(configObj[b.columnName]?.index));
|
||||
// }
|
||||
|
||||
// setDataSource([...dataSource]);
|
||||
// setOpen(true);
|
||||
// };
|
||||
|
||||
// const save = () => {
|
||||
// setUsersConfigRequest({
|
||||
// config_id: usersConfigStore.config![props.usersConfigKey]?.config_id,
|
||||
// config_name: props.usersConfigKey,
|
||||
// config_value: modeRef.current == 'save' ? JSON.stringify(dataSource) : '',
|
||||
// });
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <Button
|
||||
// title={t('配置列') as string}
|
||||
// style={{
|
||||
// display: 'inline-flex',
|
||||
// alignItems: 'center',
|
||||
// justifyContent: 'center',
|
||||
// width: 32,
|
||||
// position: 'relative',
|
||||
// }}
|
||||
// onClick={show}
|
||||
// icon={<FilterOutlined />}>
|
||||
// {!!usersConfigStore.config![props.usersConfigKey]?.config_value && (
|
||||
// <i
|
||||
// style={{
|
||||
// position: 'absolute',
|
||||
// width: 8,
|
||||
// height: 8,
|
||||
// background: Colors.error,
|
||||
// borderRadius: 8,
|
||||
// top: 1,
|
||||
// right: 2,
|
||||
// }}></i>
|
||||
// )}
|
||||
// </Button>
|
||||
// <DrawerPlugin
|
||||
// open={open}
|
||||
// width={isPhone ? '95vw' : 760}
|
||||
// title={
|
||||
// <GapBox>
|
||||
// <div>表格列配置</div>
|
||||
// <Button
|
||||
// loading={loading}
|
||||
// key={'1'}
|
||||
// type='primary'
|
||||
// onClick={() => {
|
||||
// modeRef.current = 'save';
|
||||
// save();
|
||||
// }}>
|
||||
// 确定
|
||||
// </Button>
|
||||
// <Button
|
||||
// loading={loading}
|
||||
// type='primary'
|
||||
// ghost
|
||||
// key={'2'}
|
||||
// onClick={() => {
|
||||
// modal.confirm({
|
||||
// title: '系统提示',
|
||||
// content: '确定还原配置?',
|
||||
// onOk: () => {
|
||||
// modeRef.current = 'reset';
|
||||
// save();
|
||||
// },
|
||||
// ...modalBaseConfig,
|
||||
// });
|
||||
// }}>
|
||||
// 还原
|
||||
// </Button>
|
||||
// </GapBox>
|
||||
// }
|
||||
// onClose={() => {
|
||||
// setOpen(false);
|
||||
// }}>
|
||||
// <DndContext sensors={sensors} modifiers={[restrictToVerticalAxis]} onDragEnd={dragEndEvent}>
|
||||
// <SortableContext items={dataSource.map((el) => el.columnName)} strategy={verticalListSortingStrategy}>
|
||||
// <TablePlugin
|
||||
// style={{ marginBottom: 0 }}
|
||||
// scroll={{ x: 700 }}
|
||||
// components={{
|
||||
// body: {
|
||||
// row: TableRow,
|
||||
// },
|
||||
// }}
|
||||
// rowKey={'columnName'}
|
||||
// columns={columns}
|
||||
// dataSource={dataSource}
|
||||
// />
|
||||
// </SortableContext>
|
||||
// </DndContext>
|
||||
// </DrawerPlugin>
|
||||
// </>
|
||||
// );
|
||||
// };
|
||||
53
src/components/TablePlugin.tsx
Normal file
53
src/components/TablePlugin.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { ConfigProvider, Table } from 'antd';
|
||||
import type { TableProps } from 'antd/lib/table';
|
||||
import type React from 'react';
|
||||
import { isObject } from '@/utils/common';
|
||||
|
||||
export const formatTableSort = (sort: any) => {
|
||||
if (isObject(sort) && sort.order) {
|
||||
return `${sort.field} ${sort.order.replace('end', '')}`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 排序顺序格式化
|
||||
* @param dataIndex 列数据索引
|
||||
* @param order 排序顺序
|
||||
* @returns 'ascend' | 'descend' | undefined
|
||||
*/
|
||||
export const formatTableSortOrder = (dataIndex: string, order?: string): 'ascend' | 'descend' | undefined => {
|
||||
if (order) {
|
||||
const [name, sort] = order.split(' ');
|
||||
if (name == dataIndex) {
|
||||
return sort == 'asc' ? 'ascend' : 'descend';
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
type IProps = TableProps<any>;
|
||||
/** Table简单封装
|
||||
*
|
||||
* @props size="small"
|
||||
* @props tableLayout="fixed"
|
||||
* @props showSorterTooltip={false}
|
||||
* @props pagination={false}
|
||||
* @props bordered={true}
|
||||
*
|
||||
*/
|
||||
export const TablePlugin: React.FC<IProps> = (props) => {
|
||||
return (
|
||||
<ConfigProvider renderEmpty={() => '暂无数据'}>
|
||||
<Table
|
||||
size='small'
|
||||
tableLayout='fixed'
|
||||
showSorterTooltip={false}
|
||||
pagination={false}
|
||||
bordered
|
||||
style={{ marginBottom: 16 }}
|
||||
{...props}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
128
src/configs/config.ts
Normal file
128
src/configs/config.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type React from 'react';
|
||||
|
||||
/** 默认错误图片 */
|
||||
// export const BreakImgUrl = pathAddApiString('/static/images/break_img.svg');
|
||||
// export const DefaultAvatar = pathAddApiString('/static/img/avatar.svg');
|
||||
|
||||
/** 头部高度 */
|
||||
export const headerHeight = 40;
|
||||
|
||||
export const OSSBaseUrl = 'https://cdn.fzcfkj.com/';
|
||||
|
||||
/** 头部导航高度 */
|
||||
export const headerTabNavHeight = 32;
|
||||
|
||||
export const DefaultERPName = (() => {
|
||||
return '易宝赞普惠版后台系统';
|
||||
})();
|
||||
|
||||
export const Colors = {
|
||||
primary: 'rgb(22, 93, 255)',
|
||||
success: 'rgb(0, 180, 42)',
|
||||
successBg: 'rgb(175, 240, 181)',
|
||||
black: 'rgb(0, 0, 0)',
|
||||
warning: 'rgb(255, 125, 0)',
|
||||
error: 'rgb(245, 63, 63)',
|
||||
danger: 'rgb(245, 63, 63)',
|
||||
} as const;
|
||||
|
||||
export const styleConfig = {
|
||||
borderRadius: 2,
|
||||
} as const;
|
||||
|
||||
export const FlexCenter: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
export const FlexCenterInLine: React.CSSProperties = {
|
||||
display: 'inline-flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
/** 撤回码标记 */
|
||||
export const RecallCodeMarking = '-';
|
||||
|
||||
/** 菜单房间 */
|
||||
export const RoomName = [
|
||||
{ label: '主卧', value: '主卧' },
|
||||
{ label: '次卧', value: '次卧' },
|
||||
{ label: '老人房', value: '老人房' },
|
||||
{ label: '儿童房', value: '儿童房' },
|
||||
{ label: '阳台', value: '阳台' },
|
||||
{ label: '客厅', value: '客厅' },
|
||||
{ label: '厨房', value: '厨房' },
|
||||
{ label: '卫生间', value: '卫生间' },
|
||||
];
|
||||
|
||||
export const OrderState = {
|
||||
/** 未审核 */
|
||||
ERP_ORDER_STATE_NOT: 110,
|
||||
/** 部分审核 */
|
||||
ERP_ORDER_STATE_PART: 120,
|
||||
/** 全部审核 */
|
||||
ERP_ORDER_STATE_ALL: 130,
|
||||
/** 已关闭 */
|
||||
ERP_ORDER_STATE_CLOSE: 140,
|
||||
/** 部分销售 */
|
||||
ERP_ORDER_STATE_PART_SALE: 150,
|
||||
/** 完成销售 */
|
||||
ERP_ORDER_STATE_ALL_SALE: 160,
|
||||
} as const;
|
||||
|
||||
export const OrderStateColors = {
|
||||
/** 未审核 */
|
||||
110: 'red',
|
||||
/** 部分审核 */
|
||||
120: 'orange',
|
||||
/** 全部审核 */
|
||||
130: 'green',
|
||||
/** 已关闭 */
|
||||
140: 'gray',
|
||||
/** 部分销售 */
|
||||
150: 'blue',
|
||||
/** 完成销售 */
|
||||
160: 'arcoblue',
|
||||
} as const;
|
||||
|
||||
export const BonusType = { 1: '计件', 2: '计时' };
|
||||
|
||||
/** 生产订单状态 */
|
||||
export const ProduceState = {
|
||||
/** 未审核 */
|
||||
PRODUCE_ORDER_STATE_NOT: 110,
|
||||
/** 部分审核 */
|
||||
PRODUCE_ORDER_STATE_PART: 120,
|
||||
/** 全部审核 */
|
||||
PRODUCE_ORDER_STATE_ALL: 130,
|
||||
/** 已关闭 */
|
||||
PRODUCE_ORDER_STATE_CLOSE: 140,
|
||||
/** 加工中 */
|
||||
PRODUCE_ORDER_STATE_WORKING: 150,
|
||||
/** 已完工 */
|
||||
PRODUCE_ORDER_STATE_WORKED: 160,
|
||||
} as const;
|
||||
|
||||
export const ProduceStateObj: Record<string, { label: string; color: string }> = {
|
||||
/** 未审核 */
|
||||
110: { label: '未审核', color: 'red' },
|
||||
/** 部分审核 */
|
||||
120: { label: '部分审核', color: 'orange' },
|
||||
/** 全部审核 */
|
||||
130: { label: '全部审核', color: 'green' },
|
||||
/** 已关闭 */
|
||||
140: { label: '已关闭', color: 'gray' },
|
||||
/** 加工中 */
|
||||
150: { label: '加工中', color: 'blue' },
|
||||
/** 已完工 */
|
||||
160: { label: '已完工', color: 'arcoblue' },
|
||||
} as const;
|
||||
|
||||
/** 工序状态 */
|
||||
export const ProcessState: Record<string, { label: string; color: string }> = {
|
||||
0: { label: '未开始', color: '#0fc6c2' },
|
||||
1: { label: '进行中', color: '#7bc616' },
|
||||
2: { label: '已完成', color: '#86909c' },
|
||||
};
|
||||
50
src/configs/menuConfig.tsx
Normal file
50
src/configs/menuConfig.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { DashboardOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import type React from 'react';
|
||||
|
||||
/* cspell:disable */
|
||||
const iconStyle: React.CSSProperties = { fontSize: 18 };
|
||||
|
||||
interface MenuDataItem {
|
||||
name: string;
|
||||
icon?: React.ReactNode;
|
||||
path?: string;
|
||||
hideInMenu?: boolean;
|
||||
auth?: string;
|
||||
children?: MenuDataItem[];
|
||||
docUrl?: string;
|
||||
}
|
||||
|
||||
//const docUrl = 'https://docs.qq.com/aio/DS2NCRFFseG9Ma3Ja?p=';
|
||||
|
||||
const userMenu: MenuDataItem = {
|
||||
name: '用户管理',
|
||||
icon: <UserOutlined style={iconStyle} />,
|
||||
children: [
|
||||
{ name: '用户管理', path: '/user/list', auth: '' },
|
||||
// { name: '岗位角色', path: '/user/group', auth: 'SF_ERP_GROUP_VIEW' },
|
||||
],
|
||||
};
|
||||
const staffMenu: MenuDataItem = {
|
||||
name: '后台用户',
|
||||
icon: <UserOutlined style={iconStyle} />,
|
||||
children: [
|
||||
{ name: '组织架构', path: '/staff/dep', auth: 'SF_ERP_DEPART_VIEW' },
|
||||
{ name: '岗位角色', path: '/staff/group', auth: 'SF_ERP_GROUP_VIEW' },
|
||||
{ name: '员工管理', path: '/staff/list', auth: 'SF_ERP_STAFF_VIEW' },
|
||||
{ name: '我的权限', path: '/staff/my', auth: 'SF_MY_RIGHT_VIEW' },
|
||||
// { name: '日志管理', path: '/system/record', auth: 'SF_ERP_LOG_VIEW' },
|
||||
],
|
||||
};
|
||||
|
||||
const asideMenuConfig: MenuDataItem[] = [
|
||||
{
|
||||
name: '系统看板',
|
||||
// path: '/',
|
||||
icon: <DashboardOutlined style={iconStyle} />,
|
||||
children: [{ name: '系统主页', path: '/home/index', auth: '' }],
|
||||
},
|
||||
userMenu,
|
||||
staffMenu,
|
||||
];
|
||||
|
||||
export { asideMenuConfig };
|
||||
46
src/configs/routes.ts
Normal file
46
src/configs/routes.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { lazy } from 'react';
|
||||
import AppLayout from '@/layouts/AppLayout';
|
||||
import EmptyLayout from '@/layouts/EmptyLayout';
|
||||
import ErrorPage from '@/pages/Error';
|
||||
import Index from '@/pages/Index';
|
||||
import Dep from '@/pages/Staff/dep';
|
||||
import Group from '@/pages/Staff/group';
|
||||
import UserList from '@/pages/User/List';
|
||||
import type { IRouteItem } from '@/router/types';
|
||||
|
||||
export const routes: IRouteItem[] = [
|
||||
{
|
||||
path: '/home',
|
||||
Layout: AppLayout,
|
||||
children: [
|
||||
{ path: '/index', Component: Index },
|
||||
// { path: '/index', Component: lazy(() => import('@/pages/Index')) },
|
||||
// { path: '/home', Component: Index, },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
Layout: AppLayout,
|
||||
children: [
|
||||
{ path: '/list', Component: UserList },
|
||||
// { path: '/group', Component: lazy(() => import('@/pages/Staff/group')) },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/staff',
|
||||
Layout: AppLayout,
|
||||
children: [
|
||||
{ path: '/dep', Component: Dep },
|
||||
{ path: '/group', Component: Group },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
Layout: EmptyLayout,
|
||||
children: [
|
||||
{ path: '/index', Component: lazy(() => import('@/pages/Login')) },
|
||||
//
|
||||
],
|
||||
},
|
||||
{ path: '*', Layout: ErrorPage, children: [] },
|
||||
];
|
||||
10
src/index.css
Normal file
10
src/index.css
Normal file
@@ -0,0 +1,10 @@
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@supports (height: 100dvh) {
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
}
|
||||
25
src/interfaces/common.ts
Normal file
25
src/interfaces/common.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { ForwardedRef } from 'react';
|
||||
|
||||
/** params的基础类型 */
|
||||
export type IParamsBase = {
|
||||
curr_page: number;
|
||||
page_count: number;
|
||||
order?: string;
|
||||
};
|
||||
|
||||
/** ajax返回的基础类型 */
|
||||
export type IAjaxDataBase = {
|
||||
count: number | undefined;
|
||||
err_msg?: string;
|
||||
err_code?: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type IRef = {
|
||||
ref: ForwardedRef<any>;
|
||||
};
|
||||
|
||||
export type IOption = {
|
||||
value: string | number;
|
||||
label: string;
|
||||
};
|
||||
56
src/interfaces/finance.ts
Normal file
56
src/interfaces/finance.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/** 供应商 type */
|
||||
export type ISupplierType = {
|
||||
supplier_id?: number;
|
||||
name?: string;
|
||||
phone?: string;
|
||||
tax_no?: string;
|
||||
invoice_title?: string;
|
||||
bank_name?: string;
|
||||
bank_no?: string;
|
||||
finance_phone?: string;
|
||||
address?: string;
|
||||
tax_rate?: number | string;
|
||||
create_date?: string;
|
||||
comments?: string;
|
||||
};
|
||||
|
||||
/** 经销商 type */
|
||||
export type ISaleInfo = {
|
||||
sms_code?: string;
|
||||
sale_id?: number;
|
||||
name?: string;
|
||||
phone?: string;
|
||||
level?: number | string;
|
||||
level_name?: string;
|
||||
default_discount?: number | string;
|
||||
last_discount?: number;
|
||||
tax_no?: string;
|
||||
invoice_title?: string;
|
||||
bank_name?: string;
|
||||
bank_no?: string;
|
||||
finance_phone?: string;
|
||||
address?: string;
|
||||
tax_rate?: number;
|
||||
pay_ratio?: number;
|
||||
signed_date?: string;
|
||||
create_date?: string;
|
||||
comments?: string;
|
||||
is_staff?: any;
|
||||
login_name?: string; // 业务员信息
|
||||
bind_login_name?: string; // 绑定账号信息
|
||||
bind_user_phone?: string; // 绑定账号信息
|
||||
bind_user_id?: any; // 绑定账号信息
|
||||
staff_user_id?: any;
|
||||
staff_name?: string;
|
||||
nick_name?: string;
|
||||
};
|
||||
|
||||
/** 折扣 type */
|
||||
export type IDiscountInfo = {
|
||||
discount_id?: number;
|
||||
name?: string;
|
||||
code?: string;
|
||||
discount_value?: number;
|
||||
comments?: string;
|
||||
create_date?: string;
|
||||
};
|
||||
86
src/interfaces/staff.ts
Normal file
86
src/interfaces/staff.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/** 部门 */
|
||||
export type IDepartment = {
|
||||
comments?: string;
|
||||
company_id?: number;
|
||||
create_date?: string;
|
||||
department_id: number;
|
||||
name: string;
|
||||
state?: number;
|
||||
dept_type?: number;
|
||||
};
|
||||
|
||||
export type IGroup = {
|
||||
comments?: string;
|
||||
company_id?: number;
|
||||
create_date?: string;
|
||||
group_id: number;
|
||||
name: string;
|
||||
state?: number;
|
||||
rights?: any;
|
||||
};
|
||||
|
||||
export type IRightsItem3 = {
|
||||
comments: null | string;
|
||||
function_ch_name: string;
|
||||
function_en_name: string;
|
||||
function_id: number;
|
||||
function_sort: number;
|
||||
function_type: number;
|
||||
menu_id: number;
|
||||
checked?: boolean;
|
||||
};
|
||||
|
||||
export type IRightsItem2 = {
|
||||
comments: null | string;
|
||||
menu_ch_name: string;
|
||||
menu_en_name: string;
|
||||
menu_icon: null | string;
|
||||
menu_id: number;
|
||||
menu_sort: number;
|
||||
menu_type: number;
|
||||
menu_url: string;
|
||||
parent_menu_id: number;
|
||||
state: number;
|
||||
children: IRightsItem3[];
|
||||
};
|
||||
|
||||
export type IRightsItem1 = {
|
||||
comments: null | string;
|
||||
menu_ch_name: string;
|
||||
menu_en_name: string;
|
||||
menu_icon: null | string;
|
||||
menu_id: number;
|
||||
menu_sort: number;
|
||||
menu_type: number;
|
||||
menu_url: string;
|
||||
parent_menu_id: number;
|
||||
state: number;
|
||||
children: IRightsItem2[];
|
||||
};
|
||||
|
||||
export type IRights = IRightsItem1[];
|
||||
|
||||
export type IStaff = {
|
||||
group_type: any;
|
||||
comments: null | string;
|
||||
create_date: string;
|
||||
dep_name: string;
|
||||
department_id: number;
|
||||
group_id: string;
|
||||
staff_id: number;
|
||||
group_name: string;
|
||||
last_login_time: null | string;
|
||||
login_name: string;
|
||||
logo: null | string;
|
||||
nick_name: null | string;
|
||||
staff_type: number;
|
||||
state: number;
|
||||
user_id: number;
|
||||
user_phone: string;
|
||||
user_sex: number;
|
||||
visit_times: number;
|
||||
staff_state: number;
|
||||
wx_open_id?: string;
|
||||
wx_nick_name?: string;
|
||||
wx_head_img?: string;
|
||||
};
|
||||
40
src/interfaces/user.ts
Normal file
40
src/interfaces/user.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export interface UserInfo {
|
||||
login_name?: string;
|
||||
logo?: string;
|
||||
last_login_time?: string;
|
||||
nick_name?: string;
|
||||
state?: number;
|
||||
user_id?: number;
|
||||
user_sex?: number;
|
||||
user_phone?: string;
|
||||
visit_times?: string;
|
||||
create_date?: string;
|
||||
logoKey?: number;
|
||||
wx_head_img?: string;
|
||||
wx_nick_name?: string;
|
||||
wx_open_id?: string;
|
||||
self_account?: number;
|
||||
}
|
||||
|
||||
export interface CompanyInfo {
|
||||
company_address?: string;
|
||||
company_desc?: string;
|
||||
company_domain?: string;
|
||||
company_id?: number;
|
||||
company_logo?: string;
|
||||
company_name?: string;
|
||||
company_state?: string;
|
||||
create_date?: string;
|
||||
eff_date?: string;
|
||||
exp_date?: string;
|
||||
erp_name?: string;
|
||||
last_login_name?: string;
|
||||
user_id?: string;
|
||||
domain?: string;
|
||||
desc?: string;
|
||||
company_list?: any[];
|
||||
sale_id?: any;
|
||||
staff_type?: any;
|
||||
erp_version?: number;
|
||||
session_id?: string;
|
||||
}
|
||||
129
src/layouts/AppLayout.tsx
Normal file
129
src/layouts/AppLayout.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Layout, Spin } from 'antd';
|
||||
import { Content, Header } from 'antd/es/layout/layout';
|
||||
import Sider from 'antd/es/layout/Sider';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Footer from '@/components/Footer';
|
||||
import { GapBox } from '@/components/GapBox';
|
||||
import { HeaderUserInfo } from '@/components/Header/HeaderUserInfo';
|
||||
import { NavigateMenuDrawer } from '@/components/SiderMenu/NavigateMenuDrawer';
|
||||
import NavMenu from '@/components/SiderMenu/NavMenu';
|
||||
import TabNavPlugin from '@/components/TabNavPlugin';
|
||||
import { DefaultERPName, headerHeight } from '@/configs/config';
|
||||
import Outlet from '@/router/Outlet';
|
||||
import { useAuthStore } from '@/store/AuthStore';
|
||||
import { useUserStore } from '@/store/UserStore';
|
||||
import { getDevice, toObject } from '@/utils/common';
|
||||
import { loginState } from './base';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
|
||||
const AppLayout = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const isPhone = getDevice() == 'phone';
|
||||
const userStore = useUserStore();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
loginState().then((res) => {
|
||||
// console.log(res);
|
||||
userStore.updateUser(toObject(res.user_info));
|
||||
authStore.updateAuth(toObject(res.auth_info) as any);
|
||||
localStorage.setItem('admin_user_id', res?.user_info?.user_id);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!loading ? (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Header
|
||||
style={{
|
||||
background: '#fff',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 101,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
borderBottom: '1px solid #ddd',
|
||||
height: headerHeight,
|
||||
flexShrink: 0,
|
||||
lineHeight: 1,
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
<GapBox style={{ display: 'inline-flex' }}>
|
||||
{isPhone ? (
|
||||
<NavigateMenuDrawer />
|
||||
) : (
|
||||
<div style={{ fontWeight: 'bold', fontSize: 16 }}>{DefaultERPName}</div>
|
||||
)}
|
||||
{Date.now()}
|
||||
</GapBox>
|
||||
<div style={{ display: 'inline-flex' }}>
|
||||
<HeaderUserInfo />
|
||||
</div>
|
||||
</Header>
|
||||
<Layout style={{ justifyContent: 'flex-start' }}>
|
||||
{isPhone ? null : (
|
||||
<Sider
|
||||
// collapsible
|
||||
// onCollapse={(collapsed: boolean) => {
|
||||
// setCollapsed(collapsed);
|
||||
// }}
|
||||
style={{
|
||||
background: '#fff',
|
||||
overflow: 'auto',
|
||||
// height: `calc(100vh - ${headerHeight}px)`,
|
||||
position: 'sticky',
|
||||
// zIndex: 1000,
|
||||
left: 0,
|
||||
// top: headerHeight,
|
||||
}}
|
||||
width={200}
|
||||
// width={window?.dfConfig?.language == 'zh-cn' ? 100 : 240}
|
||||
// collapsed={collapsed}
|
||||
// collapsedWidth={60}
|
||||
>
|
||||
<NavMenu />
|
||||
{/* <NavMenuCard left={100} /> */}
|
||||
</Sider>
|
||||
)}
|
||||
<Content
|
||||
style={{
|
||||
// minHeight: `calc(100vh - ${headerHeight}px)`,
|
||||
padding: isPhone ? '12px 0 0 0' : '12px 12px 0 12px',
|
||||
overflow: 'inherit',
|
||||
background: '#F4F4F4',
|
||||
boxSizing: 'border-box',
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<TabNavPlugin />
|
||||
{/* <SelfAccount /> */}
|
||||
<div style={{ minHeight: `calc(100vh - ${headerHeight}px - 32px - 50px)`, background: '#fff' }}>
|
||||
<ErrorBoundary>
|
||||
<Outlet />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
<Footer />
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
) : (
|
||||
<Spin
|
||||
style={{ height: '90vh', display: 'flex', alignItems: 'center', justifyContent: 'center', maxHeight: '90vh' }}
|
||||
size='large'
|
||||
tip={<span style={{ color: '#000' }}>加载数据...</span>}
|
||||
>
|
||||
|
||||
</Spin>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default AppLayout;
|
||||
11
src/layouts/EmptyLayout.tsx
Normal file
11
src/layouts/EmptyLayout.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import Outlet from '@/router/Outlet';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
|
||||
const EmptyLayout = () => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Outlet />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
export default EmptyLayout;
|
||||
81
src/layouts/ErrorBoundary.tsx
Normal file
81
src/layouts/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Button, Result } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
type IProps = {
|
||||
fallback?: (error: any, info?: any) => React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
/** 错误边界组件 */
|
||||
class ErrorBoundary extends React.Component<IProps> {
|
||||
state: { hasError: boolean; errorMsg?: React.ReactNode };
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(_error: any) {
|
||||
// console.log("error", error);
|
||||
// 更新状态,以便下一次渲染将显示后备 UI。
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error: any, info: any) {
|
||||
// console.log(error, info);
|
||||
this.setState({ errorMsg: `${error}${info.componentStack}` });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// 你可以渲染任何自定义后备 UI
|
||||
return (
|
||||
this.props?.fallback?.(this.state.errorMsg) || (
|
||||
<Result
|
||||
status='error'
|
||||
title={
|
||||
<div style={{ fontSize: 20 }}>
|
||||
<div>页面发生异常,错误内容已上报!</div>
|
||||
<div>开发人员紧急修复中,敬请耐心等待!</div>
|
||||
</div>
|
||||
}
|
||||
subTitle={
|
||||
<pre
|
||||
style={{
|
||||
color: '#ff4d4f',
|
||||
whiteSpace: 'break-spaces',
|
||||
padding: 20,
|
||||
textAlign: 'left',
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{this.state.errorMsg}
|
||||
</pre>
|
||||
}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
minHeight: 'calc(100vh - 180px)',
|
||||
}}
|
||||
extra={[
|
||||
<Button
|
||||
type='primary'
|
||||
key='console'
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
请刷新重试
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return this.props?.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
13
src/layouts/base.ts
Normal file
13
src/layouts/base.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { requestLite } from '@/utils/useRequest';
|
||||
|
||||
export const loginState = async () => {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
requestLite('/Users/loginState').then((res) => {
|
||||
if (res?.err_code == 0) {
|
||||
resolve(res);
|
||||
} else {
|
||||
reject(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
9
src/main.tsx
Normal file
9
src/main.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App.tsx';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
// <StrictMode>
|
||||
<App />,
|
||||
// </StrictMode>,
|
||||
);
|
||||
26
src/pages/Error/index.tsx
Normal file
26
src/pages/Error/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Button, Result } from 'antd';
|
||||
|
||||
const ErrorPage = () => {
|
||||
document.title = '404 - 抱歉,您访问的页面不存在';
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Result
|
||||
status='404'
|
||||
title='404'
|
||||
subTitle={'抱歉,您访问的页面不存在'}
|
||||
extra={
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={() => {
|
||||
location.href = '/';
|
||||
}}
|
||||
>
|
||||
{'返回首页'}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorPage;
|
||||
34
src/pages/Index/index.tsx
Normal file
34
src/pages/Index/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
const Index = () => {
|
||||
return (
|
||||
<div>
|
||||
开发中...
|
||||
{/* Index */}
|
||||
{/* <Button
|
||||
onClick={() => {
|
||||
// console.log(a);
|
||||
throw new Error('测试');
|
||||
}}
|
||||
>
|
||||
测试
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
notificationEventBus.emit({ description: '你好' });
|
||||
}}
|
||||
>
|
||||
测试2
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
notificationEventBus.emitClose('MonitorUpdate');
|
||||
}}
|
||||
>
|
||||
关闭提示
|
||||
</Button>
|
||||
<a href='#/login'>login</a> */}
|
||||
{/* {undefined.map()} */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
86
src/pages/Login/index.tsx
Normal file
86
src/pages/Login/index.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Button, Input } from 'antd';
|
||||
import { stringify } from 'qs';
|
||||
import { useState } from 'react';
|
||||
import loginBg from '@/assets/loginBg.jpg';
|
||||
import { FormItemPlugin, FormPlugin } from '@/components/FormPlugin';
|
||||
import { DefaultERPName } from '@/configs/config';
|
||||
import { notificationEventBus } from '@/utils/EventBus';
|
||||
import { useRequest } from '@/utils/useRequest';
|
||||
|
||||
const Login = () => {
|
||||
const [userInfo, setUserInfo] = useState({ login_name: 'zhengw', password: '123456', login_type: 1 });
|
||||
const { request, loading } = useRequest('Users/login', {
|
||||
onSuccessCodeZero() {
|
||||
notificationEventBus.emit({ description: '登录成功' });
|
||||
location.href = '#/';
|
||||
},
|
||||
});
|
||||
|
||||
document.title = '登录';
|
||||
|
||||
const login = () => {
|
||||
request(stringify(userInfo));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundImage: `url(${loginBg})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 400,
|
||||
padding: 24,
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: 4,
|
||||
boxSizing: 'border-box',
|
||||
background: 'rgba(255,255,255,0.8)',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 24, fontSize: 20 }}>{DefaultERPName}后台登录</div>
|
||||
<FormPlugin>
|
||||
<FormItemPlugin allCol={24}>
|
||||
<Input
|
||||
prefix='账号'
|
||||
size='large'
|
||||
autoFocus
|
||||
value={userInfo.login_name}
|
||||
onChange={(e) => {
|
||||
userInfo.login_name = e.target.value;
|
||||
setUserInfo({ ...userInfo });
|
||||
}}
|
||||
onPressEnter={login}
|
||||
/>
|
||||
</FormItemPlugin>
|
||||
<FormItemPlugin allCol={24}>
|
||||
<Input.Password
|
||||
prefix='密码'
|
||||
size='large'
|
||||
value={userInfo.password}
|
||||
onChange={(e) => {
|
||||
userInfo.password = e.target.value;
|
||||
setUserInfo({ ...userInfo });
|
||||
}}
|
||||
onPressEnter={login}
|
||||
/>
|
||||
</FormItemPlugin>
|
||||
<FormItemPlugin allCol={24} style={{ marginBottom: 0 }}>
|
||||
<Button type='primary' size='large' block onClick={login} loading={loading}>
|
||||
登录
|
||||
</Button>
|
||||
</FormItemPlugin>
|
||||
</FormPlugin>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
17
src/pages/Staff/dep/index.tsx
Normal file
17
src/pages/Staff/dep/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import PageContainerPlugin from '@/components/PageContainer/PageContainerPlugin';
|
||||
|
||||
const DepContent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div>Dep</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Dep = () => (
|
||||
<PageContainerPlugin breadcrumb={['用户管理', '组织架构']}>
|
||||
<DepContent />
|
||||
</PageContainerPlugin>
|
||||
);
|
||||
|
||||
export default Dep;
|
||||
17
src/pages/Staff/group/index.tsx
Normal file
17
src/pages/Staff/group/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import PageContainerPlugin from '@/components/PageContainer/PageContainerPlugin';
|
||||
|
||||
const GroupContent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div>Group</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Group = () => (
|
||||
<PageContainerPlugin breadcrumb={['用户管理', '岗位角色']}>
|
||||
<GroupContent />
|
||||
</PageContainerPlugin>
|
||||
);
|
||||
|
||||
export default Group;
|
||||
26
src/pages/User/List/components/UserEditModal.tsx
Normal file
26
src/pages/User/List/components/UserEditModal.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import type React from 'react';
|
||||
import { useImperativeHandle, useState } from 'react';
|
||||
import ModalPlugin from '@/components/ModalPlugin';
|
||||
import type { IRef } from '@/utils/type';
|
||||
|
||||
interface IProps extends IRef {}
|
||||
|
||||
export type IUserEditModalType = {
|
||||
show: () => void;
|
||||
};
|
||||
|
||||
export const UserEditModal: React.FC<IProps> = (props) => {
|
||||
console.log(props.ref);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useImperativeHandle(props.ref, () => ({
|
||||
show: () => {
|
||||
setOpen(true);
|
||||
},
|
||||
}));
|
||||
return (
|
||||
<ModalPlugin open={open} onCancel={() => setOpen(false)}>
|
||||
11111
|
||||
</ModalPlugin>
|
||||
);
|
||||
};
|
||||
89
src/pages/User/List/components/state.ts
Normal file
89
src/pages/User/List/components/state.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { proxy } from 'valtio';
|
||||
import { deepClone } from 'valtio/utils';
|
||||
import type { IOption, IParamsBase } from '@/interfaces/common';
|
||||
import { UserServices } from '@/services/UserServices';
|
||||
import { toArray, toNumber, toObject } from '@/utils/common';
|
||||
import { requestLite } from '@/utils/useRequest2';
|
||||
|
||||
const defaultParams = { curr_page: 1, page_count: 20 };
|
||||
|
||||
type IParams = IParamsBase & {
|
||||
order_no?: string;
|
||||
custom_order_no?: string;
|
||||
custom_name?: string;
|
||||
custom_phone?: string;
|
||||
end_user_address?: string;
|
||||
end_user_phone?: string;
|
||||
end_user_name?: string;
|
||||
payed_state?: string;
|
||||
document_dateL?: string;
|
||||
document_dateU?: string;
|
||||
create_dateL?: string;
|
||||
create_dateU?: string;
|
||||
process_state?: any[];
|
||||
order_step?: any[];
|
||||
category_id?: any;
|
||||
};
|
||||
|
||||
export const userListStateProxy = proxy<{
|
||||
params: IParams;
|
||||
loading: boolean;
|
||||
ajaxData: any[];
|
||||
orderStepsOptions: IOption[];
|
||||
count: number;
|
||||
amount: {
|
||||
tot_discount_money?: string;
|
||||
tot_payed_amount?: string;
|
||||
tot_tax_last_money?: string;
|
||||
tot_un_payed_amount?: string;
|
||||
};
|
||||
showMoreSearch: boolean;
|
||||
reset: () => void;
|
||||
getData: () => void;
|
||||
clear: () => void;
|
||||
page: (current?: number, pageSize?: number) => void;
|
||||
// !分页回调函数
|
||||
pageCallback?: () => void;
|
||||
}>({
|
||||
params: deepClone(defaultParams),
|
||||
loading: false,
|
||||
showMoreSearch: false,
|
||||
ajaxData: [],
|
||||
orderStepsOptions: [],
|
||||
amount: {},
|
||||
count: 0,
|
||||
reset() {
|
||||
this.params = deepClone(defaultParams);
|
||||
this.getData();
|
||||
},
|
||||
page(current, pageSize) {
|
||||
if (!this.loading) {
|
||||
this.params.curr_page = current || 1;
|
||||
this.params.page_count = pageSize || this.params.page_count;
|
||||
this.getData();
|
||||
this.pageCallback?.();
|
||||
}
|
||||
},
|
||||
async getData() {
|
||||
this.loading = true;
|
||||
const temp: any = deepClone(this.params);
|
||||
if (temp.process_state?.length) {
|
||||
temp.process_state = temp.process_state.join(',');
|
||||
}
|
||||
if (temp.order_step?.length) {
|
||||
temp.order_step = temp.order_step.join(',');
|
||||
}
|
||||
const res: any = await requestLite(UserServices.list, temp);
|
||||
this.loading = false;
|
||||
this.ajaxData = toArray(res?.data);
|
||||
if (this.ajaxData.length == 0 && this.params.curr_page > 1) {
|
||||
this.page(this.params.curr_page - 1);
|
||||
}
|
||||
this.count = toNumber(res?.count);
|
||||
this.amount = toObject(res?.amount);
|
||||
},
|
||||
clear() {
|
||||
this.ajaxData = [];
|
||||
this.pageCallback = undefined;
|
||||
},
|
||||
});
|
||||
167
src/pages/User/List/index.tsx
Normal file
167
src/pages/User/List/index.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Button } from 'antd';
|
||||
import { useRef } from 'react';
|
||||
import { useSnapshot } from 'valtio';
|
||||
import { FormItemPlugin, FormPlugin } from '@/components/FormPlugin';
|
||||
import { GapBox } from '@/components/GapBox';
|
||||
import PageContainerPlugin from '@/components/PageContainer/PageContainerPlugin';
|
||||
import { FooterPagination, HeaderPagination } from '@/components/PaginationPlugin';
|
||||
import { MoreSearchButton, ResetButton, SearchButton, SearchInputPlugin } from '@/components/SearchButton';
|
||||
import type { ColumnsTypeUltra2 } from '@/components/TableColumnsFilterPlugin2';
|
||||
import { formatTableSort, TablePlugin } from '@/components/TablePlugin';
|
||||
import { tableFixedByPhone } from '@/utils/common';
|
||||
import { userListStateProxy } from './components/state';
|
||||
import { type IUserEditModalType, UserEditModal } from './components/UserEditModal';
|
||||
|
||||
const SearchBox = () => {
|
||||
const snap = useSnapshot(userListStateProxy);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormPlugin gutter={16}>
|
||||
<FormItemPlugin label='订单单号'>
|
||||
<SearchInputPlugin
|
||||
value={snap.params.order_no}
|
||||
onPressEnter={() => {
|
||||
userListStateProxy.page(1);
|
||||
}}
|
||||
onEnd={(v) => {
|
||||
userListStateProxy.params.order_no = v;
|
||||
}}
|
||||
/>
|
||||
</FormItemPlugin>
|
||||
<FormItemPlugin label='自定义单号'>
|
||||
<SearchInputPlugin
|
||||
value={snap.params.custom_order_no}
|
||||
onPressEnter={() => {
|
||||
userListStateProxy.page(1);
|
||||
}}
|
||||
onEnd={(v) => {
|
||||
userListStateProxy.params.custom_order_no = v;
|
||||
}}
|
||||
/>
|
||||
</FormItemPlugin>
|
||||
<FormItemPlugin label='经销商名称'>
|
||||
<SearchInputPlugin
|
||||
value={snap.params.custom_name}
|
||||
onPressEnter={() => {
|
||||
userListStateProxy.page(1);
|
||||
}}
|
||||
onEnd={(v) => {
|
||||
userListStateProxy.params.custom_name = v;
|
||||
}}
|
||||
/>
|
||||
</FormItemPlugin>
|
||||
|
||||
<FormItemPlugin>
|
||||
<GapBox>
|
||||
<SearchButton
|
||||
onClick={() => {
|
||||
userListStateProxy.page(1);
|
||||
}}
|
||||
/>
|
||||
<ResetButton
|
||||
onClick={() => {
|
||||
userListStateProxy.reset();
|
||||
}}
|
||||
/>
|
||||
<MoreSearchButton
|
||||
show={snap.showMoreSearch}
|
||||
onClick={() => {
|
||||
userListStateProxy.showMoreSearch = !userListStateProxy.showMoreSearch;
|
||||
}}
|
||||
/>
|
||||
</GapBox>
|
||||
</FormItemPlugin>
|
||||
</FormPlugin>
|
||||
<FormPlugin style={{ display: snap.showMoreSearch ? 'flex' : 'none' }} gutter={16}>
|
||||
<FormItemPlugin label='经销商手机'>
|
||||
<SearchInputPlugin
|
||||
value={snap.params.custom_phone}
|
||||
onPressEnter={() => {
|
||||
userListStateProxy.page(1);
|
||||
}}
|
||||
onEnd={(v) => {
|
||||
userListStateProxy.params.custom_phone = v;
|
||||
}}
|
||||
/>
|
||||
</FormItemPlugin>
|
||||
</FormPlugin>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Content = () => {
|
||||
const snap = useSnapshot(userListStateProxy);
|
||||
const tableColumns: ColumnsTypeUltra2<any> = [
|
||||
{
|
||||
columnName: '操作',
|
||||
title: '操作',
|
||||
fixed: tableFixedByPhone('left'),
|
||||
width: 52,
|
||||
render: () => {
|
||||
return <GapBox style={{ justifyContent: 'center', flexWrap: 'wrap' }}>{/* */}</GapBox>;
|
||||
},
|
||||
},
|
||||
{ columnName: '创建时间', width: 150, title: '创建时间', dataIndex: 'create_date', sorter: true },
|
||||
{ columnName: '备注', title: '备注', dataIndex: 'comments', width: 180 },
|
||||
{ columnName: '' },
|
||||
];
|
||||
|
||||
const UserEditModalRef = useRef<IUserEditModalType>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GapBox style={{ marginBottom: 12, justifyContent: 'space-between' }}>
|
||||
<GapBox>
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={() => {
|
||||
UserEditModalRef.current?.show();
|
||||
}}
|
||||
>
|
||||
新增用户
|
||||
</Button>
|
||||
</GapBox>
|
||||
<HeaderPagination
|
||||
current={snap.params.curr_page}
|
||||
pageSize={snap.params.page_count}
|
||||
total={snap.count}
|
||||
onChange={(page: number) => {
|
||||
userListStateProxy.page(page);
|
||||
}}
|
||||
/>
|
||||
</GapBox>
|
||||
<TablePlugin
|
||||
sticky={{ offsetHeader: window.dfConfig.tableStickyOffsetHeader }}
|
||||
scroll={{ x: 1000 }}
|
||||
loading={snap.loading}
|
||||
onChange={(_p, _f, sort: any) => {
|
||||
userListStateProxy.params.order = formatTableSort(sort);
|
||||
userListStateProxy.page(1);
|
||||
}}
|
||||
dataSource={snap.ajaxData}
|
||||
rowKey={'user_id'}
|
||||
columns={tableColumns}
|
||||
/>
|
||||
<FooterPagination
|
||||
current={snap.params.curr_page}
|
||||
pageSize={snap.params.page_count}
|
||||
total={snap.count}
|
||||
onChange={(page: number, pageSize: number): void => {
|
||||
userListStateProxy.page(page, pageSize);
|
||||
}}
|
||||
/>
|
||||
|
||||
<UserEditModal ref={UserEditModalRef} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UserList = () => (
|
||||
<PageContainerPlugin breadcrumb={['用户管理', '用户列表']}>
|
||||
<SearchBox />
|
||||
<Content />
|
||||
</PageContainerPlugin>
|
||||
);
|
||||
|
||||
export default UserList;
|
||||
8
src/router/Link.tsx
Normal file
8
src/router/Link.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import type React from "react";
|
||||
import { urlFormat } from "./routerUtils";
|
||||
|
||||
const Link: React.FC<{ href: string; children: React.ReactNode }> = (props) => {
|
||||
return <a href={urlFormat(props.href)}>{props.children}</a>;
|
||||
};
|
||||
|
||||
export default Link;
|
||||
48
src/router/Outlet.tsx
Normal file
48
src/router/Outlet.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Spin } from 'antd';
|
||||
import React, { Suspense, useEffect, useRef, useState } from 'react';
|
||||
import { routeMatchingHash } from './routerUtils';
|
||||
|
||||
const Outlet = () => {
|
||||
const [key, setKey] = useState(1);
|
||||
const OutLetRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handle = async () => {
|
||||
const res: any = routeMatchingHash();
|
||||
if (OutLetRef.current != res?.Component) {
|
||||
OutLetRef.current = res?.Component;
|
||||
setKey(Date.now());
|
||||
}
|
||||
};
|
||||
handle();
|
||||
|
||||
window.addEventListener('hashchange', handle);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('hashchange', handle);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment key={key}>
|
||||
{OutLetRef.current ? (
|
||||
OutLetRef.current?.$$typeof == Symbol.for('react.lazy') ? (
|
||||
<Suspense
|
||||
fallback={
|
||||
<Spin
|
||||
style={{ display: 'flex', height: '80vh', alignItems: 'center', justifyContent: 'center' }}
|
||||
spinning
|
||||
/>
|
||||
}
|
||||
>
|
||||
<OutLetRef.current />
|
||||
</Suspense>
|
||||
) : (
|
||||
<OutLetRef.current />
|
||||
)
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Outlet;
|
||||
32
src/router/Router.tsx
Normal file
32
src/router/Router.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { routerData } from './router-data.js';
|
||||
import { routeMatchingHash, routerFormat } from './routerUtils.js';
|
||||
import type { IRouteItem } from './types.js';
|
||||
|
||||
const Router: React.FC<{ routes: IRouteItem[] }> = (props) => {
|
||||
const { routes } = props;
|
||||
const [key, setKey] = useState(1);
|
||||
const LayoutRef = useRef<any>(null);
|
||||
routerData.routers = routerFormat(routes);
|
||||
|
||||
useEffect(() => {
|
||||
const handle = () => {
|
||||
const res: any = routeMatchingHash();
|
||||
if (LayoutRef.current != res?.Layout) {
|
||||
LayoutRef.current = res?.Layout;
|
||||
setKey(Date.now());
|
||||
}
|
||||
};
|
||||
handle();
|
||||
|
||||
window.addEventListener('hashchange', handle);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('hashchange', handle);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <React.Fragment key={key}>{LayoutRef.current ? <LayoutRef.current /> : null}</React.Fragment>;
|
||||
};
|
||||
|
||||
export default Router;
|
||||
9
src/router/router-data.ts
Normal file
9
src/router/router-data.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
interface IRouterData {
|
||||
mode: 'hash' | 'history';
|
||||
routers: Map<string, { title?: string; Layout: any; Component: any }>;
|
||||
}
|
||||
|
||||
export const routerData: IRouterData = {
|
||||
mode: 'hash',
|
||||
routers: new Map(),
|
||||
};
|
||||
72
src/router/routerUtils.ts
Normal file
72
src/router/routerUtils.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { routerData } from './router-data';
|
||||
|
||||
/** 路由导航 */
|
||||
export const navigate = (path: string) => {
|
||||
window.location.hash = urlFormat(path);
|
||||
};
|
||||
|
||||
export const urlFormat = (path: string) => {
|
||||
const url = `${path || ''}`.trim();
|
||||
if (url.startsWith('http') || url.startsWith('blob')) {
|
||||
return url;
|
||||
}
|
||||
return `#${url}`;
|
||||
};
|
||||
|
||||
/** 获取查询参数 */
|
||||
export const getURLSearchParams = () => {
|
||||
const { hash } = location;
|
||||
const params = hash.includes('?') ? hash.slice(hash.indexOf('?')) : '';
|
||||
return new URLSearchParams(params);
|
||||
};
|
||||
|
||||
export const getHash = () => {
|
||||
let hash = location.hash;
|
||||
if (hash.startsWith('#')) {
|
||||
hash = hash.slice(1);
|
||||
}
|
||||
if (hash.includes('?')) {
|
||||
hash = hash.slice(0, hash.indexOf('?'));
|
||||
}
|
||||
hash = hash.endsWith('/') ? hash.slice(0, hash.length - 1) : hash;
|
||||
return hash;
|
||||
};
|
||||
|
||||
export const routerFormat = (routes: any[]) => {
|
||||
const router = new Map();
|
||||
routes.forEach((el) => {
|
||||
router.set(`${el.path}`, { ...el });
|
||||
if (Array.isArray(el.children)) {
|
||||
el.children.forEach((ell: any, i: number) => {
|
||||
if (i == 0) {
|
||||
router.set(`${el.path}`, { ...el, ...ell });
|
||||
}
|
||||
router.set(`${el.path}${ell.path}`, { ...el, ...ell });
|
||||
});
|
||||
}
|
||||
});
|
||||
return router;
|
||||
};
|
||||
|
||||
export const routeMatchingHash = () => {
|
||||
const hash = getHash();
|
||||
let obj: any = {};
|
||||
// console.log(hash);
|
||||
if (routerData.routers.has(hash)) {
|
||||
obj = routerData.routers.get(hash);
|
||||
} else {
|
||||
if (hash) {
|
||||
if (routerData.routers.has('*')) {
|
||||
obj = routerData.routers.get('*');
|
||||
}
|
||||
//
|
||||
// console.warn('请配置 * 错误路由');
|
||||
} else {
|
||||
// 返回第一个路由
|
||||
obj = routerData.routers.values().next().value;
|
||||
}
|
||||
}
|
||||
|
||||
// document.title = obj.title || '';
|
||||
return obj;
|
||||
};
|
||||
12
src/router/types.ts
Normal file
12
src/router/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export type IRouteItemChild = {
|
||||
path: string;
|
||||
Component: any;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export type IRouteItem = {
|
||||
path: string;
|
||||
Layout: any;
|
||||
title?: string;
|
||||
children: IRouteItemChild[];
|
||||
};
|
||||
3
src/services/UserServices.ts
Normal file
3
src/services/UserServices.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const UserServices = {
|
||||
list: '/UserServices/list',
|
||||
} as const;
|
||||
18
src/store/AuthStore.ts
Normal file
18
src/store/AuthStore.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { create, type StoreApi, type UseBoundStore } from 'zustand';
|
||||
|
||||
export type IAuth = { [key: string]: boolean };
|
||||
export type IAuthByPathname = { [key: string]: string };
|
||||
|
||||
type IUser = {
|
||||
auth: IAuth;
|
||||
authByPathname: IAuthByPathname;
|
||||
updateAuth: (data: IAuth) => void;
|
||||
initAuthByPathname: (data: IAuthByPathname) => void;
|
||||
};
|
||||
|
||||
export const useAuthStore: UseBoundStore<StoreApi<IUser>> = create((set) => ({
|
||||
auth: {},
|
||||
updateAuth: (data) => set(() => ({ auth: data })),
|
||||
authByPathname: {},
|
||||
initAuthByPathname: (data: IAuthByPathname) => set(() => ({ authByPathname: data })),
|
||||
}));
|
||||
14
src/store/CompanyStore.ts
Normal file
14
src/store/CompanyStore.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { create, type StoreApi, type UseBoundStore } from "zustand";
|
||||
import type { CompanyInfo } from "./type";
|
||||
|
||||
type IBear = {
|
||||
company: CompanyInfo;
|
||||
updateCompany: (data: CompanyInfo) => void;
|
||||
};
|
||||
|
||||
export const useCompanyStore: UseBoundStore<StoreApi<IBear>> = create(
|
||||
(set) => ({
|
||||
company: {},
|
||||
updateCompany: (data) => set(() => ({ company: data })),
|
||||
}),
|
||||
);
|
||||
25
src/store/ConfigStore.ts
Normal file
25
src/store/ConfigStore.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { create, type StoreApi, type UseBoundStore } from 'zustand';
|
||||
|
||||
interface IConfigType {
|
||||
// config_id: number;
|
||||
config_value: string;
|
||||
// rel_config_id: number;
|
||||
// create_date: string;
|
||||
// config_type: string;
|
||||
// config_name: string;
|
||||
// config_desc: string;
|
||||
// form_type: string;
|
||||
// setting: string;
|
||||
// config_state: number;
|
||||
}
|
||||
|
||||
type IConfig = {
|
||||
config: { [key: string]: IConfigType };
|
||||
updateConfig: (data: { [key: string]: IConfigType }) => void;
|
||||
};
|
||||
|
||||
/** 参数化配置store */
|
||||
export const useConfigStore: UseBoundStore<StoreApi<IConfig>> = create((set) => ({
|
||||
config: {},
|
||||
updateConfig: (data) => set(() => ({ config: data })),
|
||||
}));
|
||||
11
src/store/RefreshStore.ts
Normal file
11
src/store/RefreshStore.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { create, type StoreApi, type UseBoundStore } from 'zustand';
|
||||
|
||||
type IUser = {
|
||||
refresh: number;
|
||||
updateRefresh: (data: number) => void;
|
||||
};
|
||||
|
||||
export const useRefreshStore: UseBoundStore<StoreApi<IUser>> = create((set) => ({
|
||||
refresh: 1,
|
||||
updateRefresh: (data) => set(() => ({ refresh: data })),
|
||||
}));
|
||||
22
src/store/UserConfigStore.ts
Normal file
22
src/store/UserConfigStore.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// import { create, type StoreApi, type UseBoundStore } from 'zustand';
|
||||
// import type { UsersConfig } from '@/services/UserServicesAndConfig';
|
||||
|
||||
// type IUsersConfig = {
|
||||
// [key in keyof typeof UsersConfig]?: {
|
||||
// config_id?: number;
|
||||
// config_name: string;
|
||||
// config_value: string;
|
||||
// update_date: string | null;
|
||||
// };
|
||||
// };
|
||||
|
||||
// type IUser = {
|
||||
// config: IUsersConfig;
|
||||
// updateConfig: (data: IUsersConfig) => void;
|
||||
// };
|
||||
|
||||
// /** 用户配置store */
|
||||
// export const useUsersConfigStore: UseBoundStore<StoreApi<IUser>> = create((set) => ({
|
||||
// config: {},
|
||||
// updateConfig: (data) => set(() => ({ config: data })),
|
||||
// }));
|
||||
12
src/store/UserStore.ts
Normal file
12
src/store/UserStore.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { create, type StoreApi, type UseBoundStore } from "zustand";
|
||||
import type { UserInfo } from "./type";
|
||||
|
||||
type IUser = {
|
||||
user: UserInfo;
|
||||
updateUser: (data: UserInfo) => void;
|
||||
};
|
||||
|
||||
export const useUserStore: UseBoundStore<StoreApi<IUser>> = create((set) => ({
|
||||
user: {},
|
||||
updateUser: (data) => set(() => ({ user: data })),
|
||||
}));
|
||||
11
src/store/indexDBStore.ts
Normal file
11
src/store/indexDBStore.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { create, type StoreApi, type UseBoundStore } from 'zustand';
|
||||
|
||||
type IIndexDB = {
|
||||
db: any;
|
||||
init: (data: any) => void;
|
||||
};
|
||||
|
||||
export const useIndexDBStore: UseBoundStore<StoreApi<IIndexDB>> = create((set) => ({
|
||||
db: {},
|
||||
init: (data) => set(() => ({ db: data })),
|
||||
}));
|
||||
39
src/store/type.ts
Normal file
39
src/store/type.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export type UserInfo = {
|
||||
login_name?: string;
|
||||
logo?: string;
|
||||
last_login_time?: string;
|
||||
nick_name?: string;
|
||||
state?: number;
|
||||
user_id?: number;
|
||||
user_sex?: number;
|
||||
user_phone?: string;
|
||||
visit_times?: string;
|
||||
create_date?: string;
|
||||
logoKey?: number;
|
||||
wx_head_img?: string;
|
||||
wx_nick_name?: string;
|
||||
wx_open_id?: string;
|
||||
self_account?: number;
|
||||
};
|
||||
|
||||
export type CompanyInfo = {
|
||||
company_address?: string;
|
||||
company_desc?: string;
|
||||
company_domain?: string;
|
||||
company_id?: number;
|
||||
company_logo?: string;
|
||||
company_name?: string;
|
||||
company_state?: string;
|
||||
create_date?: string;
|
||||
eff_date?: string;
|
||||
exp_date?: string;
|
||||
erp_name?: string;
|
||||
last_login_name?: string;
|
||||
user_id?: string;
|
||||
domain?: string;
|
||||
desc?: string;
|
||||
company_list?: any[];
|
||||
sale_id?: any;
|
||||
staff_type?: any;
|
||||
erp_version?: number;
|
||||
};
|
||||
150
src/utils/EventBus.ts
Normal file
150
src/utils/EventBus.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
// class EventBus {
|
||||
// eventMap: { [key: string]: (() => void)[] } = {};
|
||||
// constructor() {
|
||||
// // 存储事件映射:key=事件名,value=回调函数数组
|
||||
// this.eventMap = Object.create(null);
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 订阅事件
|
||||
// * @param {string} eventName - 事件名称
|
||||
// * @param {Function} callback - 事件触发时的回调函数
|
||||
// */
|
||||
// on(eventName: string, callback: () => void) {
|
||||
// if (typeof callback !== 'function') {
|
||||
// throw new Error('回调函数必须是函数类型');
|
||||
// }
|
||||
// // 若事件不存在,初始化一个空数组
|
||||
// if (!this.eventMap[eventName]) {
|
||||
// this.eventMap[eventName] = [];
|
||||
// }
|
||||
// // 将回调加入事件对应的数组(支持多个订阅者)
|
||||
// this.eventMap[eventName].push(callback);
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 发布事件(触发订阅者回调)
|
||||
// * @param {string} eventName - 事件名称
|
||||
// * @param {...any} args - 传递给回调的参数(支持多个)
|
||||
// */
|
||||
// emit(eventName: string, ...args: any) {
|
||||
// // 若事件无订阅者,直接返回
|
||||
// if (!this.eventMap[eventName]) return;
|
||||
// // 遍历所有订阅者,执行回调(传递参数)
|
||||
// // 用slice()创建副本,避免执行回调时修改数组(如取消订阅)导致遍历异常
|
||||
// this.eventMap[eventName].slice().forEach((callback: () => void) => {
|
||||
// callback.apply(this, args);
|
||||
// });
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 取消订阅
|
||||
// * @param {string} eventName - 事件名称
|
||||
// * @param {Function} [callback] - 要取消的回调(不传则取消该事件所有订阅)
|
||||
// */
|
||||
// off(eventName: string, callback: () => void) {
|
||||
// const callbacks = this.eventMap[eventName];
|
||||
// if (!callbacks) return;
|
||||
|
||||
// // 情况1:不传callback,取消该事件所有订阅
|
||||
// if (!callback) {
|
||||
// delete this.eventMap[eventName];
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // 情况2:传callback,只取消指定回调(避免影响其他订阅者)
|
||||
// const index = callbacks.indexOf(callback);
|
||||
// if (index !== -1) {
|
||||
// callbacks.splice(index, 1);
|
||||
// // 若事件无剩余订阅者,删除该事件(优化内存)
|
||||
// if (callbacks.length === 0) {
|
||||
// delete this.eventMap[eventName];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 订阅一次事件(触发后自动取消订阅)
|
||||
// * @param {string} eventName - 事件名称
|
||||
// * @param {Function} callback - 回调函数
|
||||
// */
|
||||
// once(eventName: string, callback: () => void) {
|
||||
// // 包装回调:执行后自动取消订阅
|
||||
// const wrapCallback = (...args: any) => {
|
||||
// callback.apply(this, args);
|
||||
// this.off(eventName, wrapCallback);
|
||||
// };
|
||||
// this.on(eventName, wrapCallback);
|
||||
// }
|
||||
// }
|
||||
|
||||
/** 单例 */
|
||||
// function singleton(className: object) {
|
||||
// let ins: any = null;
|
||||
// const proxy = new Proxy(className, {
|
||||
// construct(target, args) {
|
||||
// if (!ins) {
|
||||
// ins = Reflect.construct(target, args);
|
||||
// }
|
||||
// return ins;
|
||||
// },
|
||||
// });
|
||||
// proxy.prototype.constructor = proxy;
|
||||
// return proxy;
|
||||
// }
|
||||
|
||||
// const Bus = singleton(EventBus);
|
||||
|
||||
// export const eventBus = new Bus();
|
||||
|
||||
type INotificationConfig = {
|
||||
/** 标题 默认: 系统通知 */
|
||||
title?: ReactNode;
|
||||
/** 内容 */
|
||||
description?: ReactNode;
|
||||
/** 当前通知唯一标志 */
|
||||
key?: string;
|
||||
/** 通知类型, 默认: success */
|
||||
type?: 'success' | 'info' | 'error' | 'warning';
|
||||
duration?: number;
|
||||
};
|
||||
|
||||
const notificationEvent: any = {};
|
||||
|
||||
/**
|
||||
* 通知事件
|
||||
*/
|
||||
export const notificationEventBus = {
|
||||
/**
|
||||
* 监听打开通知事件
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
onOpen(callback: (data: INotificationConfig) => void) {
|
||||
notificationEvent.notification = callback;
|
||||
},
|
||||
/**
|
||||
* 发送通知数据
|
||||
* @param {INotificationConfig} data INotificationConfig
|
||||
*/
|
||||
emit(data: INotificationConfig) {
|
||||
data.type = data.type || 'success';
|
||||
data.title = data.title || '系统通知';
|
||||
notificationEvent.notification?.(data);
|
||||
},
|
||||
/**
|
||||
* 发送关闭通知
|
||||
* @param key 通知唯一标志
|
||||
*/
|
||||
emitClose(key: string) {
|
||||
notificationEvent.notificationClose?.(key);
|
||||
},
|
||||
/**
|
||||
* 监听关闭通知事件
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
onClose(callback: (key: string) => void) {
|
||||
notificationEvent.notificationClose = callback;
|
||||
},
|
||||
};
|
||||
4
src/utils/UniqueKey.ts
Normal file
4
src/utils/UniqueKey.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
let count = 1;
|
||||
/** 生成唯一key */
|
||||
// export const uKey = () => `${Date.now().toString(36)}-${Math.random().toString(36).substring(2)}`;
|
||||
export const uKey = () => count++;
|
||||
366
src/utils/common.ts
Normal file
366
src/utils/common.ts
Normal file
@@ -0,0 +1,366 @@
|
||||
import Big from 'big.js';
|
||||
import dayjs from 'dayjs';
|
||||
import { DefaultERPName } from '@/configs/config';
|
||||
|
||||
/** 获取设备屏幕大小 */
|
||||
export const getDevice = (): 'phone' | 'tablet' | 'desktop' => {
|
||||
const width = window.innerWidth;
|
||||
const isPhone = typeof navigator !== 'undefined' && navigator && navigator.userAgent.match(/phone/gi);
|
||||
|
||||
if (width < 768 || isPhone) {
|
||||
return 'phone';
|
||||
} else if (width < 1280 && width > 768) {
|
||||
return 'tablet';
|
||||
} else {
|
||||
return 'desktop';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断字符串包含字符串
|
||||
* @param str 待判断的字符串
|
||||
* @param search 查询字符串
|
||||
* @returns Boolean
|
||||
*/
|
||||
export const includesString = (str: string, search: string): boolean => {
|
||||
return `${str}`.toLocaleUpperCase().includes(search.toLocaleUpperCase());
|
||||
};
|
||||
|
||||
/** 路径前面添加 api */
|
||||
export const pathAddApiString = (path: string): string => {
|
||||
/** 生产环境不加 api */
|
||||
// if (import.meta.env.PROD) {
|
||||
// return path;
|
||||
// }
|
||||
if (typeof path == 'string') {
|
||||
if (`${path}`.startsWith('http') || `${path}`.startsWith('blob:http')) {
|
||||
return path;
|
||||
}
|
||||
if (`${path}`.startsWith('/')) {
|
||||
return `/api${path}`;
|
||||
}
|
||||
return `/api/${path}`;
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
/** 只保留数字 */
|
||||
export const preserveNumbers = (val: any): string => {
|
||||
return `${val}`.replace(/[^0-9]/gi, '');
|
||||
};
|
||||
|
||||
/** 判断数据是不是数组类型 */
|
||||
export const isArray = (data: any): boolean => {
|
||||
return data && Array.isArray(data);
|
||||
};
|
||||
|
||||
export const toArray = (data: any): any[] => {
|
||||
return isArray(data) ? data : [];
|
||||
};
|
||||
|
||||
/** 判断数据是不是对象类型 */
|
||||
export const isObject = (data: any): boolean => {
|
||||
return data && `${Object.prototype.toString.call(data)}`.includes('Object');
|
||||
};
|
||||
|
||||
export const toObject = (data: any): object => {
|
||||
return isObject(data) ? data : {};
|
||||
};
|
||||
|
||||
/** object clean */
|
||||
export const objectClear = (data: any): any => {
|
||||
if (isObject(data)) {
|
||||
for (const key of Object.keys(data)) {
|
||||
data[key] = undefined;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
/** 获取时间毫秒 */
|
||||
export const getNowTime = (): number => Date.now();
|
||||
|
||||
/** 日期格式化 YYYY-MM-DD */
|
||||
export const dateFormat = (date: Date): string => {
|
||||
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date
|
||||
.getDate()
|
||||
.toString()
|
||||
.padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
/** 获取当前日期 格式化 YYYY-MM-DD */
|
||||
export const getNowDate = (): string => {
|
||||
return dateFormat(new Date());
|
||||
};
|
||||
|
||||
/** 判断是json数据 */
|
||||
export const isJson = (str: any): boolean => {
|
||||
if (str && typeof str == 'string') {
|
||||
try {
|
||||
const obj = JSON.parse(str);
|
||||
return obj && typeof obj == 'object';
|
||||
} catch (_e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 解码json数据
|
||||
* @param data 数据
|
||||
* @returns array | object | null
|
||||
*/
|
||||
export const jsonParse = (data: any): any[] | object | null => {
|
||||
if (data) {
|
||||
if (typeof data == 'string') {
|
||||
try {
|
||||
const obj = JSON.parse(data);
|
||||
if (['Array', 'Object'].includes(Object.prototype.toString.call(obj).slice(8, -1))) {
|
||||
return obj;
|
||||
}
|
||||
} catch (_e) {
|
||||
//
|
||||
}
|
||||
} else if (['Array', 'Object'].includes(Object.prototype.toString.call(data).slice(8, -1))) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/** 判断是数字 */
|
||||
export const isNumber = (value: any): boolean => {
|
||||
// biome-ignore lint/suspicious/noGlobalIsNan: <isNaN>
|
||||
// biome-ignore lint/suspicious/noGlobalIsFinite: <isFinite>
|
||||
return !isNaN(Number.parseFloat(value)) && isFinite(value);
|
||||
};
|
||||
|
||||
/** 转成数字 */
|
||||
export const toNumber = (str: any): number => {
|
||||
return isNumber(str) ? Number(str) : 0;
|
||||
};
|
||||
|
||||
/** 转成字符串并去除前后空格 */
|
||||
export const toStringAndTrim = (value: any, trim = true): string => {
|
||||
const str = typeof value == 'string' ? value : '';
|
||||
return trim ? str.trim() : str;
|
||||
};
|
||||
|
||||
/** 替换中文标点 ?【】:() */
|
||||
export const chinesePunctuationReplace = (str: string) => {
|
||||
// ?【】:()
|
||||
|
||||
if (str && typeof str == 'string') {
|
||||
return str
|
||||
.replace(/?/g, '?')
|
||||
.replace(/【/g, '[')
|
||||
.replace(/】/g, ']')
|
||||
.replace(/:/g, ':')
|
||||
.replace(/(/g, '(')
|
||||
.replace(/)/g, ')')
|
||||
.replace(/“/g, '"')
|
||||
.replace(/”/g, '"')
|
||||
.replace(/‘/g, '"')
|
||||
.replace(/’/g, '"');
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
/** 打开二维码窗口 */
|
||||
export const openPrintWindow = (msg: string, desc?: string) => {
|
||||
window.open(
|
||||
`/#/print/qrcode?msg=${encodeURIComponent(msg)}${desc ? `&desc=${encodeURIComponent(desc)}` : ''}`,
|
||||
'_blank',
|
||||
'height=600,width=800',
|
||||
);
|
||||
};
|
||||
|
||||
export const formatErpConfigList = (data: any[]) => {
|
||||
const obj: any = {};
|
||||
toArray(data).forEach((item) => {
|
||||
obj[item.config_type_en] = {
|
||||
config_value: item.config_value,
|
||||
};
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
|
||||
/** 转换文件大小 */
|
||||
export const formatFileSize = (fileSize: any): string => {
|
||||
const file_size = toNumber(fileSize);
|
||||
if (file_size == 0) {
|
||||
return '0B';
|
||||
}
|
||||
const unitArr = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
let index = 0;
|
||||
index = Math.floor(Math.log(file_size) / Math.log(1024));
|
||||
const size = file_size / 1024 ** index;
|
||||
return size.toFixed(0) + unitArr[index];
|
||||
};
|
||||
|
||||
/** 文本脱敏 */
|
||||
export function desensitizedCommon(str: string, begin = 1, end = 1) {
|
||||
if (!str && begin + end >= str.length) {
|
||||
return '';
|
||||
}
|
||||
const leftStr = str.substring(0, begin);
|
||||
const rightStr = str.substring(str.length - end, str.length);
|
||||
let strCon = '';
|
||||
for (let i = 0; i < str.length - end - begin; i++) {
|
||||
strCon += '*';
|
||||
}
|
||||
return leftStr + strCon + rightStr;
|
||||
}
|
||||
|
||||
export const isImageFile = (path: string) => {
|
||||
return /\.(jpe?g|png|gif|svg|webp|bmp)$/i.test(path);
|
||||
};
|
||||
|
||||
export const getBoundingClientRect = (
|
||||
dom: any,
|
||||
): {
|
||||
bottom: number;
|
||||
height: number;
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
width: number;
|
||||
} => {
|
||||
return (
|
||||
dom?.getBoundingClientRect() || {
|
||||
bottom: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 商品数量转换成显示单位数量
|
||||
* @param goods 商品
|
||||
* @param {string} key 商品数量的key
|
||||
* @returns {Object} {nums: number, unit: string}
|
||||
*/
|
||||
export const goodsStockFormatShowUnit = (goods: any, key: string): { nums: any; unit: string } => {
|
||||
const g: any = toObject(goods);
|
||||
const unit = g.depot_show_unit_name || '';
|
||||
const value = g[key];
|
||||
// if (goods.depot_show_unit && goods.base_unit != goods.depot_show_unit) {
|
||||
// value = new Big(toNumber(value) / toNumber(goods.unit_ratio)).toFixed(3);
|
||||
// unit = goods.depot_show_unit_name;
|
||||
// }
|
||||
|
||||
return {
|
||||
nums: value,
|
||||
unit,
|
||||
};
|
||||
};
|
||||
|
||||
type IGoodsRatioFormatNum = {
|
||||
base_unit: any;
|
||||
two_unit: any;
|
||||
ratio: any;
|
||||
unit: any;
|
||||
num: any;
|
||||
};
|
||||
/** 商品根据比例换算数量 */
|
||||
export const goodsRatioFormatNum = (goods: IGoodsRatioFormatNum): number => {
|
||||
let num = toNumber(goods.num);
|
||||
const ratio = toNumber(goods.ratio);
|
||||
if (goods.base_unit == goods.unit) {
|
||||
num = Big(num).times(ratio).toNumber();
|
||||
} else {
|
||||
if (ratio != 0) {
|
||||
num = Big(num).div(ratio).toNumber();
|
||||
} else {
|
||||
console.warn('goodsRatioFormatNum被除数为0');
|
||||
}
|
||||
}
|
||||
return num;
|
||||
};
|
||||
|
||||
/**
|
||||
* 数组对象排序 数字类型
|
||||
* @param data
|
||||
* @returns data
|
||||
*/
|
||||
export const arraySortTypeNumber = (arr: any, order?: string) => {
|
||||
const data = toArray(arr);
|
||||
if (order) {
|
||||
const [key, sort] = order.split(' ');
|
||||
data.sort((a: any, b: any) => {
|
||||
if (sort === 'asc') {
|
||||
return toNumber(a[key]) - toNumber(b[key]);
|
||||
} else {
|
||||
return toNumber(b[key]) - toNumber(a[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
/** 导出文本 */
|
||||
export const exportText = (text: string, filename: string) => {
|
||||
const blob = new Blob([`${text}`], { type: 'text/plain;charset=utf-8' });
|
||||
const objectURL = URL.createObjectURL(blob);
|
||||
downloadFile(objectURL, filename);
|
||||
URL.revokeObjectURL(objectURL);
|
||||
};
|
||||
|
||||
/** 下载文件 */
|
||||
export const downloadFile = (objectURL: string, filename: string) => {
|
||||
const aTag = document.createElement('a');
|
||||
aTag.href = objectURL;
|
||||
aTag.download = filename;
|
||||
aTag.click();
|
||||
};
|
||||
|
||||
/** 表格锁列小屏幕不锁列 */
|
||||
export const tableFixedByPhone = (fixed: 'left' | 'right') => {
|
||||
if (window.dfConfig.isPhone) {
|
||||
return undefined;
|
||||
}
|
||||
return fixed;
|
||||
};
|
||||
|
||||
/** ajax参数转json字符串 */
|
||||
export const paramsToJson = (params: any): object => {
|
||||
const obj: any = {};
|
||||
Object.keys(toObject(params)).forEach((key) => {
|
||||
if (isArray(params[key]) || isObject(params[key])) {
|
||||
obj[key] = JSON.stringify(params[key]);
|
||||
} else {
|
||||
obj[key] = params[key];
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
|
||||
export async function dingRequest(msg: string) {
|
||||
const loginName = localStorage.getItem('login_name');
|
||||
const userPhone = localStorage.getItem('user_phone');
|
||||
const userIP = localStorage.getItem('userIP');
|
||||
const href = window.location.href;
|
||||
const userAgent = navigator.userAgent;
|
||||
|
||||
let baseMsg = `${DefaultERPName}发生错误\n报错地址:${href}\n登录用户:${loginName}\n手机号码:${userPhone}\nIP地址:${userIP}\n`;
|
||||
baseMsg += `浏览器信息:${userAgent}\n触发时间:${dayjs().format('YYYY-MM-DD HH:mm:ss')}`;
|
||||
|
||||
// !开发环境
|
||||
if (import.meta.env.DEV || location.hostname == 'free.erp') {
|
||||
return;
|
||||
}
|
||||
// 发送钉钉消息
|
||||
return await fetch('https://chenfeng.tech:7779/Weixin-User-ding', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
},
|
||||
body: `token=3945cee5317f212d824da3fef16e321bb23ae5c3cf6b5f81d37f90568be89b1e&msg=${baseMsg}\n${msg}`,
|
||||
});
|
||||
}
|
||||
184
src/utils/commonUtils.ts
Normal file
184
src/utils/commonUtils.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import Big from 'big.js';
|
||||
import { isArray, isObject, toNumber, toObject, toStringAndTrim } from './common.ts';
|
||||
|
||||
/** 搜索参数格式化(字符串去除前后空格) */
|
||||
export const paramsFormat = (
|
||||
params: any,
|
||||
config?: {
|
||||
/** 是否将数组转json */
|
||||
arrayToJson?: boolean;
|
||||
/** 是否将对象转json */
|
||||
objectToJson?: boolean;
|
||||
/** 是否将数组和对象转json */
|
||||
toJson?: boolean;
|
||||
},
|
||||
): object => {
|
||||
const obj: any = {};
|
||||
Object.keys(toObject(params)).forEach((key) => {
|
||||
if (typeof params[key] === 'string') {
|
||||
obj[key] = toStringAndTrim(params[key]);
|
||||
} else {
|
||||
if (config?.toJson && (isArray(params[key]) || isObject(params[key]))) {
|
||||
obj[key] = JSON.stringify(params[key]);
|
||||
} else {
|
||||
if (config?.arrayToJson && isArray(params[key])) {
|
||||
obj[key] = JSON.stringify(params[key]);
|
||||
} else if (config?.objectToJson && isObject(params[key])) {
|
||||
obj[key] = JSON.stringify(params[key]);
|
||||
} else {
|
||||
obj[key] = params[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* sleep
|
||||
* @param callback 回调函数
|
||||
* @param ms 毫秒, 默认300ms
|
||||
* @returns
|
||||
*/
|
||||
export const sleep = (callback?: () => void, ms = 300): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
callback?.();
|
||||
resolve(true);
|
||||
}, ms);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 空数据是返回前一页
|
||||
* @param dataLength
|
||||
* @param curr_page 当前页
|
||||
* @param page 分页函数
|
||||
*/
|
||||
export const navigateBackIfEmpty = (dataLength: number, curr_page: number, page: (curr: number) => void) => {
|
||||
dataLength === 0 && curr_page > 1 && page(curr_page - 1);
|
||||
};
|
||||
|
||||
/** 检测支持语言 */
|
||||
export const checkSupportLanguage = (lang?: string | null) => (lang && ['zh-cn', 'en'].includes(lang) ? lang : 'zh-cn');
|
||||
|
||||
// export const notificationFun = (option: {
|
||||
// title?: React.ReactNode;
|
||||
// type?: 'info' | 'success' | 'warning' | 'error' | 'normal';
|
||||
// content?: React.ReactNode;
|
||||
// }) => {
|
||||
// const { title = t('系统提示'), type = 'success' } = option;
|
||||
// Notification[type]({
|
||||
// title: title,
|
||||
// content: option.content || '',
|
||||
// });
|
||||
// };
|
||||
|
||||
// export const modalFun = (option: {
|
||||
// title?: React.ReactNode;
|
||||
// type?: 'info' | 'success' | 'warning' | 'error' | 'confirm';
|
||||
// content?: React.ReactNode;
|
||||
// onOk: (e?: MouseEvent) => Promise<any>;
|
||||
// okText?: string;
|
||||
// okButtonProps?: ButtonProps;
|
||||
// }) => {
|
||||
// const { title = t('系统提示'), type = 'confirm', onOk } = option;
|
||||
// Modal[type]({
|
||||
// title: title,
|
||||
// content: option.content || '',
|
||||
// okButtonProps: {
|
||||
// autoFocus: true,
|
||||
// ...option.okButtonProps,
|
||||
// },
|
||||
// onOk: onOk,
|
||||
// okText: option.okText,
|
||||
// });
|
||||
// };
|
||||
|
||||
/** 笛卡尔积 */
|
||||
export const cartesianProduct = (...arrays: any[]) => {
|
||||
let result: any[] = [];
|
||||
// 处理边界情况:如果没有传入数组,返回空数组
|
||||
if (arrays.length === 0) return result;
|
||||
|
||||
// 初始结果为第一个数组的每个元素组成的单元素数组
|
||||
result = arrays[0].map((item: any) => [item]);
|
||||
|
||||
// 遍历剩余的数组,与当前结果合并计算笛卡尔积
|
||||
for (let i = 1; i < arrays.length; i++) {
|
||||
const currentArray = arrays[i];
|
||||
const temp: any[] = [];
|
||||
// 对于结果中的每个已有组合
|
||||
for (const existingCombination of result) {
|
||||
// 与当前数组的每个元素组合
|
||||
for (const item of currentArray) {
|
||||
temp.push([...existingCombination, item]);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新结果为新的组合
|
||||
result = temp;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const blobToFile = (blob: BlobPart[], fileName: string, options?: FilePropertyBag) => {
|
||||
return new File(
|
||||
blob,
|
||||
fileName,
|
||||
options ?? {
|
||||
lastModified: Date.now(),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/** Map数据转对象 */
|
||||
export const MapToObject = (data: Map<any, any>) => Object.fromEntries(data);
|
||||
|
||||
/** Map数据转对象json */
|
||||
export const MapToJson = (data: Map<any, any>) => JSON.stringify(MapToObject(data));
|
||||
|
||||
/** 深拷贝 Map 对象(支持基本类型和引用类型只支持(一层)) */
|
||||
export const deepCopyMap = (data: Map<any, any>) => {
|
||||
const newMap = new Map();
|
||||
for (const [key, value] of data) {
|
||||
if (Array.isArray(value)) {
|
||||
// 处理数组
|
||||
newMap.set(key, [...value]); // 数组深拷贝(一层)
|
||||
} else if (isObject(value)) {
|
||||
// 处理对象(非 null 且为对象类型)
|
||||
newMap.set(key, { ...value }); // 对象深拷贝(一层)
|
||||
} else {
|
||||
// 基本类型直接赋值
|
||||
newMap.set(key, value);
|
||||
}
|
||||
}
|
||||
return newMap;
|
||||
};
|
||||
|
||||
/** Set数据转数组 */
|
||||
export const SetToArray = (data: Set<any>) => [...data];
|
||||
|
||||
/** Set数据转数组json */
|
||||
export const SetToJson = (data: Set<any>) => JSON.stringify(SetToArray(data));
|
||||
|
||||
/** 价格保留位数 */
|
||||
export const priceRetentionDigits = (value?: number) => {
|
||||
return toNumber(Big(toNumber(value)).toFixed(window.dfERPConfig.PRICE_HOLD_POINT));
|
||||
};
|
||||
|
||||
/** 价格保留位数字符串类型 */
|
||||
export const priceRetentionDigitsString = (value?: number) => {
|
||||
return Big(toNumber(value)).toFixed(window.dfERPConfig.PRICE_HOLD_POINT);
|
||||
};
|
||||
|
||||
/** 数量保留位数 */
|
||||
export const numRetentionDigits = (value?: number) => {
|
||||
return toNumber(Big(toNumber(value)).toFixed(window.dfERPConfig.NUMS_HOLD_POINT));
|
||||
};
|
||||
|
||||
/** 数量保留位数字符串类型 */
|
||||
export const numRetentionDigitsString = (value?: number) => {
|
||||
return Big(toNumber(value)).toFixed(window.dfERPConfig.NUMS_HOLD_POINT);
|
||||
};
|
||||
25
src/utils/copyToClipboard.ts
Normal file
25
src/utils/copyToClipboard.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export const copyToClipboard = (textToCopy: string) => {
|
||||
// navigator clipboard 需要https等安全上下文
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// navigator clipboard 向剪贴板写文本
|
||||
return navigator.clipboard.writeText(textToCopy);
|
||||
} else {
|
||||
// 创建text area
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = textToCopy;
|
||||
// 使text area不在viewport,同时设置不可见
|
||||
textArea.style.position = 'absolute';
|
||||
textArea.style.opacity = '0';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// 执行复制命令并移除文本框
|
||||
document.execCommand('copy') ? resolve() : reject();
|
||||
textArea.remove();
|
||||
});
|
||||
}
|
||||
};
|
||||
69
src/utils/http.ts
Normal file
69
src/utils/http.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { pathAddApiString } from './common';
|
||||
import type { IAjaxDataBase } from './type';
|
||||
|
||||
// 辅助方法:统一处理响应内容(支持 json/text/blob 等格式)
|
||||
// async function getResponseContent(response: any) {
|
||||
// try {
|
||||
// // 优先尝试解析为 JSON(大部分接口的错误响应格式)
|
||||
// return await response.json();
|
||||
// } catch (_e) {
|
||||
// try {
|
||||
// // 若不是 JSON,解析为文本
|
||||
// return await response.text();
|
||||
// } catch (_e2) {
|
||||
// // 若为二进制数据(如图片错误),返回类型提示
|
||||
// return `[二进制数据,类型: ${response.headers.get('Content-Type')}]`;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
const ajax = async (method: string, url: string, data: any, config?: any) => {
|
||||
try {
|
||||
const res: IAjaxDataBase = await fetch(pathAddApiString(url), {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
},
|
||||
method: method,
|
||||
body: data,
|
||||
credentials: 'include',
|
||||
mode: 'cors',
|
||||
...config,
|
||||
}).then((res) => res.json());
|
||||
|
||||
return res;
|
||||
} catch (error: any) {
|
||||
const err = {
|
||||
message: error.message,
|
||||
status: error.status || '无', // 仅 HTTP 状态错误有
|
||||
url: error.url || url,
|
||||
responseBody: error.responseBody || '无', // 仅 HTTP 状态错误有
|
||||
stack: error.stack, // 错误堆栈(定位代码位置)
|
||||
cause: error.cause, // 网络错误的底层原因(Chrome 96+ 支持)
|
||||
type: error.name, // 错误类型(如 TypeError/AbortError)
|
||||
};
|
||||
|
||||
// console.error('【fetch 详细错误】', error);
|
||||
// 可根据错误类型做针对性处理
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('请求被手动取消');
|
||||
} else if (error.message.includes('CORS')) {
|
||||
console.log('跨域错误,请检查服务端跨域配置');
|
||||
}
|
||||
|
||||
throw err; // 可选:向上层抛出,让调用方处理
|
||||
}
|
||||
};
|
||||
|
||||
export const post = (url: string, data?: any, config?: any) => {
|
||||
return ajax('POST', url, data, config);
|
||||
};
|
||||
|
||||
export const get = (url: string, data: any, config?: any) => {
|
||||
return ajax('GET', url, data, config);
|
||||
};
|
||||
|
||||
export const request = {
|
||||
post,
|
||||
get,
|
||||
};
|
||||
25
src/utils/type.ts
Normal file
25
src/utils/type.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { ForwardedRef } from 'react';
|
||||
|
||||
/** params的基础类型 */
|
||||
export type IParamsBase = {
|
||||
curr_page: number;
|
||||
page_count: number;
|
||||
order?: string;
|
||||
};
|
||||
|
||||
/** ajax返回的基础类型 */
|
||||
export type IAjaxDataBase = {
|
||||
count: number | undefined;
|
||||
err_msg?: string;
|
||||
err_code?: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type IRef = {
|
||||
ref: ForwardedRef<any>;
|
||||
};
|
||||
|
||||
export type IOption = {
|
||||
value: string | number;
|
||||
label: string;
|
||||
};
|
||||
51
src/utils/update.js
Normal file
51
src/utils/update.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as fs from "node:fs";
|
||||
|
||||
// 获取版本号
|
||||
const currDate = new Date();
|
||||
const year = currDate.getFullYear();
|
||||
const month = currDate.getMonth() + 1;
|
||||
const strMonth = month >= 1 && month <= 9 ? `0${month}` : `${month}`;
|
||||
const date = currDate.getDate();
|
||||
const strDate = date >= 0 && date <= 9 ? `0${date}` : `${date}`;
|
||||
const hourMinutes = `${currDate.getHours()}${currDate.getMinutes()}`;
|
||||
const version = `V${year}${strMonth}${strDate}-${hourMinutes}`;
|
||||
const logDir = process.argv.slice(2)[0] ?? "";
|
||||
//span: { xl: 2, xxl: 2 },
|
||||
// 读取组件版本
|
||||
fs.readFile("package.json", "utf8", (_err, dataStr) => {
|
||||
const data = JSON.parse(dataStr);
|
||||
let comStr = "[\n";
|
||||
Object.keys(data.dependencies).forEach((item, index) => {
|
||||
comStr += ` {\n key: ${
|
||||
index + 1
|
||||
},\n label: '${item}',\n value: '${
|
||||
data.dependencies[item]
|
||||
}',\n },\n`;
|
||||
});
|
||||
comStr += " ]";
|
||||
|
||||
const cfg = `// 基础配置文件,记录版本号,编译时间等
|
||||
export const EnvConfig = {
|
||||
version: '${version}',
|
||||
compile: '${currDate.toLocaleString()}',
|
||||
helpName: '易宝赞系统操作手册',
|
||||
helpAddr: 'https://docs.qq.com/doc/DQXZTV3lvcXpqdkt3',
|
||||
docName: '易宝赞系统更新说明 2025-04-27',
|
||||
docAddr: 'https://docs.qq.com/aio/DS2NCRFFseG9Ma3Ja?p=60db8i0gVHuMAuNx56deBp',
|
||||
comItems: ${comStr},
|
||||
}`;
|
||||
|
||||
fs.writeFile(`./${logDir}/.cfg.ts`, cfg, (err) => {
|
||||
if (err) console.log("日志写入失败", err);
|
||||
else console.log("日志写入成功");
|
||||
});
|
||||
|
||||
fs.writeFile(
|
||||
`./${logDir}/public/ver.txt`,
|
||||
`${Math.round(Date.now() / 1000).toString(16)}`,
|
||||
(err) => {
|
||||
if (err) console.log("版本写入失败", err);
|
||||
else console.log("版本写入成功");
|
||||
}
|
||||
);
|
||||
});
|
||||
154
src/utils/useRequest.ts
Normal file
154
src/utils/useRequest.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { useState } from 'react';
|
||||
// import { dingRequest } from '@/services/common';
|
||||
import { dingRequest, toNumber } from './common';
|
||||
import { notificationEventBus } from './EventBus';
|
||||
import { post } from './http';
|
||||
import type { IAjaxDataBase } from './type';
|
||||
|
||||
type IOptions = {
|
||||
// onSuccess?: (res: IAjaxDataBase, params: any, cancel: boolean) => void;
|
||||
onSuccess?: (res: IAjaxDataBase, params: any) => void;
|
||||
/** 错误码为 0 的时候执行 */
|
||||
onSuccessCodeZero?: (res: IAjaxDataBase, params: any) => void;
|
||||
onCatch?: (error: any) => void;
|
||||
// method?: 'POST' | 'GET';
|
||||
hideNotice?: boolean;
|
||||
};
|
||||
|
||||
export const useRequest = (url: string, options?: IOptions) => {
|
||||
// 请求返回的数据
|
||||
const [data, setData] = useState<any>(null);
|
||||
// 请求返回的错误信息
|
||||
const [error, setError] = useState<any>(null);
|
||||
// 请求的loading 状态
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
// const cancelRef = useRef<any>(false);
|
||||
// function cancelRequest() {
|
||||
// cancelRef.current = true;
|
||||
// }
|
||||
|
||||
// loader 方法通过 fetch API 发出 http 请求
|
||||
const request = async (data?: any, config?: any) => {
|
||||
// cancelRef.current = false;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await post(url, data, config);
|
||||
|
||||
if (res?.err_code != 0) {
|
||||
if (res.err_code == 110000) {
|
||||
location.href = '#/login';
|
||||
} else {
|
||||
if (options?.hideNotice != true) {
|
||||
notificationEventBus.emit({
|
||||
title: `${'错误码'}:${res.err_code}`,
|
||||
description: res.err_msg,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (res?.err_code === 0) {
|
||||
options?.onSuccessCodeZero?.(res, data);
|
||||
}
|
||||
options?.onSuccess?.(res, data);
|
||||
setData(res);
|
||||
setCount(toNumber(res?.count));
|
||||
setLoading(false);
|
||||
return res;
|
||||
} catch (error: any) {
|
||||
setError(error);
|
||||
setLoading(false);
|
||||
// const traces = toArray(error?.response?.data?.traces).map((el) => {
|
||||
// return {
|
||||
// file: el?.file,
|
||||
// line: el?.line,
|
||||
// message: el?.message,
|
||||
// };
|
||||
// });
|
||||
console.log(error);
|
||||
notificationEventBus.emit({ title: `${'服务错误'}`, description: `${error?.message}`, type: 'error' });
|
||||
options?.onCatch?.(error);
|
||||
|
||||
// const params = JSON.parse(JSON.stringify(data));
|
||||
// console.log(error?.response?.data?.traces);
|
||||
const msg = `接口地址:${url}\n响应信息:${error?.message}\n错误信息:${JSON.stringify(
|
||||
error?.response?.data || '',
|
||||
).replace(/"/g, '')}\n请求参数:${JSON.stringify(data || '').replace(/"/g, '')}`;
|
||||
// console.log(msg);
|
||||
dingRequest(msg || '错误');
|
||||
}
|
||||
};
|
||||
// 将 loader, data, error, loading 作为自定义 hook 的返回值 cancelRequest
|
||||
return { request, data, loading, error, count };
|
||||
};
|
||||
|
||||
type IConfig = {
|
||||
hideNotice?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 轻量级请求
|
||||
* @param url 请求地址
|
||||
* @param data 请求参数
|
||||
* @param config 请求配置项 + 提示组件 {}
|
||||
* @returns Promise<any>
|
||||
* */
|
||||
export const requestLite = async (url: string, data?: any, config?: IConfig) => {
|
||||
const { hideNotice = false, ...option } = config || {};
|
||||
try {
|
||||
const res = await post(url, data, option);
|
||||
if (res?.err_code != 0) {
|
||||
if (res?.err_code == 110000) {
|
||||
location.href = '#/login';
|
||||
} else {
|
||||
if (!hideNotice) {
|
||||
notificationEventBus.emit({ title: `${'错误码'}:${res.err_code}`, description: res.err_msg, type: 'error' });
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
if (!hideNotice) {
|
||||
notificationEventBus.emit({ title: `${'服务错误'}`, description: `${error?.message}`, type: 'error' });
|
||||
}
|
||||
const msg = `接口地址:${url}\n响应信息:${error?.message}\n错误信息:${JSON.stringify(
|
||||
error?.response?.data || '',
|
||||
).replace(/"/g, '')}\n请求参数:${JSON.stringify(data || '').replace(/"/g, '')}`;
|
||||
dingRequest(msg || '错误');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 请求获取文件
|
||||
* @param url 文件地址
|
||||
* @param options 参数
|
||||
* @param options.oss 判断是不是 oss文件, 默认 true
|
||||
* @returns Promise<File>
|
||||
*/
|
||||
// export const requestFile = async (
|
||||
// url: string,
|
||||
// options: { oss?: boolean } = { oss: true },
|
||||
// ) => {
|
||||
// const u = url.startsWith("/") ? url.substring(1) : url;
|
||||
// return new Promise<Blob>((resolve, reject) => {
|
||||
// try {
|
||||
// fetch(options.oss ? `${OSSBaseUrl}${u}` : url, { mode: "cors" })
|
||||
// .then((response) => {
|
||||
// if (!response.ok) {
|
||||
// throw new Error(`HTTP 错误!状态:${response.status}`);
|
||||
// }
|
||||
// return response.blob();
|
||||
// })
|
||||
// .then((res) => {
|
||||
// // const fileName = url.split('/').pop() || '';
|
||||
// // resolve(blobToFile([res], fileName));
|
||||
// resolve(res);
|
||||
// });
|
||||
// } catch (error) {
|
||||
// reject(error);
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
79
src/utils/useRequest2.ts
Normal file
79
src/utils/useRequest2.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
// import { OSSBaseUrl } from '@/config/config';
|
||||
import type { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import { dingRequest } from './common';
|
||||
import { post } from './http';
|
||||
|
||||
type IConfig = {
|
||||
notification?: NotificationInstance;
|
||||
};
|
||||
|
||||
/**
|
||||
* 轻量级请求
|
||||
* @param url 请求地址
|
||||
* @param data 请求参数
|
||||
* @param config 请求配置项 + 提示组件 {}
|
||||
* @returns Promise<any>
|
||||
* */
|
||||
export const requestLite = async (url: string, data?: any, config?: IConfig) => {
|
||||
const { notification, ...option } = config || {};
|
||||
try {
|
||||
const res = await post(url, data, option);
|
||||
if (res?.err_code != 0) {
|
||||
if (res?.err_code == 110000) {
|
||||
location.href = '#/login';
|
||||
} else {
|
||||
if (notification) {
|
||||
notification.error({
|
||||
title: `${'错误码'}:${res.err_code}`,
|
||||
description: res.err_msg,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
if (notification) {
|
||||
notification.error({
|
||||
title: `${'服务错误'}`,
|
||||
description: `${error?.message}, ${error?.response?.data?.message || ''}`,
|
||||
});
|
||||
}
|
||||
const msg = `接口地址:${url}\n响应信息:${error?.message}\n错误信息:${JSON.stringify(
|
||||
error?.response?.data || '',
|
||||
).replace(/"/g, '')}\n请求参数:${JSON.stringify(data || '').replace(/"/g, '')}`;
|
||||
dingRequest(msg || '错误');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 请求获取文件
|
||||
* @param url 文件地址
|
||||
* @param options 参数
|
||||
* @param options.oss 判断是不是 oss文件, 默认 true
|
||||
* @returns Promise<File>
|
||||
*/
|
||||
// export const requestFile = async (
|
||||
// url: string,
|
||||
// options: { oss?: boolean } = { oss: true },
|
||||
// ) => {
|
||||
// const u = url.startsWith("/") ? url.substring(1) : url;
|
||||
// return new Promise<Blob>((resolve, reject) => {
|
||||
// try {
|
||||
// fetch(options.oss ? `${OSSBaseUrl}${u}` : url, { mode: "cors" })
|
||||
// .then((response) => {
|
||||
// if (!response.ok) {
|
||||
// throw new Error(`HTTP 错误!状态:${response.status}`);
|
||||
// }
|
||||
// return response.blob();
|
||||
// })
|
||||
// .then((res) => {
|
||||
// // const fileName = url.split('/').pop() || '';
|
||||
// // resolve(blobToFile([res], fileName));
|
||||
// resolve(res);
|
||||
// });
|
||||
// } catch (error) {
|
||||
// reject(error);
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
23
src/vite-env.d.ts
vendored
Normal file
23
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare interface Window {
|
||||
dfConfig: {
|
||||
isPhone: boolean;
|
||||
/** 表格设置粘性头部 */
|
||||
tableStickyOffsetHeader: number;
|
||||
/** 语言 */
|
||||
language: "zh-cn" | "en";
|
||||
/** 视口高度 */
|
||||
vhUnit: "vh" | "dvh";
|
||||
};
|
||||
showProcessGraphId?: number;
|
||||
debug: {
|
||||
add: (data: { [key: string]: any }) => void;
|
||||
};
|
||||
dfERPConfig: {
|
||||
/** 价格小数点保留位数 */
|
||||
PRICE_HOLD_POINT: number;
|
||||
/** 数量小数点保留位数 */
|
||||
NUMS_HOLD_POINT: number;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user