新项目, antd6, react19
This commit is contained in:
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user