6A!Ko^Z`Ch=|>k1`@`4qTs+stQT^aTs0&kO6-^#ssF5wE
zy94G--$kJeaJ82ZfB=BHc{@@n3O@#f%_jaPI3K8cxP9X#nCVShJx}C*EGKS;=pJr3
zvv-|+h<>ed^_Yu`>fJ$zP=@Un=8U!7<><|Ey^6J{uCunjk$^cmaB<6wD;5wF5dX)I
z9T5^-Vsj&+8W6@835W?nk>IG8RZmQ6-?wQt^}n>Sm7nTzNIcZV9cHYl3TlXJtY;a1G4<)RzZXV!
KoDuMQ?B4+HAflH5
literal 0
HcmV?d00001
diff --git a/src/assets/react.svg b/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/Footer/MonitorUpdate.tsx b/src/components/Footer/MonitorUpdate.tsx
new file mode 100644
index 0000000..5577351
--- /dev/null
+++ b/src/components/Footer/MonitorUpdate.tsx
@@ -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(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: (
+
+
发现系统版本更新,请刷新页面
+
+ 刷新前,请先保存页面数据!!!
+
+
+
+
+
+
+ ),
+ duration: 0,
+ });
+ }
+ });
+ }, 60000);
+
+ return () => {
+ clearInterval(timerRef.current);
+ };
+ }, []);
+
+ return null;
+};
+export default MonitorUpdate;
diff --git a/src/components/Footer/index.module.css b/src/components/Footer/index.module.css
new file mode 100644
index 0000000..98e48ec
--- /dev/null
+++ b/src/components/Footer/index.module.css
@@ -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;
+}
diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx
new file mode 100644
index 0000000..b47364d
--- /dev/null
+++ b/src/components/Footer/index.tsx
@@ -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 = (props) => {
+ const currentYear = new Date().getFullYear();
+
+ return (
+
+ );
+};
+
+export default Footer;
diff --git a/src/components/FormPlugin/index.module.css b/src/components/FormPlugin/index.module.css
new file mode 100644
index 0000000..bfef852
--- /dev/null
+++ b/src/components/FormPlugin/index.module.css
@@ -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%;
+}
diff --git a/src/components/FormPlugin/index.tsx b/src/components/FormPlugin/index.tsx
new file mode 100644
index 0000000..3487f9c
--- /dev/null
+++ b/src/components/FormPlugin/index.tsx
@@ -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> = (props) => {
+ const { style, children, labelWidth, gutter = 0, itemMarginBottom } = props;
+
+ return (
+
+ {children}
+
+ );
+};
+
+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> = (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 (
+
+ {label ? (
+
+ ) : null}
+
+ {children}
+
+ {props.errorMsg}
+
+
+
+ );
+};
diff --git a/src/components/GapBox.tsx b/src/components/GapBox.tsx
new file mode 100644
index 0000000..625c88d
--- /dev/null
+++ b/src/components/GapBox.tsx
@@ -0,0 +1,28 @@
+import type React from 'react';
+import type { PropsWithChildren } from 'react';
+
+type IProps = {
+ style?: React.CSSProperties;
+ gap?: number; // 间距
+ onClick?: React.MouseEventHandler;
+ title?: string;
+};
+
+export const GapBox: React.FC> = ({ children, onClick, style, title, gap }) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/Header/HeaderUserInfo.tsx b/src/components/Header/HeaderUserInfo.tsx
new file mode 100644
index 0000000..c2ab48f
--- /dev/null
+++ b/src/components/Header/HeaderUserInfo.tsx
@@ -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 (
+
+ {userInfo.login_name}
+ {
+ location.hash = '#/login';
+ }}
+ >
+
+
+
+ );
+};
diff --git a/src/components/ModalPlugin.tsx b/src/components/ModalPlugin.tsx
new file mode 100644
index 0000000..74c1b30
--- /dev/null
+++ b/src/components/ModalPlugin.tsx
@@ -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 = (props) => {
+ const { modal } = App.useApp();
+ return (
+ {
+ 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}
+
+ );
+};
+export default ModalPlugin;
diff --git a/src/components/PageContainer/BreadcrumbPlugin.tsx b/src/components/PageContainer/BreadcrumbPlugin.tsx
new file mode 100644
index 0000000..992e5b8
--- /dev/null
+++ b/src/components/PageContainer/BreadcrumbPlugin.tsx
@@ -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 = (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(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) ? (
+
+ ({ title: item }))}
+ style={{ minHeight: 22, ...props.breadcrumbStyle }}
+ />
+ {/* {!!src && (
+
+ } />
+
+ )} */}
+ {/* {likeMenus.includes(lo.pathname) ? (
+
+ {
+ save('remove');
+ }}
+ />
+
+ ) : (
+
+ {
+ save('add');
+ }}
+ />
+
+ )} */}
+
+ ) : null;
+};
+export default BreadcrumbPlugin;
diff --git a/src/components/PageContainer/PageContainerPlugin.tsx b/src/components/PageContainer/PageContainerPlugin.tsx
new file mode 100644
index 0000000..cea06ea
--- /dev/null
+++ b/src/components/PageContainer/PageContainerPlugin.tsx
@@ -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> = (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 (
+
+
+ {/* {props.children} */}
+ {!authStr || authStr.split(',').some((key) => auth?.[key.trim()]) ? (
+ props.children
+ ) : (
+
+ {'抱歉!您当前没有权限访问该页面'}
+ {'如果您是主账号,请购买相应的增值包'}
+ {'如果您是子账号,请联系管理员进行相应的授权'}
+
+ }
+ extra={
+
+ }
+ />
+ )}
+
+ );
+};
+
+export default PageContainerPlugin;
diff --git a/src/components/PaginationPlugin.tsx b/src/components/PaginationPlugin.tsx
new file mode 100644
index 0000000..353cc06
--- /dev/null
+++ b/src/components/PaginationPlugin.tsx
@@ -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 = (props) => {
+ return (
+ {
+ 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 = (props) => {
+ return (
+ {
+ props.onChange(pageSize != props.pageSize ? 1 : page, pageSize);
+ }}
+ // onShowSizeChange={(_current, size) => {
+ // props.onShowSizeChange(size);
+ // }}
+ // size={!isPhone ? 'small' : 'default'}
+ showTotal={(total) => `共 ${total} 条`}
+ />
+ );
+};
diff --git a/src/components/SearchButton.tsx b/src/components/SearchButton.tsx
new file mode 100644
index 0000000..cc5a9bb
--- /dev/null
+++ b/src/components/SearchButton.tsx
@@ -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 = (props) => {
+ const title = '搜索' as string;
+ return (
+
+ );
+};
+
+export const ResetButton: React.FC = (props) => {
+ const title = '重置' as string;
+ return (
+
+ );
+};
+
+export const MoreSearchButton: React.FC = (props) => {
+ const title = (props.show ? '收起' : '展开') as string;
+ return (
+
+ );
+};
+
+interface ISearchInput {
+ placeholder?: string;
+ value?: string;
+ onEnd?: (newValue?: string, oldValue?: string) => void;
+ onPressEnter?: (newValue?: string, oldValue?: string) => void;
+}
+
+export const SearchInputPlugin: React.FC = (props) => {
+ const { placeholder, value, onPressEnter, onEnd } = props;
+ const [val, setVal] = useState();
+
+ useEffect(() => {
+ setVal(value);
+ }, [value]);
+
+ return (
+ {
+ setVal(v.target.value);
+ }}
+ // onClear={() => {
+ // onEnd?.('', value);
+ // }}
+ onBlur={() => {
+ onEnd?.(val, value);
+ }}
+ onPressEnter={() => {
+ onEnd?.(val, value);
+ onPressEnter?.(val, value);
+ }}
+ />
+ );
+};
diff --git a/src/components/SiderMenu/NavMenu.tsx b/src/components/SiderMenu/NavMenu.tsx
new file mode 100644
index 0000000..5c66a74
--- /dev/null
+++ b/src/components/SiderMenu/NavMenu.tsx
@@ -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 = (props) => {
+ const [openKeys, setOpenKeys] = useState([]);
+ const auth = useAuthStore().auth;
+ const company = useCompanyStore().company;
+ // 新窗口打开的链接
+ const [newWindowUrl] = useState<{ [key: string]: { target: string } }>({});
+ const [hash, setHash] = useState(getHash());
+ type MenuItem = Required['items'][number];
+ /** 导航菜单 数据处理 */
+ const [menuOptions, setMenuOptions] = useState
+
+
+ {urlList.map((el) => (
+ {
+ 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' : ''
+ }`}
+ >
+
+ {el.title}
+
+ {
+ 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;
+ }
+ }
+ }}
+ />
+
+ ))}
+
+
+ >
+ )}
+ >
+ );
+};
+
+export default TabNavPlugin;
diff --git a/src/components/TableColumnsFilterPlugin.tsx b/src/components/TableColumnsFilterPlugin.tsx
new file mode 100644
index 0000000..2090e9f
--- /dev/null
+++ b/src/components/TableColumnsFilterPlugin.tsx
@@ -0,0 +1,87 @@
+import type { ColumnType } from 'antd/es/table';
+import { isArray, jsonParse } from '@/utils/common';
+
+/** 表格数据类型扩展 ColumnsType */
+export type ColumnsTypeUltra = (ColumnType & {
+ /** 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;
+// /** 存储的key */
+// storageKey: string;
+// /** 事件 */
+// onChange?: () => void;
+// placement?: TooltipPlacement;
+// };
+
+// /** 表格栏筛选组件
+// *
+// * @param tableColumns 表格列数据
+// * @param storageKey 存储的key
+// * @param onChange 事件
+// */
+// export const TableColumnsFilter: React.FC = (props) => {
+// const [hideColumns, setHideColumns] = useState([]);
+// useEffect(() => {
+// setHideColumns(TableColumnsStorage(props.storageKey));
+// }, []);
+// return (
+//
+// {('配置列')}
+// {props.tableColumns.map((item: any) => {
+// if (item.unset || typeof item.title != 'string') return null;
+// return (
+// {
+// 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}
+//
+// );
+// })}
+//
+// }
+// trigger={['click']}
+// placement={props.placement || 'bottomRight'}>
+// }
+// />
+//
+// );
+// };
diff --git a/src/components/TableColumnsFilterPlugin2.tsx b/src/components/TableColumnsFilterPlugin2.tsx
new file mode 100644
index 0000000..3fc4f62
--- /dev/null
+++ b/src/components/TableColumnsFilterPlugin2.tsx
@@ -0,0 +1,332 @@
+import type { ColumnType } from 'antd/es/table';
+
+// interface RowProps extends React.HTMLAttributes {
+// 'data-row-key': string;
+// }
+
+/** 表格数据类型扩展 ColumnsType */
+export type ColumnsTypeUltra2 = (ColumnType & {
+ /** 前端排序拖拽唯一columnName, 展示列的名称 */
+ columnName: string;
+ fixed?: 'left' | 'right';
+})[];
+
+// type IProps = {
+// /** 表格列数据 */
+// tableColumns: ColumnsTypeUltra2;
+// usersConfigKey: string;
+// placement?: TooltipPlacement;
+// };
+
+/** 表格栏筛选组件 */
+// export const TableColumnsFilter2: React.FC = (props) => {
+// const [dataSource, setDataSource] = useState([]);
+// 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 (
+//
+// {React.Children.map(children, (child) => {
+// if ((child as React.ReactElement).key === 'sort') {
+// return React.cloneElement(child as React.ReactElement, {
+// children: (
+//
+//
+// }
+// {...listeners}
+// />
+//
+// ),
+// });
+// }
+// return child;
+// })}
+//
+// );
+// };
+
+// 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 = [
+// { title: '排序', key: 'sort', width: 48 },
+// {
+// title: '显示列',
+// width: 64,
+// align: 'center',
+// dataIndex: 'show',
+// render(value, record) {
+// return (
+// {
+// record.show = e;
+// }}
+// />
+// );
+// },
+// },
+// { title: '列名', dataIndex: 'columnName', width: 140 },
+// {
+// title: (
+//
+// 宽度
+// 1个字约14px
+//
+// ),
+// dataIndex: 'width',
+// width: 140,
+// render(val, record) {
+// return (
+// {
+// record.width = val;
+// }}
+// onBlur={() => {
+// setTimeout(() => {
+// if (!record.width) {
+// record.width = 44;
+// setDataSource([...dataSource]);
+// }
+// }, 0);
+// }}
+// addonAfter='px'
+// />
+// );
+// },
+// },
+// {
+// title: (
+//
+// 固定列
+// 请勿在中间列锁列, 会导致表格错位
+//
+// ),
+// dataIndex: 'fixed',
+// width: 290,
+// render(val, record) {
+// return (
+// {
+// record.fixed = e.target.value;
+// }}
+// defaultValue={val || ''}
+// key={val}>
+// 无
+// 左边
+// 右边
+//
+// );
+// },
+// },
+// ];
+
+// 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 (
+// <>
+// }>
+// {!!usersConfigStore.config![props.usersConfigKey]?.config_value && (
+//
+// )}
+//
+//
+// 表格列配置
+//
+//
+//
+// }
+// onClose={() => {
+// setOpen(false);
+// }}>
+//
+// el.columnName)} strategy={verticalListSortingStrategy}>
+//
+//
+//
+//
+// >
+// );
+// };
diff --git a/src/components/TablePlugin.tsx b/src/components/TablePlugin.tsx
new file mode 100644
index 0000000..97bf2c9
--- /dev/null
+++ b/src/components/TablePlugin.tsx
@@ -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;
+/** Table简单封装
+ *
+ * @props size="small"
+ * @props tableLayout="fixed"
+ * @props showSorterTooltip={false}
+ * @props pagination={false}
+ * @props bordered={true}
+ *
+ */
+export const TablePlugin: React.FC = (props) => {
+ return (
+ '暂无数据'}>
+
+
+ );
+};
diff --git a/src/configs/config.ts b/src/configs/config.ts
new file mode 100644
index 0000000..a26b2f5
--- /dev/null
+++ b/src/configs/config.ts
@@ -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 = {
+ /** 未审核 */
+ 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 = {
+ 0: { label: '未开始', color: '#0fc6c2' },
+ 1: { label: '进行中', color: '#7bc616' },
+ 2: { label: '已完成', color: '#86909c' },
+};
diff --git a/src/configs/menuConfig.tsx b/src/configs/menuConfig.tsx
new file mode 100644
index 0000000..1a66861
--- /dev/null
+++ b/src/configs/menuConfig.tsx
@@ -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: ,
+ children: [
+ { name: '用户管理', path: '/user/list', auth: '' },
+ // { name: '岗位角色', path: '/user/group', auth: 'SF_ERP_GROUP_VIEW' },
+ ],
+};
+const staffMenu: MenuDataItem = {
+ name: '后台用户',
+ icon: ,
+ 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: ,
+ children: [{ name: '系统主页', path: '/home/index', auth: '' }],
+ },
+ userMenu,
+ staffMenu,
+];
+
+export { asideMenuConfig };
diff --git a/src/configs/routes.ts b/src/configs/routes.ts
new file mode 100644
index 0000000..39f0d89
--- /dev/null
+++ b/src/configs/routes.ts
@@ -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: [] },
+];
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..758ed6f
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,10 @@
+body {
+ margin: 0;
+ min-height: 100vh;
+}
+
+@supports (height: 100dvh) {
+ body {
+ min-height: 100vh;
+ }
+}
diff --git a/src/interfaces/common.ts b/src/interfaces/common.ts
new file mode 100644
index 0000000..7587377
--- /dev/null
+++ b/src/interfaces/common.ts
@@ -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;
+};
+
+export type IOption = {
+ value: string | number;
+ label: string;
+};
diff --git a/src/interfaces/finance.ts b/src/interfaces/finance.ts
new file mode 100644
index 0000000..03f3654
--- /dev/null
+++ b/src/interfaces/finance.ts
@@ -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;
+};
diff --git a/src/interfaces/staff.ts b/src/interfaces/staff.ts
new file mode 100644
index 0000000..a399bd8
--- /dev/null
+++ b/src/interfaces/staff.ts
@@ -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;
+};
diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts
new file mode 100644
index 0000000..87a1e6e
--- /dev/null
+++ b/src/interfaces/user.ts
@@ -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;
+}
diff --git a/src/layouts/AppLayout.tsx b/src/layouts/AppLayout.tsx
new file mode 100644
index 0000000..ff96c70
--- /dev/null
+++ b/src/layouts/AppLayout.tsx
@@ -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 ? (
+
+
+
+ {isPhone ? (
+
+ ) : (
+ {DefaultERPName}
+ )}
+ {Date.now()}
+
+
+
+
+
+
+ {isPhone ? null : (
+ {
+ // 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}
+ >
+
+ {/* */}
+
+ )}
+
+
+ {/* */}
+
+
+
+
+
+
+
+
+
+ ) : (
+ 加载数据...}
+ >
+
+
+ )}
+ >
+ );
+};
+export default AppLayout;
diff --git a/src/layouts/EmptyLayout.tsx b/src/layouts/EmptyLayout.tsx
new file mode 100644
index 0000000..6983f33
--- /dev/null
+++ b/src/layouts/EmptyLayout.tsx
@@ -0,0 +1,11 @@
+import Outlet from '@/router/Outlet';
+import ErrorBoundary from './ErrorBoundary';
+
+const EmptyLayout = () => {
+ return (
+
+
+
+ );
+};
+export default EmptyLayout;
diff --git a/src/layouts/ErrorBoundary.tsx b/src/layouts/ErrorBoundary.tsx
new file mode 100644
index 0000000..d2daf65
--- /dev/null
+++ b/src/layouts/ErrorBoundary.tsx
@@ -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 {
+ 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) || (
+
+ 页面发生异常,错误内容已上报!
+ 开发人员紧急修复中,敬请耐心等待!
+
+ }
+ subTitle={
+
+ {this.state.errorMsg}
+
+ }
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ flexDirection: 'column',
+ minHeight: 'calc(100vh - 180px)',
+ }}
+ extra={[
+ ,
+ ]}
+ />
+ )
+ );
+ }
+
+ return this.props?.children;
+ }
+}
+
+export default ErrorBoundary;
diff --git a/src/layouts/base.ts b/src/layouts/base.ts
new file mode 100644
index 0000000..6638eed
--- /dev/null
+++ b/src/layouts/base.ts
@@ -0,0 +1,13 @@
+import { requestLite } from '@/utils/useRequest';
+
+export const loginState = async () => {
+ return new Promise((resolve, reject) => {
+ requestLite('/Users/loginState').then((res) => {
+ if (res?.err_code == 0) {
+ resolve(res);
+ } else {
+ reject(null);
+ }
+ });
+ });
+};
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..f329669
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,9 @@
+import { createRoot } from 'react-dom/client';
+import './index.css';
+import App from './App.tsx';
+
+createRoot(document.getElementById('root')!).render(
+ //
+ ,
+ // ,
+);
diff --git a/src/pages/Error/index.tsx b/src/pages/Error/index.tsx
new file mode 100644
index 0000000..fd3ea8d
--- /dev/null
+++ b/src/pages/Error/index.tsx
@@ -0,0 +1,26 @@
+import { Button, Result } from 'antd';
+
+const ErrorPage = () => {
+ document.title = '404 - 抱歉,您访问的页面不存在';
+ return (
+
+ {
+ location.href = '/';
+ }}
+ >
+ {'返回首页'}
+
+ }
+ />
+
+ );
+};
+
+export default ErrorPage;
diff --git a/src/pages/Index/index.tsx b/src/pages/Index/index.tsx
new file mode 100644
index 0000000..e0d2c80
--- /dev/null
+++ b/src/pages/Index/index.tsx
@@ -0,0 +1,34 @@
+const Index = () => {
+ return (
+
+ 开发中...
+ {/* Index */}
+ {/*
+
+
+
login */}
+ {/* {undefined.map()} */}
+
+ );
+};
+
+export default Index;
diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx
new file mode 100644
index 0000000..24a1a07
--- /dev/null
+++ b/src/pages/Login/index.tsx
@@ -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 (
+
+
+
{DefaultERPName}后台登录
+
+
+ {
+ userInfo.login_name = e.target.value;
+ setUserInfo({ ...userInfo });
+ }}
+ onPressEnter={login}
+ />
+
+
+ {
+ userInfo.password = e.target.value;
+ setUserInfo({ ...userInfo });
+ }}
+ onPressEnter={login}
+ />
+
+
+
+
+
+
+
+ );
+};
+
+export default Login;
diff --git a/src/pages/Staff/dep/index.tsx b/src/pages/Staff/dep/index.tsx
new file mode 100644
index 0000000..20418c2
--- /dev/null
+++ b/src/pages/Staff/dep/index.tsx
@@ -0,0 +1,17 @@
+import PageContainerPlugin from '@/components/PageContainer/PageContainerPlugin';
+
+const DepContent = () => {
+ return (
+
+ );
+};
+
+const Dep = () => (
+
+
+
+);
+
+export default Dep;
diff --git a/src/pages/Staff/group/index.tsx b/src/pages/Staff/group/index.tsx
new file mode 100644
index 0000000..bc86d32
--- /dev/null
+++ b/src/pages/Staff/group/index.tsx
@@ -0,0 +1,17 @@
+import PageContainerPlugin from '@/components/PageContainer/PageContainerPlugin';
+
+const GroupContent = () => {
+ return (
+
+ );
+};
+
+const Group = () => (
+
+
+
+);
+
+export default Group;
diff --git a/src/pages/User/List/components/UserEditModal.tsx b/src/pages/User/List/components/UserEditModal.tsx
new file mode 100644
index 0000000..1069174
--- /dev/null
+++ b/src/pages/User/List/components/UserEditModal.tsx
@@ -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 = (props) => {
+ console.log(props.ref);
+ const [open, setOpen] = useState(false);
+
+ useImperativeHandle(props.ref, () => ({
+ show: () => {
+ setOpen(true);
+ },
+ }));
+ return (
+ setOpen(false)}>
+ 11111
+
+ );
+};
diff --git a/src/pages/User/List/components/state.ts b/src/pages/User/List/components/state.ts
new file mode 100644
index 0000000..413fd64
--- /dev/null
+++ b/src/pages/User/List/components/state.ts
@@ -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;
+ },
+});
diff --git a/src/pages/User/List/index.tsx b/src/pages/User/List/index.tsx
new file mode 100644
index 0000000..f00ab1c
--- /dev/null
+++ b/src/pages/User/List/index.tsx
@@ -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 (
+ <>
+
+
+ {
+ userListStateProxy.page(1);
+ }}
+ onEnd={(v) => {
+ userListStateProxy.params.order_no = v;
+ }}
+ />
+
+
+ {
+ userListStateProxy.page(1);
+ }}
+ onEnd={(v) => {
+ userListStateProxy.params.custom_order_no = v;
+ }}
+ />
+
+
+ {
+ userListStateProxy.page(1);
+ }}
+ onEnd={(v) => {
+ userListStateProxy.params.custom_name = v;
+ }}
+ />
+
+
+
+
+ {
+ userListStateProxy.page(1);
+ }}
+ />
+ {
+ userListStateProxy.reset();
+ }}
+ />
+ {
+ userListStateProxy.showMoreSearch = !userListStateProxy.showMoreSearch;
+ }}
+ />
+
+
+
+
+
+ {
+ userListStateProxy.page(1);
+ }}
+ onEnd={(v) => {
+ userListStateProxy.params.custom_phone = v;
+ }}
+ />
+
+
+ >
+ );
+};
+
+const Content = () => {
+ const snap = useSnapshot(userListStateProxy);
+ const tableColumns: ColumnsTypeUltra2 = [
+ {
+ columnName: '操作',
+ title: '操作',
+ fixed: tableFixedByPhone('left'),
+ width: 52,
+ render: () => {
+ return {/* */};
+ },
+ },
+ { columnName: '创建时间', width: 150, title: '创建时间', dataIndex: 'create_date', sorter: true },
+ { columnName: '备注', title: '备注', dataIndex: 'comments', width: 180 },
+ { columnName: '' },
+ ];
+
+ const UserEditModalRef = useRef(null);
+
+ return (
+ <>
+
+
+
+
+ {
+ userListStateProxy.page(page);
+ }}
+ />
+
+ {
+ userListStateProxy.params.order = formatTableSort(sort);
+ userListStateProxy.page(1);
+ }}
+ dataSource={snap.ajaxData}
+ rowKey={'user_id'}
+ columns={tableColumns}
+ />
+ {
+ userListStateProxy.page(page, pageSize);
+ }}
+ />
+
+
+ >
+ );
+};
+
+const UserList = () => (
+
+
+
+
+);
+
+export default UserList;
diff --git a/src/router/Link.tsx b/src/router/Link.tsx
new file mode 100644
index 0000000..59865c4
--- /dev/null
+++ b/src/router/Link.tsx
@@ -0,0 +1,8 @@
+import type React from "react";
+import { urlFormat } from "./routerUtils";
+
+const Link: React.FC<{ href: string; children: React.ReactNode }> = (props) => {
+ return {props.children};
+};
+
+export default Link;
diff --git a/src/router/Outlet.tsx b/src/router/Outlet.tsx
new file mode 100644
index 0000000..000a089
--- /dev/null
+++ b/src/router/Outlet.tsx
@@ -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(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 (
+
+ {OutLetRef.current ? (
+ OutLetRef.current?.$$typeof == Symbol.for('react.lazy') ? (
+
+ }
+ >
+
+
+ ) : (
+
+ )
+ ) : null}
+
+ );
+};
+
+export default Outlet;
diff --git a/src/router/Router.tsx b/src/router/Router.tsx
new file mode 100644
index 0000000..a4f3ecc
--- /dev/null
+++ b/src/router/Router.tsx
@@ -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(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 {LayoutRef.current ? : null};
+};
+
+export default Router;
diff --git a/src/router/router-data.ts b/src/router/router-data.ts
new file mode 100644
index 0000000..4ff5e14
--- /dev/null
+++ b/src/router/router-data.ts
@@ -0,0 +1,9 @@
+interface IRouterData {
+ mode: 'hash' | 'history';
+ routers: Map;
+}
+
+export const routerData: IRouterData = {
+ mode: 'hash',
+ routers: new Map(),
+};
diff --git a/src/router/routerUtils.ts b/src/router/routerUtils.ts
new file mode 100644
index 0000000..f7055e0
--- /dev/null
+++ b/src/router/routerUtils.ts
@@ -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;
+};
diff --git a/src/router/types.ts b/src/router/types.ts
new file mode 100644
index 0000000..15a3728
--- /dev/null
+++ b/src/router/types.ts
@@ -0,0 +1,12 @@
+export type IRouteItemChild = {
+ path: string;
+ Component: any;
+ title?: string;
+};
+
+export type IRouteItem = {
+ path: string;
+ Layout: any;
+ title?: string;
+ children: IRouteItemChild[];
+};
diff --git a/src/services/UserServices.ts b/src/services/UserServices.ts
new file mode 100644
index 0000000..8e5c2dc
--- /dev/null
+++ b/src/services/UserServices.ts
@@ -0,0 +1,3 @@
+export const UserServices = {
+ list: '/UserServices/list',
+} as const;
diff --git a/src/store/AuthStore.ts b/src/store/AuthStore.ts
new file mode 100644
index 0000000..49d78f1
--- /dev/null
+++ b/src/store/AuthStore.ts
@@ -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> = create((set) => ({
+ auth: {},
+ updateAuth: (data) => set(() => ({ auth: data })),
+ authByPathname: {},
+ initAuthByPathname: (data: IAuthByPathname) => set(() => ({ authByPathname: data })),
+}));
diff --git a/src/store/CompanyStore.ts b/src/store/CompanyStore.ts
new file mode 100644
index 0000000..c2e0abb
--- /dev/null
+++ b/src/store/CompanyStore.ts
@@ -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> = create(
+ (set) => ({
+ company: {},
+ updateCompany: (data) => set(() => ({ company: data })),
+ }),
+);
diff --git a/src/store/ConfigStore.ts b/src/store/ConfigStore.ts
new file mode 100644
index 0000000..4e8f546
--- /dev/null
+++ b/src/store/ConfigStore.ts
@@ -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> = create((set) => ({
+ config: {},
+ updateConfig: (data) => set(() => ({ config: data })),
+}));
diff --git a/src/store/RefreshStore.ts b/src/store/RefreshStore.ts
new file mode 100644
index 0000000..e5d1df5
--- /dev/null
+++ b/src/store/RefreshStore.ts
@@ -0,0 +1,11 @@
+import { create, type StoreApi, type UseBoundStore } from 'zustand';
+
+type IUser = {
+ refresh: number;
+ updateRefresh: (data: number) => void;
+};
+
+export const useRefreshStore: UseBoundStore> = create((set) => ({
+ refresh: 1,
+ updateRefresh: (data) => set(() => ({ refresh: data })),
+}));
diff --git a/src/store/UserConfigStore.ts b/src/store/UserConfigStore.ts
new file mode 100644
index 0000000..7694ee5
--- /dev/null
+++ b/src/store/UserConfigStore.ts
@@ -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> = create((set) => ({
+// config: {},
+// updateConfig: (data) => set(() => ({ config: data })),
+// }));
diff --git a/src/store/UserStore.ts b/src/store/UserStore.ts
new file mode 100644
index 0000000..90cc037
--- /dev/null
+++ b/src/store/UserStore.ts
@@ -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> = create((set) => ({
+ user: {},
+ updateUser: (data) => set(() => ({ user: data })),
+}));
diff --git a/src/store/indexDBStore.ts b/src/store/indexDBStore.ts
new file mode 100644
index 0000000..71ef91f
--- /dev/null
+++ b/src/store/indexDBStore.ts
@@ -0,0 +1,11 @@
+import { create, type StoreApi, type UseBoundStore } from 'zustand';
+
+type IIndexDB = {
+ db: any;
+ init: (data: any) => void;
+};
+
+export const useIndexDBStore: UseBoundStore> = create((set) => ({
+ db: {},
+ init: (data) => set(() => ({ db: data })),
+}));
diff --git a/src/store/type.ts b/src/store/type.ts
new file mode 100644
index 0000000..d52e02f
--- /dev/null
+++ b/src/store/type.ts
@@ -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;
+};
diff --git a/src/utils/EventBus.ts b/src/utils/EventBus.ts
new file mode 100644
index 0000000..bc58faf
--- /dev/null
+++ b/src/utils/EventBus.ts
@@ -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;
+ },
+};
diff --git a/src/utils/UniqueKey.ts b/src/utils/UniqueKey.ts
new file mode 100644
index 0000000..b881bb8
--- /dev/null
+++ b/src/utils/UniqueKey.ts
@@ -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++;
diff --git a/src/utils/common.ts b/src/utils/common.ts
new file mode 100644
index 0000000..d6d5238
--- /dev/null
+++ b/src/utils/common.ts
@@ -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:
+ // biome-ignore lint/suspicious/noGlobalIsFinite:
+ 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}`,
+ });
+}
diff --git a/src/utils/commonUtils.ts b/src/utils/commonUtils.ts
new file mode 100644
index 0000000..f8428fd
--- /dev/null
+++ b/src/utils/commonUtils.ts
@@ -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 => {
+ 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;
+// 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) => Object.fromEntries(data);
+
+/** Map数据转对象json */
+export const MapToJson = (data: Map) => JSON.stringify(MapToObject(data));
+
+/** 深拷贝 Map 对象(支持基本类型和引用类型只支持(一层)) */
+export const deepCopyMap = (data: Map) => {
+ 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) => [...data];
+
+/** Set数据转数组json */
+export const SetToJson = (data: Set) => 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);
+};
diff --git a/src/utils/copyToClipboard.ts b/src/utils/copyToClipboard.ts
new file mode 100644
index 0000000..89d8d53
--- /dev/null
+++ b/src/utils/copyToClipboard.ts
@@ -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((resolve, reject) => {
+ // 执行复制命令并移除文本框
+ document.execCommand('copy') ? resolve() : reject();
+ textArea.remove();
+ });
+ }
+};
diff --git a/src/utils/http.ts b/src/utils/http.ts
new file mode 100644
index 0000000..b7f58ee
--- /dev/null
+++ b/src/utils/http.ts
@@ -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,
+};
diff --git a/src/utils/type.ts b/src/utils/type.ts
new file mode 100644
index 0000000..7587377
--- /dev/null
+++ b/src/utils/type.ts
@@ -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;
+};
+
+export type IOption = {
+ value: string | number;
+ label: string;
+};
diff --git a/src/utils/update.js b/src/utils/update.js
new file mode 100644
index 0000000..16bbc7d
--- /dev/null
+++ b/src/utils/update.js
@@ -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("版本写入成功");
+ }
+ );
+});
diff --git a/src/utils/useRequest.ts b/src/utils/useRequest.ts
new file mode 100644
index 0000000..e4e6c24
--- /dev/null
+++ b/src/utils/useRequest.ts
@@ -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(null);
+ // 请求返回的错误信息
+ const [error, setError] = useState(null);
+ // 请求的loading 状态
+ const [loading, setLoading] = useState(false);
+ const [count, setCount] = useState(0);
+
+ // const cancelRef = useRef(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
+ * */
+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
+ */
+// export const requestFile = async (
+// url: string,
+// options: { oss?: boolean } = { oss: true },
+// ) => {
+// const u = url.startsWith("/") ? url.substring(1) : url;
+// return new Promise((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);
+// }
+// });
+// };
diff --git a/src/utils/useRequest2.ts b/src/utils/useRequest2.ts
new file mode 100644
index 0000000..7601dae
--- /dev/null
+++ b/src/utils/useRequest2.ts
@@ -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
+ * */
+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
+ */
+// export const requestFile = async (
+// url: string,
+// options: { oss?: boolean } = { oss: true },
+// ) => {
+// const u = url.startsWith("/") ? url.substring(1) : url;
+// return new Promise((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);
+// }
+// });
+// };
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..eeeb99f
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1,23 @@
+///
+
+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;
+ };
+}
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 0000000..c3e704f
--- /dev/null
+++ b/tsconfig.app.json
@@ -0,0 +1,33 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true,
+
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ }
+ },
+ "include": ["src"]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..8a67f62
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..6b3413b
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,37 @@
+import path from 'node:path';
+import react from '@vitejs/plugin-react-swc';
+// import { visualizer } from 'rollup-plugin-visualizer';
+import { defineConfig } from 'vite';
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [
+ react(), // 打包后生成体积分析报告(dist/stats.html)
+ // visualizer({
+ // // open: true, // 自动打开报告页面
+ // filename: 'stats.html',
+ // }),
+ ],
+ base: './',
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+ server: {
+ port: 4050,
+ host: '0.0.0.0',
+ proxy: {
+ '/api': {
+ target: 'http://192.168.1.138:93',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, ''), // 不可以省略rewrite
+ },
+ '/static': {
+ target: 'http://192.168.1.138:83',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, ''), // 不可以省略rewrite
+ },
+ },
+ },
+});