feat:初始化
All checks were successful
Build and Push Docker Image / build (push) Successful in 5m10s

This commit is contained in:
Your Name 2026-01-08 16:35:06 +08:00
parent ab613c0559
commit 9a2e1afe56
683 changed files with 345530 additions and 0 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
TOKEN_NAME=ADMIN_TOKEN
GUARD_NAME=Admin

3
.eslintrc.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
extends: require.resolve('@umijs/max/eslint'),
};

75
.github/workflows/develop.yml vendored Normal file
View File

@ -0,0 +1,75 @@
name: Build and Push Docker Image
on:
push:
branches:
- develop
env:
IMAGE_NAME: ''
REPO_NAME: ''
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Setup Environments
shell: bash
run: |
IFS="/" read -r OWNER REPO <<< "$GITHUB_REPOSITORY"
echo "REPO_NAME=$REPO" >> $GITHUB_ENV
echo "IMAGE_NAME=${{ vars.REGISTRY_URL }}/${{ vars.REGISTRY_NAMESPACE }}/$REPO:$GITHUB_REF_NAME" >> $GITHUB_ENV
- name: Checkout
uses: https://gitee.com/zuowenbo/checkout@v4.2.2
- name: Cache Docker layers
uses: https://gitee.com/zuowenbo/cache@v4.2.1
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ env.REPO_NAME }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-${{ env.REPO_NAME }}-
- name: Set up Docker Buildx
uses: https://gitee.com/zuowenbo/setup-buildx-action@v3.9.0
with:
driver-opts: image=crpi-an5dzzjadzt6601u.cn-shenzhen.personal.cr.aliyuncs.com/softnook-proxy/buildkit:buildx-stable-1
- name: Login to Docker Hub
uses: https://gitee.com/zuowenbo/login-action@v3.3.0
with:
registry: ${{ vars.REGISTRY_URL }}
username: ${{ vars.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push final image
uses: https://gitee.com/zuowenbo/build-push-action@v6.14.0
with:
context: .
tags: ${{ env.IMAGE_NAME }}
push: true
platforms: linux/amd64
build-args: BRANCH=${{ env.GITHUB_REF_NAME }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
- name: Deploy to remote
uses: https://gitee.com/zuowenbo/ssh-action@v1.0.3
with:
host: ${{ vars.DEV_HOST_IP }}
port: 22
username: 'root'
password: ${{ secrets.DEV_HOST_PASSWORD }}
script: |
docker login -u '${{ vars.REGISTRY_USERNAME }}' -p '${{ secrets.REGISTRY_PASSWORD }}' ${{ vars.REGISTRY_URL}}
docker volume create ${{ env.REPO_NAME }} || true
docker pull ${{ env.IMAGE_NAME }}
docker stop ${{ env.REPO_NAME }} || true
docker rm ${{ env.REPO_NAME }} || true
docker run -d --restart always --network my_network --name ${{ env.REPO_NAME }} ${{ env.IMAGE_NAME }}

75
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,75 @@
name: Build and Push Docker Image
on:
push:
branches:
- main
env:
IMAGE_NAME: ''
REPO_NAME: ''
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Setup Environments
shell: bash
run: |
IFS="/" read -r OWNER REPO <<< "$GITHUB_REPOSITORY"
echo "REPO_NAME=$REPO" >> $GITHUB_ENV
echo "IMAGE_NAME=${{ vars.REGISTRY_URL }}/${{ vars.REGISTRY_NAMESPACE }}/$REPO:$GITHUB_REF_NAME" >> $GITHUB_ENV
- name: Checkout
uses: https://gitee.com/zuowenbo/checkout@v4.2.2
- name: Cache Docker layers
uses: https://gitee.com/zuowenbo/cache@v4.2.1
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ env.REPO_NAME }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-${{ env.REPO_NAME }}-
- name: Set up Docker Buildx
uses: https://gitee.com/zuowenbo/setup-buildx-action@v3.9.0
with:
driver-opts: image=crpi-an5dzzjadzt6601u.cn-shenzhen.personal.cr.aliyuncs.com/softnook-proxy/buildkit:buildx-stable-1
- name: Login to Docker Hub
uses: https://gitee.com/zuowenbo/login-action@v3.3.0
with:
registry: ${{ vars.REGISTRY_URL }}
username: ${{ vars.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push final image
uses: https://gitee.com/zuowenbo/build-push-action@v6.14.0
with:
context: .
tags: ${{ env.IMAGE_NAME }}
push: true
platforms: linux/amd64
build-args: BRANCH=${{ env.GITHUB_REF_NAME }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
- name: Deploy to remote
uses: https://gitee.com/zuowenbo/ssh-action@v1.0.3
with:
host: ${{ vars.MAIN_HOST_IP }}
port: 22
username: 'root'
password: ${{ secrets.MAIN_HOST_PASSWORD }}
script: |
docker login -u ${{ vars.REGISTRY_USERNAME }} -p ${{ secrets.REGISTRY_PASSWORD }} ${{ vars.REGISTRY_URL}}
docker volume create ${{ env.REPO_NAME }} || true
docker pull ${{ env.IMAGE_NAME }}
docker stop ${{ env.REPO_NAME }} || true
docker rm ${{ env.REPO_NAME }} || true
docker run -d --restart always --network my_network --name ${{ env.REPO_NAME }} ${{ env.IMAGE_NAME }}

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
/node_modules
/.env.local
/.umirc.local.ts
/config/config.local.ts
/src/.umi
/src/.umi-production
/src/.umi-test
/.umi
/.umi-production
/.umi-test
/dist
/.mfsu
.swc
/.idea

12
.lintstagedrc Normal file
View File

@ -0,0 +1,12 @@
{
"*.{md,json}": ["prettier --cache --write"],
"*.{js,jsx}": ["max lint --fix --eslint-only", "prettier --cache --write"],
"*.{css,less}": [
"max lint --fix --stylelint-only",
"prettier --cache --write"
],
"*.ts?(x)": [
"max lint --fix --eslint-only",
"prettier --cache --parser=typescript --write"
]
}

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
registry=https://registry.npmjs.com/

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
.umi
.umi-production

8
.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"printWidth": 80,
"singleQuote": true,
"trailingComma": "all",
"proseWrap": "never",
"overrides": [{ "files": ".prettierrc", "options": { "parser": "json" } }],
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-packagejson"]
}

3
.stylelintrc.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
extends: require.resolve('@umijs/max/stylelint'),
};

27
.umirc.ts Normal file
View File

@ -0,0 +1,27 @@
import { defineConfig } from '@umijs/max';
export default defineConfig({
antd: {},
access: {},
model: {},
initialState: {},
request: {},
layout: {},
//ai 加的 mfsu
mfsu: false,
favicons: ['/favicon.ico'],
npmClient: 'npm',
define: {
'process.env.TOKEN_NAME': process.env.TOKEN_NAME,
'process.env.GUARD_NAME': process.env.GUARD_NAME,
},
proxy: {
'/api/': {
target: 'http://10.39.13.78:8002/',
// target: 'https://gcadmin-test.linyikj.com.cn',
// target: 'http://guocaiservice.com',
// changeOrigin: true,
pathRewrite: { '^': '' },
},
},
});

1
.vercel/project.json Normal file
View File

@ -0,0 +1 @@
{ "projectName": "trae_pay-admin_tj5u" }

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}

15
Dockerfile Normal file
View File

@ -0,0 +1,15 @@
# 构建阶段
FROM registry.cn-shenzhen.aliyuncs.com/zuoge-proxy/node:20-alpine AS build
RUN npm install -g pnpm --registry=https://registry.npmmirror.com
WORKDIR /app
COPY package.json pnpm-lock.yaml* ./
RUN pnpm install --registry=https://registry.npmmirror.com
COPY . .
RUN pnpm build
# 部署阶段
FROM crpi-an5dzzjadzt6601u.cn-shenzhen.personal.cr.aliyuncs.com/softnook-proxy/nginx:1.27.3-alpine AS app
COPY --from=build /app/dist /usr/share/nginx/html
COPY docker/nginx.conf /etc/nginx/nginx.conf

56
docker/nginx.conf Executable file
View File

@ -0,0 +1,56 @@
# Generated by nginxconfig.io
# See nginxconfig.txt for the configuration share link
user nginx;
pid /var/run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 65535;
# Load modules
include /etc/nginx/modules-enabled/*.conf;
events {
multi_accept on;
worker_connections 65535;
}
http {
charset utf-8;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
log_not_found off;
types_hash_max_size 2048;
types_hash_bucket_size 64;
client_max_body_size 16M;
# MIME
include mime.types;
default_type application/octet-stream;
# Logging
access_log /dev/stdout;
error_log /dev/stderr warn;
# example.com
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
index index.html index.htm;
# index.html fallback
location / {
try_files $uri $uri/ /index.html;
}
# gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
}
}

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

8
gencode.json Normal file
View File

@ -0,0 +1,8 @@
{
"url": "http://10.39.13.78:8002/api/docs/openapi",
"module": "Admin",
"outPath": "./src/gen/",
"apis": {
"firstLine": "import { MyResponseType } from '@/common';\nimport { request } from '@umijs/max';"
}
}

22676
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

49
package.json Normal file
View File

@ -0,0 +1,49 @@
{
"private": true,
"author": "helloworld <hello@world.com>",
"scripts": {
"build": "max build",
"dev": "max dev",
"format": "prettier --cache --write .",
"gencode": "node ./node_modules/.bin/gencode-ts",
"postinstall": "max setup",
"setup": "max setup",
"start": "npm run dev"
},
"dependencies": {
"@ant-design/charts": "^2.6.6",
"@ant-design/icons": "^5.0.1",
"@ant-design/plots": "^2.6.6",
"@ant-design/pro-components": "^2.8.10",
"@antv/l7": "^2.23.0",
"@antv/l7-leaflet": "^1.0.2",
"@antv/l7-maps": "^2.23.0",
"@antv/l7plot": "^0.5.11",
"@umijs/max": "^4.3.10",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-react": "^1.0.6",
"antd": "^5.4.0",
"axios": "^1.7.2",
"dayjs": "^1.11.12",
"fast-deep-equal": "^3.1.3",
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"leaflet": "^1.9.4",
"lodash": "^4.17.21",
"react-activation": "^0.13.4",
"react-use": "^17.5.1",
"valtio": "^1.13.2"
},
"devDependencies": {
"@types/file-saver": "^2.0.7",
"@types/leaflet": "^1.9.21",
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"gencode-ts-cli": "^0.0.1",
"lint-staged": "^13.2.0",
"prettier": "^2.8.7",
"prettier-plugin-organize-imports": "^3.2.2",
"prettier-plugin-packagejson": "^2.4.3",
"typescript": "^5.0.3"
}
}

1
public/0C8IWjohWE.txt Normal file
View File

@ -0,0 +1 @@
c6c4bcb00af97510754b9d56def22047

View File

@ -0,0 +1 @@
2hQvP05DYNSk0Krf

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

10
src/access.ts Normal file
View File

@ -0,0 +1,10 @@
export default (initialState: API.UserInfo) => {
// 在这里按照初始化数据定义项目中的权限,统一管理
// 参考文档 https://umijs.org/docs/max/access
const canSeeAdmin = !!(
initialState && initialState.name !== 'dontHaveAccess'
);
return {
canSeeAdmin,
};
};

21
src/app.tsx Normal file
View File

@ -0,0 +1,21 @@
// 运行时配置
import React from 'react';
import {
LayoutConfig,
MyRootContainer,
requestConfig,
stateActions,
} from './common';
export const request = requestConfig;
export async function getInitialState(): Promise<any> {
return await stateActions.me();
}
export const layout = LayoutConfig;
export function rootContainer(container: React.ReactNode) {
return React.createElement(MyRootContainer, null, container);
}

0
src/assets/.gitkeep Normal file
View File

BIN
src/assets/bitcoin.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,14 @@
import { SlateDescendant } from '@wangeditor/editor';
declare module '@wangeditor/editor' {
// 扩展 Text
interface SlateText {
text: string;
}
// 扩展 Element
interface SlateElement {
type: string;
children: SlateDescendant[];
}
}

View File

@ -0,0 +1,113 @@
import { Apis } from '@/gen/Apis';
import { IDomEditor, IToolbarConfig } from '@wangeditor/editor';
import { Editor, Toolbar } from '@wangeditor/editor-for-react';
import '@wangeditor/editor/dist/css/style.css'; // 引入 css
import axios from 'axios';
import { useEffect, useState } from 'react';
type InsertFnType = (url: string) => void;
function MyEditor(props: any) {
// editor 实例
const [editor, setEditor] = useState<IDomEditor | null>(null); // TS 语法
// 编辑器内容
const [html, setHtml] = useState('');
useEffect(() => {
setTimeout(() => {
setHtml(props.value || '');
}, 500);
}, []);
// 工具栏配置
const toolbarConfig: Partial<IToolbarConfig> = {}; // TS 语法
// const toolbarConfig = { } // JS 语法
const customRequest = (
file: any,
onSuccess: (url: string, file: any) => void,
) => {
Apis.Common.Auth.PreUpload({
filename: file.name,
alc: 'public-read',
}).then(async (res: any) => {
axios
.put(res.data.url, file, {
headers: res.data.headers,
onUploadProgress: ({ total, loaded }) => {
console.log('loaded', total, loaded);
},
})
.then(({ data: response }) => {
console.log('response', response);
if (response.errorMessage) {
console.log(response, file);
} else {
onSuccess(res.data.url.split('?')[0], file);
}
});
});
return {
abort() {
console.log('upload progress is aborted.');
},
};
};
// 编辑器配置
const editorConfig: Partial<any> = {
// TS 语法
// const editorConfig = { // JS 语法
placeholder: '请输入内容...',
MENU_CONF: {
uploadVideo: {
async customUpload(file: File, insertFn: InsertFnType) {
// TS 语法
customRequest(file, (url) => {
console.log(url, '视频');
insertFn(url);
});
},
},
uploadImage: {
async customUpload(file: File, insertFn: InsertFnType) {
// TS 语法
customRequest(file, (url) => {
console.log(url, '图片');
insertFn(url);
});
},
},
},
};
// 及时销毁 editor ,重要!
useEffect(() => {
return () => {
if (editor === null) return;
editor.destroy();
setEditor(null);
};
}, [editor]);
return (
<div style={{ border: '1px solid #ccc', zIndex: 100 }} key="Editor">
<Toolbar
editor={editor}
defaultConfig={toolbarConfig}
mode="default"
key="Toolbar"
style={{ borderBottom: '1px solid #ccc' }}
/>
<Editor
defaultConfig={editorConfig}
value={html}
key="Editor2"
onCreated={setEditor}
onChange={(editor: any) => props?.onChange?.(editor.getHtml())}
mode="default"
style={{ height: '320px', overflowY: 'hidden' }}
/>
</div>
);
}
export default MyEditor;

View File

@ -0,0 +1,118 @@
import { PermissionsType, useMyState } from '@/common';
import { useLocation } from 'react-router-dom';
const loopMenu = (permissions: PermissionsType[] | undefined) => {
let tree: PermissionsType[] = [];
let map: Record<number, PermissionsType> = {};
permissions?.forEach((permission) => {
map[permission.id] = {
path: permission.type === 'Button' ? 'null' : permission.path,
name: permission.name,
label: permission.name,
key: permission.path || permission.id.toString(),
hideInMenu: permission.type === 'Button',
};
});
permissions?.forEach((permission) => {
let node = map[permission.id];
const parentId = permission?.parent_id;
if (parentId !== null && parentId !== undefined) {
const parentNode = map[parentId];
if (parentNode) {
// 初始化 children 如果不存在
if (!Array.isArray(parentNode.children)) {
parentNode.children = [];
}
parentNode.children.push(node);
} else {
// 父节点不存在,作为根节点处理
console.warn(
`Parent node with id ${parentId} not found for permission ${permission.id}`,
);
tree.push(node);
}
} else {
tree.push(node);
}
});
return tree?.[0]?.children;
};
// 递归查找当前路由对应的菜单项
const findMenuByPath = (menus: any[], currentPath: string): any => {
if (!menus || !Array.isArray(menus)) return null;
for (const menu of menus) {
// 跳过path为null或无效值的菜单
if (!menu.path || menu.path === 'null' || menu.path === 'undefined') {
// 继续检查子菜单
if (menu.children && menu.children.length > 0) {
const found = findMenuByPath(menu.children, currentPath);
if (found) return found;
}
continue;
}
// 精确匹配当前路由
if (menu.path === currentPath) {
return menu;
}
// 如果有子菜单,递归查找
if (menu.children && menu.children.length > 0) {
const found = findMenuByPath(menu.children, currentPath);
if (found) return found;
}
}
return null;
};
// 根据当前路由获取子菜单
const getSubMenusByCurrentPath = (menus: any[], currentPath: string) => {
if (!menus || !Array.isArray(menus) || !currentPath) {
return [];
}
const currentMenu = findMenuByPath(menus, currentPath);
// 如果找到当前菜单且有子菜单,返回子菜单
if (currentMenu && currentMenu.children && currentMenu.children.length > 0) {
return currentMenu.children;
}
return [];
};
// 自定义 Hook
export const useCurrentPermissions = () => {
const { snap } = useMyState();
const location = useLocation();
const getCurrentPermissions = (res: any, path?: string) => {
let objjs: any = [];
snap.session.permissions?.forEach((permission: any) => {
objjs.push(permission);
});
let data = objjs.sort((a: any, b: any) => {
return a._lft - b._lft;
});
const menus = loopMenu(data);
const currentSubMenus = getSubMenusByCurrentPath(
menus,
path || location.pathname,
);
// 直接过滤匹配的组件
const matchedValues = currentSubMenus
.filter((menu: any) => menu.key && res && res[menu.key])
.map((menu: any) => res[menu.key]);
return matchedValues;
};
return getCurrentPermissions;
};

View File

@ -0,0 +1,13 @@
import { Access, useAccess } from '@umijs/max';
import { ReactNode } from 'react';
export default function MyAccess({
children,
theKey,
}: {
children: ReactNode;
theKey: string;
}) {
const access = useAccess();
return <Access accessible={access.canAccess(theKey)}>{children}</Access>;
}

View File

@ -0,0 +1,147 @@
import {
EditOutlined,
PlusOutlined,
RollbackOutlined,
SaveFilled,
} from '@ant-design/icons';
import { useNavigate } from '@umijs/max';
import { Button, ButtonProps, Dropdown, Popconfirm } from 'antd';
import { MyResponseType } from '..';
type MyButtonsType = { title?: string; to?: string } & ButtonProps;
export const MyButtons = {
Create({ title, ...rest }: MyButtonsType): JSX.Element {
return (
<Button type="primary" icon={<PlusOutlined />} {...rest}>
{title}
</Button>
);
},
Default({
onConfirm,
isConfirm,
title,
description = '是否确定取消?',
...rest
}: {
onConfirm?: () => void;
isConfirm?: boolean;
description?: string;
} & MyButtonsType): JSX.Element {
return isConfirm ? (
<Popconfirm
title="提示"
description={description}
okText="是"
cancelText="否"
onConfirm={onConfirm}
>
<Button size="small" {...rest}>
{title}
</Button>
</Popconfirm>
) : (
<Button size="small" {...rest}>
{title}
</Button>
);
},
View({ title, ...rest }: MyButtonsType): JSX.Element {
const navigate = useNavigate();
return (
<Button
type="primary"
size="small"
onClick={() => {
if (rest?.to) {
navigate(rest?.to);
}
}}
{...rest}
>
{title ?? '查看'}
</Button>
);
},
Edit({ title = '编辑', ...rest }: MyButtonsType): JSX.Element {
return (
<Button type="primary" size="small" icon={<EditOutlined />} {...rest}>
{title}
</Button>
);
},
Save({ title = '保存', ...rest }: MyButtonsType): JSX.Element {
return (
<Button type="primary" icon={<SaveFilled />} {...rest}>
{title}
</Button>
);
},
Delete({
onConfirm,
title = '删除',
...rest
}: { onConfirm: () => void } & MyButtonsType): JSX.Element {
return (
<Popconfirm
title="删除提示"
description="确定删除?"
okText="是"
cancelText="否"
onConfirm={onConfirm}
>
<Button
type="primary"
size="small"
danger
// icon={<DeleteOutlined />}
{...rest}
>
{title}
</Button>
</Popconfirm>
);
},
Export({
api,
params,
title,
...rest
}: {
api: (data: any) => Promise<MyResponseType>;
params?: Record<string, any>;
} & MyButtonsType): JSX.Element {
return (
<Dropdown
menu={{
onClick: ({ item, key }: any) => {
console.log(item, key);
api?.({ ...params, ...{ download_type: key } });
},
items: [
// {
// key: 'page',
// label: '导出当前页',
// },
{
key: 'query',
label: '按条件导出',
},
// {
// key: 'all',
// label: '导出全部',
// },
],
}}
placement="bottomLeft"
arrow
>
<Button key="MyExportButton" icon={<RollbackOutlined />} {...rest}>
{title || '导出'}
</Button>
</Dropdown>
);
},
SoftDelete() {},
};

View File

@ -0,0 +1,391 @@
import {
ApiOutlined,
AreaChartOutlined,
AudioOutlined,
AuditOutlined,
BankOutlined,
BarChartOutlined,
BarcodeOutlined,
BellOutlined,
BookOutlined,
BugOutlined,
BuildOutlined,
BulbOutlined,
// 时间图标
CalendarOutlined,
CameraOutlined,
CarOutlined,
CheckOutlined,
ClockCircleOutlined,
CloseOutlined,
CloudOutlined,
ClusterOutlined,
CodeOutlined,
CompassOutlined,
ControlOutlined,
CopyOutlined,
CreditCardOutlined,
CrownOutlined,
DashboardOutlined,
DatabaseOutlined,
DeleteOutlined,
DesktopOutlined,
DisconnectOutlined,
// 财务图标
DollarOutlined,
// PrintOutlined, // 不存在,使用 PrinterOutlined 替代
DownloadOutlined,
DownOutlined,
EditOutlined,
// 地图图标
EnvironmentOutlined,
EuroOutlined,
ExclamationCircleOutlined,
ExperimentOutlined,
EyeInvisibleOutlined,
EyeOutlined,
FileExcelOutlined,
FileImageOutlined,
FileOutlined,
FilePdfOutlined,
FileTextOutlined,
FileWordOutlined,
// 其他常用图标
FireOutlined,
FlagOutlined,
// 文档图标
FolderOutlined,
FundOutlined,
GiftOutlined,
GlobalOutlined,
HeartOutlined,
HistoryOutlined,
// 常用系统图标
HomeOutlined,
IdcardOutlined,
InfoCircleOutlined,
KeyOutlined,
LaptopOutlined,
LeftOutlined,
LikeOutlined,
LineChartOutlined,
LinkOutlined,
// 状态图标
LoadingOutlined,
LockOutlined,
// 通信图标
MailOutlined,
// 导航图标
MenuOutlined,
MessageOutlined,
// 设备图标
MobileOutlined,
NotificationOutlined,
PauseCircleOutlined,
PayCircleOutlined,
PhoneOutlined,
// 媒体图标
PictureOutlined,
// 图表图标
PieChartOutlined,
PlayCircleOutlined,
PlusOutlined,
PoundOutlined,
PrinterOutlined,
QuestionCircleOutlined,
ReadOutlined,
ReloadOutlined,
RightOutlined,
RocketOutlined,
// 安全图标
SafetyOutlined,
// 操作图标
SaveOutlined,
SearchOutlined,
SecurityScanOutlined,
SettingOutlined,
ShareAltOutlined,
ShopOutlined,
// 商业图标
ShoppingCartOutlined,
StarOutlined,
StockOutlined,
SyncOutlined,
TabletOutlined,
TagOutlined,
TagsOutlined,
TeamOutlined,
ThunderboltOutlined,
// 工具图标
ToolOutlined,
TrophyOutlined,
UnlockOutlined,
UploadOutlined,
UpOutlined,
UserOutlined,
VideoCameraOutlined,
WalletOutlined,
// 网络图标
WifiOutlined,
} from '@ant-design/icons';
export type MyIconsType =
| 'BarcodeOutlined'
| 'AuditOutlined'
| 'ShopOutlined'
| 'BarChartOutlined'
| 'SettingOutlined'
| 'ControlOutlined'
| 'ClusterOutlined'
| 'BankOutlined'
| 'UserOutlined'
| 'CreditCardOutlined'
// 常用系统图标
| 'HomeOutlined'
| 'DashboardOutlined'
| 'FileOutlined'
| 'EditOutlined'
| 'DeleteOutlined'
| 'SearchOutlined'
| 'PlusOutlined'
| 'CloseOutlined'
| 'CheckOutlined'
| 'ExclamationCircleOutlined'
| 'InfoCircleOutlined'
| 'QuestionCircleOutlined'
// 导航图标
| 'MenuOutlined'
| 'LeftOutlined'
| 'RightOutlined'
| 'UpOutlined'
| 'DownOutlined'
// 操作图标
| 'SaveOutlined'
| 'CopyOutlined'
// | 'PrintOutlined' // 不存在,使用 PrinterOutlined 替代
| 'DownloadOutlined'
| 'UploadOutlined'
| 'ShareAltOutlined'
// 状态图标
| 'LoadingOutlined'
| 'SyncOutlined'
| 'ReloadOutlined'
| 'LockOutlined'
| 'UnlockOutlined'
| 'EyeOutlined'
| 'EyeInvisibleOutlined'
// 通信图标
| 'MailOutlined'
| 'PhoneOutlined'
| 'MessageOutlined'
| 'NotificationOutlined'
| 'BellOutlined'
// 文档图标
| 'FolderOutlined'
| 'FileTextOutlined'
| 'FilePdfOutlined'
| 'FileExcelOutlined'
| 'FileWordOutlined'
| 'FileImageOutlined'
// 媒体图标
| 'PictureOutlined'
| 'VideoCameraOutlined'
| 'AudioOutlined'
| 'PlayCircleOutlined'
| 'PauseCircleOutlined'
// 工具图标
| 'ToolOutlined'
| 'BugOutlined'
| 'CodeOutlined'
| 'ApiOutlined'
| 'DatabaseOutlined'
| 'CloudOutlined'
// 商业图标
| 'ShoppingCartOutlined'
| 'GiftOutlined'
| 'TrophyOutlined'
| 'StarOutlined'
| 'HeartOutlined'
| 'LikeOutlined'
// 时间图标
| 'CalendarOutlined'
| 'ClockCircleOutlined'
| 'HistoryOutlined'
// 地图图标
| 'EnvironmentOutlined'
| 'GlobalOutlined'
| 'CompassOutlined'
// 设备图标
| 'MobileOutlined'
| 'TabletOutlined'
| 'LaptopOutlined'
| 'DesktopOutlined'
| 'PrinterOutlined'
| 'CameraOutlined'
// 安全图标
| 'SafetyOutlined'
| 'SecurityScanOutlined'
| 'KeyOutlined'
| 'TeamOutlined'
| 'IdcardOutlined'
// 财务图标
| 'DollarOutlined'
| 'EuroOutlined'
| 'PoundOutlined'
| 'PayCircleOutlined'
| 'WalletOutlined'
// 图表图标
| 'PieChartOutlined'
| 'LineChartOutlined'
| 'AreaChartOutlined'
| 'FundOutlined'
| 'StockOutlined'
// 网络图标
| 'WifiOutlined'
| 'DisconnectOutlined'
| 'LinkOutlined'
// 其他常用图标
| 'FireOutlined'
| 'ThunderboltOutlined'
| 'BulbOutlined'
| 'RocketOutlined'
| 'CrownOutlined'
| 'FlagOutlined'
| 'TagOutlined'
| 'TagsOutlined'
| 'BookOutlined'
| 'ReadOutlined'
| 'ExperimentOutlined'
| 'BuildOutlined'
| 'CarOutlined';
export const MyIcons = {
BarcodeOutlined: <BarcodeOutlined />,
AuditOutlined: <AuditOutlined />,
ShopOutlined: <ShopOutlined />,
BarChartOutlined: <BarChartOutlined />,
SettingOutlined: <SettingOutlined />,
ControlOutlined: <ControlOutlined />,
ClusterOutlined: <ClusterOutlined />,
BankOutlined: <BankOutlined />,
UserOutlined: <UserOutlined />,
CreditCardOutlined: <CreditCardOutlined />,
// 常用系统图标
HomeOutlined: <HomeOutlined />,
DashboardOutlined: <DashboardOutlined />,
FileOutlined: <FileOutlined />,
EditOutlined: <EditOutlined />,
DeleteOutlined: <DeleteOutlined />,
SearchOutlined: <SearchOutlined />,
PlusOutlined: <PlusOutlined />,
CloseOutlined: <CloseOutlined />,
CheckOutlined: <CheckOutlined />,
ExclamationCircleOutlined: <ExclamationCircleOutlined />,
InfoCircleOutlined: <InfoCircleOutlined />,
QuestionCircleOutlined: <QuestionCircleOutlined />,
// 导航图标
MenuOutlined: <MenuOutlined />,
LeftOutlined: <LeftOutlined />,
RightOutlined: <RightOutlined />,
UpOutlined: <UpOutlined />,
DownOutlined: <DownOutlined />,
// 操作图标
SaveOutlined: <SaveOutlined />,
CopyOutlined: <CopyOutlined />,
// PrintOutlined: <PrintOutlined />, // 不存在,使用 PrinterOutlined 替代
DownloadOutlined: <DownloadOutlined />,
UploadOutlined: <UploadOutlined />,
ShareAltOutlined: <ShareAltOutlined />,
// 状态图标
LoadingOutlined: <LoadingOutlined />,
SyncOutlined: <SyncOutlined />,
ReloadOutlined: <ReloadOutlined />,
LockOutlined: <LockOutlined />,
UnlockOutlined: <UnlockOutlined />,
EyeOutlined: <EyeOutlined />,
EyeInvisibleOutlined: <EyeInvisibleOutlined />,
// 通信图标
MailOutlined: <MailOutlined />,
PhoneOutlined: <PhoneOutlined />,
MessageOutlined: <MessageOutlined />,
NotificationOutlined: <NotificationOutlined />,
BellOutlined: <BellOutlined />,
// 文档图标
FolderOutlined: <FolderOutlined />,
FileTextOutlined: <FileTextOutlined />,
FilePdfOutlined: <FilePdfOutlined />,
FileExcelOutlined: <FileExcelOutlined />,
FileWordOutlined: <FileWordOutlined />,
FileImageOutlined: <FileImageOutlined />,
// 媒体图标
PictureOutlined: <PictureOutlined />,
VideoCameraOutlined: <VideoCameraOutlined />,
AudioOutlined: <AudioOutlined />,
PlayCircleOutlined: <PlayCircleOutlined />,
PauseCircleOutlined: <PauseCircleOutlined />,
// 工具图标
ToolOutlined: <ToolOutlined />,
BugOutlined: <BugOutlined />,
CodeOutlined: <CodeOutlined />,
ApiOutlined: <ApiOutlined />,
DatabaseOutlined: <DatabaseOutlined />,
CloudOutlined: <CloudOutlined />,
// 商业图标
ShoppingCartOutlined: <ShoppingCartOutlined />,
GiftOutlined: <GiftOutlined />,
TrophyOutlined: <TrophyOutlined />,
StarOutlined: <StarOutlined />,
HeartOutlined: <HeartOutlined />,
LikeOutlined: <LikeOutlined />,
// 时间图标
CalendarOutlined: <CalendarOutlined />,
ClockCircleOutlined: <ClockCircleOutlined />,
HistoryOutlined: <HistoryOutlined />,
// 地图图标
EnvironmentOutlined: <EnvironmentOutlined />,
GlobalOutlined: <GlobalOutlined />,
CompassOutlined: <CompassOutlined />,
// 设备图标
MobileOutlined: <MobileOutlined />,
TabletOutlined: <TabletOutlined />,
LaptopOutlined: <LaptopOutlined />,
DesktopOutlined: <DesktopOutlined />,
PrinterOutlined: <PrinterOutlined />,
CameraOutlined: <CameraOutlined />,
// 安全图标
SafetyOutlined: <SafetyOutlined />,
SecurityScanOutlined: <SecurityScanOutlined />,
KeyOutlined: <KeyOutlined />,
TeamOutlined: <TeamOutlined />,
IdcardOutlined: <IdcardOutlined />,
// 财务图标
DollarOutlined: <DollarOutlined />,
EuroOutlined: <EuroOutlined />,
PoundOutlined: <PoundOutlined />,
PayCircleOutlined: <PayCircleOutlined />,
WalletOutlined: <WalletOutlined />,
// 图表图标
PieChartOutlined: <PieChartOutlined />,
LineChartOutlined: <LineChartOutlined />,
AreaChartOutlined: <AreaChartOutlined />,
FundOutlined: <FundOutlined />,
StockOutlined: <StockOutlined />,
// 网络图标
WifiOutlined: <WifiOutlined />,
DisconnectOutlined: <DisconnectOutlined />,
LinkOutlined: <LinkOutlined />,
// 其他常用图标
FireOutlined: <FireOutlined />,
ThunderboltOutlined: <ThunderboltOutlined />,
BulbOutlined: <BulbOutlined />,
RocketOutlined: <RocketOutlined />,
CrownOutlined: <CrownOutlined />,
FlagOutlined: <FlagOutlined />,
TagOutlined: <TagOutlined />,
TagsOutlined: <TagsOutlined />,
BookOutlined: <BookOutlined />,
ReadOutlined: <ReadOutlined />,
ExperimentOutlined: <ExperimentOutlined />,
BuildOutlined: <BuildOutlined />,
CarOutlined: <CarOutlined />,
};

View File

@ -0,0 +1,18 @@
import { Flex, Statistic } from 'antd';
export function MyStatistics({ items = {} }: { items?: Record<string, any> }) {
return (
<Flex style={{ padding: '0px 20px 20px 20px' }}>
{Object.keys(items).map((key) => {
return (
<Statistic
key={key}
title={key}
value={items[key]}
style={{ marginRight: 50 }}
/>
);
})}
</Flex>
);
}

View File

@ -0,0 +1,16 @@
import { Tag } from 'antd';
import { MyProEnumItemProps } from '../typings';
export default function MyTag({
enums,
value,
}: {
enums: MyProEnumItemProps;
value: string;
}) {
const item = enums[value] ?? undefined;
if (!item) return <>-</>;
console.log('item', value, item);
return <Tag color={item.color}>{item.text}</Tag>;
// return <Tag color={item.color}>{item.text}</Tag>;
}

View File

@ -0,0 +1,58 @@
import { ColorPicker } from 'antd';
export function MyColorPicker(props: any) {
return (
<ColorPicker
allowClear
arrow
showText
format="hex"
defaultValue={null}
presets={[
{
label: 'Recommended',
colors: [
'#000000',
'#000000E0',
'#000000A6',
'#00000073',
'#00000040',
'#00000026',
'#0000001A',
'#00000012',
'#0000000A',
'#00000005',
'#F5222D',
'#FA8C16',
'#FADB14',
'#8BBB11',
'#52C41A',
'#13A8A8',
'#1677FF',
'#2F54EB',
'#722ED1',
'#EB2F96',
'#F5222D4D',
'#FA8C164D',
'#FADB144D',
'#8BBB114D',
'#52C41A4D',
'#13A8A84D',
'#1677FF4D',
'#2F54EB4D',
'#722ED14D',
'#EB2F964D',
],
},
{
label: 'Recent',
colors: [],
},
]}
{...props}
onChange={(color) => {
props.onChange?.(color.toHexString());
}}
/>
);
}

View File

@ -0,0 +1,21 @@
import { MyProEnumItemProps } from '@/common/typings';
import { Radio } from 'antd';
export default function MyEnumRadioGroup(props: {
enums: MyProEnumItemProps;
value: string | number | undefined;
onChange: (value: string | number | undefined) => void;
}) {
const options = Object.entries(props.enums).map(([, value]) => {
return { label: value.text, value: value.value };
});
return (
<Radio.Group
options={options}
onChange={(e) => props?.onChange(e.target.value)}
value={props.value || options[0].value}
optionType="button"
buttonStyle="solid"
/>
);
}

View File

@ -0,0 +1,19 @@
import { Select, SelectProps, Space } from 'antd';
import { MyIcons } from '../MyIcons';
export function MyIconSelect(props: SelectProps) {
return (
<Select allowClear {...props}>
{Object?.entries(MyIcons).map(([key, Icon]) => {
return (
<Select.Option value={key} label={key} key={key}>
<Space>
{Icon}
{key}
</Space>
</Select.Option>
);
})}
</Select>
);
}

View File

@ -0,0 +1,12 @@
import { InputNumber } from 'antd';
export function MyMoneyInput(props: any) {
return (
<InputNumber
addonBefore="¥"
precision={2}
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
{...props}
/>
);
}

View File

@ -0,0 +1,5 @@
import { InputNumber } from 'antd';
export function MyPercentInput(props: any) {
return <InputNumber addonAfter="%" precision={2} {...props} />;
}

View File

@ -0,0 +1,55 @@
import { Tree, TreeProps } from 'antd';
import { DataNode } from 'antd/es/tree';
import { Key, useEffect, useState } from 'react';
import { MyProFormFieldProps, MyResponseType } from '../../typings';
export function MyTreeCheckable(
props: {
api: (data?: any) => Promise<MyResponseType>;
} & TreeProps<DataNode> &
MyProFormFieldProps<Key[]>,
) {
const [treeData, setTreeData] = useState<DataNode[]>([]);
const processTree = (item: any): DataNode => {
return {
...item,
key: item.id,
title: item.id + '_' + item.name,
children: item.children?.map(processTree),
};
};
useEffect(() => {
props.api?.({ guard_name: process.env.GUARD_NAME }).then((res: any) => {
const data = res.data?.map(processTree);
setTreeData(data);
});
}, []);
const onCheck: TreeProps['onCheck'] = (checkedKeys, info) => {
console.log('onCheck', checkedKeys, info);
const ids: Key[] = [];
info.checkedNodes?.forEach((item) => {
if (item.children?.length === 0) {
ids.push(item.key);
}
});
console.log('ids', ids);
props.onChange?.(ids);
};
return (treeData?.length ?? 0) > 0 ? (
<Tree
checkable
defaultExpandAll
showLine
treeData={treeData}
onCheck={onCheck}
checkedKeys={props.value ?? []}
{...props}
/>
) : (
<></>
);
}

View File

@ -0,0 +1,40 @@
.my-upload-images {
.ant-upload-list {
display: flex !important;
flex-direction: row !important;
flex-wrap: wrap !important;
gap: 8px !important;
align-items: flex-start !important;
}
.ant-upload-list-item-name {
width: 300px;
overflow: hidden;
}
.ant-upload-list-item {
margin: 0 !important;
margin-bottom: 0 !important;
margin-right: 0 !important;
}
.ant-upload-select {
margin: 0 !important;
margin-right: 0 !important;
}
// 针对文件类型的特殊样式
&.ant-upload-list-text {
.ant-upload-list-item {
display: inline-block;
width: auto;
margin-right: 8px;
}
}
// 针对图片类型的特殊样式
&.ant-upload-list-picture-card {
.ant-upload-list-item {
width: 104px;
height: 104px;
}
}
}

View File

@ -0,0 +1,175 @@
import { MyProFormFieldProps } from '@/common';
import { Apis } from '@/gen/Apis';
import { PlusOutlined, UploadOutlined } from '@ant-design/icons';
import { Button, message, Modal, Upload, UploadFile, UploadProps } from 'antd';
import { RcFile } from 'antd/es/upload';
import axios from 'axios';
import { useSetState } from 'react-use';
import './MyUploadImages.scss';
type MyType = {
uploadType?: 'image' | 'video' | 'audio' | 'file';
max?: number;
size?: number;
accept?: string;
} & UploadProps<any> &
MyProFormFieldProps<UploadFile[]>;
const getBase64 = (file: RcFile): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
export function MyUploadImages({
value,
onChange,
uploadType = 'image',
max = 1,
size = 10,
accept = '*',
...rest
}: MyType) {
const [preview, setPreview] = useSetState<{
open: boolean;
image: string;
title: string;
}>({
open: false,
image: '',
title: '',
});
console.log('MyUploadImages');
const handleCancel = () => setPreview({ open: false });
const handlePreview = async (file: UploadFile) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj as RcFile);
}
setPreview({
open: true,
image: file.url || file.preview,
title: file.name || file.url!.substring(file.url!.lastIndexOf('/') + 1),
});
};
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
console.log('newFileList', newFileList);
const _newFileList: UploadFile<any>[] = newFileList.map((file) => {
if (!file.percent) return file;
return {
name: file.name,
status: file.status,
uid: file.uid,
url: file.response,
size: file.size ?? 0,
type: file.type ?? '',
};
});
onChange?.(_newFileList);
};
const uploadButton =
uploadType === 'image' ? (
<div>
<PlusOutlined />
</div>
) : (
<Button icon={<UploadOutlined />}></Button>
);
const customRequest = ({ file, onError, onProgress, onSuccess }: any) => {
Apis.Common.Auth.PreUpload({
filename: file.name,
alc: 'public-read',
})
.then(async (res) => {
try {
axios
.put(res.data.url, file, {
headers: res.data.headers,
onUploadProgress: ({ total, loaded }) => {
if (total)
onProgress(
{ percent: Math.round((loaded / total) * 100).toFixed(2) },
file,
);
},
})
.then(({ data: response }) => {
console.log('response', response);
if (response && response.errorMessage) {
onError(response, file);
} else {
// 确保URL正确处理
const fileUrl = res.data.url.split('?')[0];
console.log('文件上传成功URL:', fileUrl);
onSuccess(fileUrl, file);
}
})
.catch((error) => {
console.error('上传文件时出错:', error);
onError(error, file);
});
} catch (error) {
console.error('处理上传请求时出错:', error);
onError(error, file);
}
})
.catch((error) => {
console.error('获取预上传URL时出错:', error);
onError(error, file);
});
return {
abort() {
console.log('upload progress is aborted.');
},
};
};
const handleBeforeUpload = (file: any, fileList: any) => {
if (file?.size > 1024 * 1024 * size) {
message.error('文件大小不能超过10MB请选择重新上传');
return false;
}
console.log('beforeUpload', file, fileList);
};
return (
<>
<Upload
accept={
uploadType === 'image'
? 'image/*'
: uploadType === 'video'
? 'video/*'
: accept
}
beforeUpload={handleBeforeUpload}
fileList={value}
listType={uploadType === 'image' ? 'picture-card' : 'text'}
onPreview={handlePreview}
onChange={handleChange}
customRequest={customRequest}
className="my-upload-images"
{...rest}
>
{!value?.length || (value && value.length < max) ? uploadButton : null}
</Upload>
<Modal
open={preview.open}
title={preview.title}
footer={null}
onCancel={handleCancel}
>
<img alt="preview" style={{ width: '100%' }} src={preview.image} />
</Modal>
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,68 @@
import { Apis } from '@/gen/Apis';
import {
LogoutOutlined,
UnlockOutlined,
UserOutlined,
} from '@ant-design/icons';
import { history } from '@umijs/max';
import { Avatar, Dropdown, MenuProps, Space } from 'antd';
import { useState } from 'react';
import { stateActions } from '../../libs/valtio/actions';
import AvatarIcon from './AvatarIcon.png';
import ChangePassword from './ChangePassword';
export default function AvatarProps({ user }: { user: any }) {
const [open, setOpen] = useState(false);
// const [openDrawer, setOpenDrawer] = useState(false);
// const showDrawer = () => {
// setOpenDrawer(true);
// };
// const onClose = () => {
// setOpenDrawer(false);
// };
const items: MenuProps['items'] = [
{
key: 'changePassword',
label: (
<Space
onClick={() => {
setOpen(true);
}}
>
<UnlockOutlined />
</Space>
),
},
{
key: 'logout',
label: (
<Space
onClick={() => {
Apis.Common.Auth.Logout().then(() => {
stateActions.setLogout();
history.push('/login');
});
}}
>
<LogoutOutlined />
退
</Space>
),
},
];
return (
<>
<Dropdown menu={{ items }} trigger={['click']}>
<a onClick={(e) => e.preventDefault()}>
<Space>
<Avatar icon={<UserOutlined />} src={AvatarIcon} size={28} />
<span>{user?.name}</span>
</Space>
</a>
</Dropdown>
<ChangePassword open={open} setOpen={setOpen} />
</>
);
}

View File

@ -0,0 +1,39 @@
import { Apis } from '@/gen/Apis';
import { ModalForm, ProFormText } from '@ant-design/pro-components';
import { message } from 'antd';
export default function ChangePassword({
open,
setOpen,
}: {
open: boolean;
setOpen: (open: boolean) => void;
}) {
return (
<ModalForm<ApiTypes.Common.Auth.ChangePassword>
open={open}
wrapperCol={{ span: 24 }}
width="500px"
title="修改密码"
onFinish={async (values) => {
return Apis.Common.Auth.ChangePassword(values)
.then(() => {
message.success('修改密码成功');
setOpen(false);
})
.catch(() => false);
}}
modalProps={{
onCancel: () => setOpen(false),
}}
>
<ProFormText.Password name="old_password" label="原密码" required />
<ProFormText.Password name="new_password" label="新密码" required />
<ProFormText.Password
name="re_new_password"
label="重复新密码"
required
/>
</ModalForm>
);
}

View File

@ -0,0 +1,40 @@
import { Button, Modal } from 'antd';
import { ReactNode, useState } from 'react';
export function MyCommonModal(props: {
title: string;
width: number;
button_text: string;
children: ReactNode;
}) {
const [isModalOpen, setIsModalOpen] = useState(false);
const showModal = () => {
setIsModalOpen(true);
};
const handleOk = () => {
setIsModalOpen(false);
};
const handleCancel = () => {
setIsModalOpen(false);
};
return (
<>
<Button onClick={showModal} size="small">
{props.button_text}
</Button>
<Modal
title={props.title}
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
width={props.width || 800}
destroyOnClose
>
{props.children}
</Modal>
</>
);
}

View File

@ -0,0 +1,149 @@
import { MyResponseType } from '@/common';
import {
ImportOutlined,
InboxOutlined,
LoadingOutlined,
} from '@ant-design/icons';
import { Button, Flex, Modal, Space, Upload, message } from 'antd';
import { useState } from 'react';
type MyImportModalType = {
title?: string;
type?: any;
danger?: boolean;
params?: Record<string, any>;
size?: 'small' | 'middle' | 'large';
templateApi?: () => Promise<MyResponseType>;
importApi: (data: any) => Promise<MyResponseType>;
reload?: () => void;
};
export function MyImportModal(props: MyImportModalType) {
const [open, setOpen] = useState(false);
const [uploading, setUploading] = useState(false);
const {
title = '批量导入',
params = {},
type = 'primary',
size = 'middle',
danger = false,
} = props;
// 处理文件上传
const handleBeforeUpload = (file: File) => {
// 文件类型验证
const validTypes = [
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
'application/vnd.oasis.opendocument.spreadsheet',
];
const fileExtension = file.name.split('.').pop()?.toLowerCase();
const isExcelFile =
file.type.includes('excel') || file.type.includes('spreadsheet');
const isCSVFile = file.type === 'text/csv' || fileExtension === 'csv';
if (!isExcelFile && !isCSVFile) {
message.error('请上传Excel或CSV文件');
return Upload.LIST_IGNORE;
}
// 开始导入
startImport(file);
return false; // 阻止默认上传
};
// 执行导入
const startImport = (file: File) => {
setUploading(true);
const formData = new FormData();
formData.append('upload_file', file);
Object.entries(params).forEach(([key, value]) => {
formData.append(key, value);
});
props
?.importApi(formData)
.then(() => {
message.success('导入成功');
setTimeout(() => {
setUploading(false);
setOpen(false);
props.reload?.();
}, 800); // 延迟一点时间关闭,让用户看到成功提示
})
.catch((error) => {
message.error(error?.message || '导入失败');
setUploading(false);
});
};
return (
<>
<Button
onClick={() => setOpen(true)}
type={type}
danger={danger}
size={size}
>
<ImportOutlined />
{title}
</Button>
<Modal
title={title}
open={open}
onCancel={() => {
if (!uploading) {
setOpen(false);
}
}}
footer={(_, { CancelBtn }) => (
<Flex style={{ width: '100%' }} justify="space-between">
<Button onClick={() => props?.templateApi?.()} disabled={uploading}>
</Button>
<Space>{!uploading && <CancelBtn />}</Space>
</Flex>
)}
maskClosable={!uploading}
closable={!uploading}
destroyOnClose
>
{uploading ? (
<div style={{ textAlign: 'center', padding: '40px 0' }}>
<LoadingOutlined
style={{ fontSize: 48, color: '#1890ff', marginBottom: 20 }}
/>
<p style={{ fontSize: 16, color: '#666' }}>
...
</p>
<p style={{ fontSize: 14, color: '#999', marginTop: 8 }}>
</p>
</div>
) : (
<Upload.Dragger
accept=".xls,.xlsx,.csv,.ods"
maxCount={1}
showUploadList={false}
beforeUpload={handleBeforeUpload}
>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text"></p>
<p className="ant-upload-hint">
Excel(.xls, .xlsx)CSV(.csv)
<br />
</p>
</Upload.Dragger>
)}
</Modal>
</>
);
}

View File

@ -0,0 +1,31 @@
import { Spin } from 'antd';
import { useMyState } from '../..';
export function MyLoading() {
const { snap } = useMyState();
if (snap.session.loading === 0) return null;
return (
<div
style={{
top: 0,
left: 0,
bottom: 0,
right: 0,
width: '100vw',
height: '100vh',
position: 'fixed',
zIndex: 9999,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0)',
}}
>
<Spin size="large">
<div style={{ paddingTop: '70px' }} />
Loading...
</Spin>
</div>
);
}

View File

@ -0,0 +1,305 @@
import { HomeOutlined } from '@ant-design/icons';
import { PageContainer, PageContainerProps } from '@ant-design/pro-components';
import { useLocation, useNavigate } from '@umijs/max';
import { Breadcrumb, Space } from 'antd';
import { useEffect, useState } from 'react';
// import './MyPageContainer.scss';
export interface TabItem {
key: string;
label: string;
path: string;
closable?: boolean;
}
interface BreadcrumbItem {
title: string;
path: string;
onClick?: () => void;
}
export interface MyPageContainerProps extends PageContainerProps {
enableTabs?: boolean;
tabKey?: string;
tabLabel?: string;
onTabChange?: (activeKey: string) => void;
}
// 注释掉标签页管理相关代码,不再使用
// // 全局标签页状态管理
// class TabsManager {
// private tabs: TabItem[] = [];
// private activeKey: string = '';
// private listeners: Set<() => void> = new Set();
// subscribe(listener: () => void) {
// this.listeners.add(listener);
// return () => this.listeners.delete(listener);
// }
// private notify() {
// this.listeners.forEach((listener) => listener());
// }
// getTabs() {
// return this.tabs;
// }
// getActiveKey() {
// return this.activeKey;
// }
// addTab(tab: TabItem) {
// const existingIndex = this.tabs.findIndex((t) => t.key === tab.key);
// if (existingIndex === -1) {
// // 如果是新标签页,插入到当前激活标签页的右边
// const currentActiveIndex = this.tabs.findIndex(
// (t) => t.key === this.activeKey,
// );
// if (currentActiveIndex !== -1) {
// // 在当前激活标签页的右边插入新标签页
// this.tabs.splice(currentActiveIndex + 1, 0, tab);
// } else {
// // 如果没有当前激活标签页,添加到末尾
// this.tabs.push(tab);
// }
// } else {
// // 如果标签页已存在,更新其信息
// this.tabs[existingIndex] = { ...this.tabs[existingIndex], ...tab };
// }
// this.activeKey = tab.key;
// this.notify();
// }
// addTabNext(tab: TabItem) {
// const existingIndex = this.tabs.findIndex((t) => t.key === tab.key);
// if (existingIndex === -1) {
// // 强制在当前激活标签页的右边插入新标签页
// const currentActiveIndex = this.tabs.findIndex(
// (t) => t.key === this.activeKey,
// );
// if (currentActiveIndex !== -1) {
// this.tabs.splice(currentActiveIndex + 1, 0, tab);
// } else {
// this.tabs.push(tab);
// }
// } else {
// // 如果标签页已存在,移动到当前激活标签页的右边
// const existingTab = this.tabs[existingIndex];
// this.tabs.splice(existingIndex, 1); // 先移除原位置的标签页
// const currentActiveIndex = this.tabs.findIndex(
// (t) => t.key === this.activeKey,
// );
// if (currentActiveIndex !== -1) {
// this.tabs.splice(currentActiveIndex + 1, 0, { ...existingTab, ...tab });
// } else {
// this.tabs.push({ ...existingTab, ...tab });
// }
// }
// this.activeKey = tab.key;
// this.notify();
// }
// removeTab(targetKey: string) {
// const targetIndex = this.tabs.findIndex((tab) => tab.key === targetKey);
// if (targetIndex === -1) return;
// const newTabs = this.tabs.filter((tab) => tab.key !== targetKey);
// if (newTabs.length === 0) {
// this.tabs = [];
// this.activeKey = '';
// history.push('/');
// } else {
// this.tabs = newTabs;
// if (this.activeKey === targetKey) {
// // 如果关闭的是当前激活的标签,激活相邻的标签
// const newActiveKey =
// targetIndex > 0 ? newTabs[targetIndex - 1].key : newTabs[0].key;
// this.activeKey = newActiveKey;
// const targetTab = newTabs.find((tab) => tab.key === newActiveKey);
// if (targetTab) {
// history.push(targetTab.path);
// }
// }
// }
// this.notify();
// }
// setActiveKey(key: string) {
// this.activeKey = key;
// const targetTab = this.tabs.find((tab) => tab.key === key);
// if (targetTab) {
// history.push(targetTab.path);
// }
// this.notify();
// }
// closeOtherTabs(currentKey: string) {
// const currentTab = this.tabs.find((tab) => tab.key === currentKey);
// if (currentTab) {
// this.tabs = [currentTab];
// this.activeKey = currentKey;
// this.notify();
// }
// }
// closeLeftTabs(currentKey: string) {
// const currentIndex = this.tabs.findIndex((tab) => tab.key === currentKey);
// if (currentIndex > 0) {
// this.tabs = this.tabs.slice(currentIndex);
// this.notify();
// }
// }
// closeRightTabs(currentKey: string) {
// const currentIndex = this.tabs.findIndex((tab) => tab.key === currentKey);
// if (currentIndex !== -1) {
// this.tabs = this.tabs.slice(0, currentIndex + 1);
// this.notify();
// }
// }
// refreshTab(key: string) {
// // 通过路由跳转的方式刷新当前标签页,避免整个页面刷新导致标签页状态丢失
// const targetTab = this.tabs.find((tab) => tab.key === key);
// if (targetTab) {
// const originalPath = targetTab.path;
// // 移除可能存在的刷新参数,确保获取干净的原始路径
// const cleanPath = originalPath
// .replace(/[?&]_refresh=\d+/g, '')
// .replace(/\?$/, '');
// // 添加时间戳参数强制刷新
// const refreshPath = cleanPath.includes('?')
// ? `${cleanPath}&_refresh=${Date.now()}`
// : `${cleanPath}?_refresh=${Date.now()}`;
// // 先更新为带刷新参数的路径并跳转
// targetTab.path = refreshPath;
// this.setActiveKey(key);
// // 延迟恢复原始路径,确保路由跳转完成
// setTimeout(() => {
// // 恢复为干净的原始路径
// targetTab.path = cleanPath;
// // 再次跳转到干净路径移除URL中的刷新参数
// history.push(cleanPath);
// this.notify();
// }, 300);
// }
// }
// }
// // 全局标签页管理器实例
// const tabsManager = new TabsManager();
export function MyPageContainer({
title,
children,
enableTabs = false, // 默认关闭多标签页功能
tabKey,
tabLabel,
onTabChange,
...rest
}: MyPageContainerProps) {
const navigate = useNavigate();
const location = useLocation(); // 使用useLocation钩子
const [dataBre, setDataBre] = useState<any>(
JSON.parse(sessionStorage.getItem('breadcrumbs') || '[]'),
);
// 简化的面包屑更新逻辑
const updateBreadcrumbs = () => {
try {
// 获取当前路径和标题
const currentPath = location.pathname + location.search;
const currentTitle =
typeof title === 'string'
? title
: String(title?.props?.children || '');
// 清空无效的面包屑数据(重置逻辑)
const newBreadcrumbs: BreadcrumbItem[] = [];
// 只添加当前页面的面包屑
if (currentPath && currentTitle) {
newBreadcrumbs.push({
title: currentTitle,
path: currentPath,
});
}
// 更新sessionStorage
try {
sessionStorage.setItem('breadcrumbs', JSON.stringify(newBreadcrumbs));
} catch (storageError) {
console.error('存储到sessionStorage失败:', storageError);
}
// 更新状态
setDataBre(newBreadcrumbs);
} catch (error) {
console.error('更新面包屑时出错:', error);
// 出错时重置面包屑
setDataBre([]);
}
};
useEffect(() => {
console.log('title', title);
setTimeout(() => {
updateBreadcrumbs();
}, 250);
}, [location.pathname, location.search, title]);
// 不再需要标签页相关的状态和逻辑
// 直接返回简化版的PageContainer
return (
<PageContainer
fixedHeader
header={{
title: (
<Space style={{ fontSize: '12px', cursor: 'pointer', color: '#999' }}>
<Breadcrumb
items={[
{
title: <HomeOutlined />,
onClick: () => {
navigate('/');
},
},
...dataBre?.map((res: any) => ({
title: res.title || '',
onClick: () => {
navigate(res.path);
},
})),
]}
/>
</Space>
),
style: {
backgroundColor: '#FFF',
boxShadow: '10px 2px 10px 0 rgba(0, 0, 0, 0.1)',
},
}}
token={{
paddingBlockPageContainerContent: 0,
paddingInlinePageContainerContent: 0,
}}
{...rest}
>
<Space
direction="vertical"
size="middle"
style={{
width: '100%',
}}
>
{children}
</Space>
</PageContainer>
);
}
// 不再导出标签页管理器
// export { tabsManager };

View File

@ -0,0 +1,410 @@
import { CloseOutlined, ReloadOutlined } from '@ant-design/icons';
import { PageContainer, PageContainerProps } from '@ant-design/pro-components';
import { history, useLocation } from '@umijs/max';
import type { MenuProps } from 'antd';
import { Dropdown, message, Space, Tabs } from 'antd';
import React, { useCallback, useEffect, useState } from 'react';
// import './MyPageContainer.scss';
export interface TabItem {
key: string;
label: string;
path: string;
closable?: boolean;
}
export interface MyPageContainerProps extends PageContainerProps {
enableTabs?: boolean;
tabKey?: string;
tabLabel?: string;
onTabChange?: (activeKey: string) => void;
}
// 全局标签页状态管理
class TabsManager {
private tabs: TabItem[] = [];
private activeKey: string = '';
private listeners: Set<() => void> = new Set();
subscribe(listener: () => void) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
private notify() {
this.listeners.forEach((listener) => listener());
}
getTabs() {
return this.tabs;
}
getActiveKey() {
return this.activeKey;
}
addTab(tab: TabItem) {
const existingIndex = this.tabs.findIndex((t) => t.key === tab.key);
if (existingIndex === -1) {
// 如果是新标签页,插入到当前激活标签页的右边
const currentActiveIndex = this.tabs.findIndex(
(t) => t.key === this.activeKey,
);
if (currentActiveIndex !== -1) {
// 在当前激活标签页的右边插入新标签页
this.tabs.splice(currentActiveIndex + 1, 0, tab);
} else {
// 如果没有当前激活标签页,添加到末尾
this.tabs.push(tab);
}
} else {
// 如果标签页已存在,更新其信息
this.tabs[existingIndex] = { ...this.tabs[existingIndex], ...tab };
}
this.activeKey = tab.key;
this.notify();
}
addTabNext(tab: TabItem) {
const existingIndex = this.tabs.findIndex((t) => t.key === tab.key);
if (existingIndex === -1) {
// 强制在当前激活标签页的右边插入新标签页
const currentActiveIndex = this.tabs.findIndex(
(t) => t.key === this.activeKey,
);
if (currentActiveIndex !== -1) {
this.tabs.splice(currentActiveIndex + 1, 0, tab);
} else {
this.tabs.push(tab);
}
} else {
// 如果标签页已存在,移动到当前激活标签页的右边
const existingTab = this.tabs[existingIndex];
this.tabs.splice(existingIndex, 1); // 先移除原位置的标签页
const currentActiveIndex = this.tabs.findIndex(
(t) => t.key === this.activeKey,
);
if (currentActiveIndex !== -1) {
this.tabs.splice(currentActiveIndex + 1, 0, { ...existingTab, ...tab });
} else {
this.tabs.push({ ...existingTab, ...tab });
}
}
this.activeKey = tab.key;
this.notify();
}
removeTab(targetKey: string) {
const targetIndex = this.tabs.findIndex((tab) => tab.key === targetKey);
if (targetIndex === -1) return;
const newTabs = this.tabs.filter((tab) => tab.key !== targetKey);
if (newTabs.length === 0) {
this.tabs = [];
this.activeKey = '';
history.push('/');
} else {
this.tabs = newTabs;
if (this.activeKey === targetKey) {
// 如果关闭的是当前激活的标签,激活相邻的标签
const newActiveKey =
targetIndex > 0 ? newTabs[targetIndex - 1].key : newTabs[0].key;
this.activeKey = newActiveKey;
const targetTab = newTabs.find((tab) => tab.key === newActiveKey);
if (targetTab) {
history.push(targetTab.path);
}
}
}
this.notify();
}
setActiveKey(key: string) {
this.activeKey = key;
const targetTab = this.tabs.find((tab) => tab.key === key);
if (targetTab) {
history.push(targetTab.path);
}
this.notify();
}
closeOtherTabs(currentKey: string) {
const currentTab = this.tabs.find((tab) => tab.key === currentKey);
if (currentTab) {
this.tabs = [currentTab];
this.activeKey = currentKey;
this.notify();
}
}
closeLeftTabs(currentKey: string) {
const currentIndex = this.tabs.findIndex((tab) => tab.key === currentKey);
if (currentIndex > 0) {
this.tabs = this.tabs.slice(currentIndex);
this.notify();
}
}
closeRightTabs(currentKey: string) {
const currentIndex = this.tabs.findIndex((tab) => tab.key === currentKey);
if (currentIndex !== -1) {
this.tabs = this.tabs.slice(0, currentIndex + 1);
this.notify();
}
}
refreshTab(key: string) {
// 通过路由跳转的方式刷新当前标签页,避免整个页面刷新导致标签页状态丢失
const targetTab = this.tabs.find((tab) => tab.key === key);
if (targetTab) {
const originalPath = targetTab.path;
// 移除可能存在的刷新参数,确保获取干净的原始路径
const cleanPath = originalPath
.replace(/[?&]_refresh=\d+/g, '')
.replace(/\?$/, '');
// 添加时间戳参数强制刷新
const refreshPath = cleanPath.includes('?')
? `${cleanPath}&_refresh=${Date.now()}`
: `${cleanPath}?_refresh=${Date.now()}`;
// 先更新为带刷新参数的路径并跳转
targetTab.path = refreshPath;
this.setActiveKey(key);
// 延迟恢复原始路径,确保路由跳转完成
setTimeout(() => {
// 恢复为干净的原始路径
targetTab.path = cleanPath;
// 再次跳转到干净路径移除URL中的刷新参数
history.push(cleanPath);
this.notify();
}, 300);
}
}
}
// 全局标签页管理器实例
const tabsManager = new TabsManager();
export function MyPageContainer({
title,
children,
enableTabs = true,
tabKey,
tabLabel,
onTabChange,
...rest
}: MyPageContainerProps) {
const location = useLocation();
const [tabs, setTabs] = useState<TabItem[]>([]);
const [activeKey, setActiveKey] = useState<string>('');
// 订阅标签页状态变化
useEffect(() => {
const unsubscribe = tabsManager.subscribe(() => {
setTabs([...tabsManager.getTabs()]);
setActiveKey(tabsManager.getActiveKey());
});
// 初始化状态
setTabs([...tabsManager.getTabs()]);
setActiveKey(tabsManager.getActiveKey());
return () => {
unsubscribe();
};
}, []);
// 当组件挂载时,添加当前页面到标签页
useEffect(() => {
if (enableTabs && tabKey && tabLabel) {
tabsManager.addTab({
key: tabKey,
label: tabLabel,
path: location.pathname + location.search,
closable: true,
});
}
}, [enableTabs, tabKey, tabLabel, location.pathname, location.search]);
const handleTabChange = useCallback(
(key: string) => {
tabsManager.setActiveKey(key);
onTabChange?.(key);
},
[onTabChange],
);
const handleTabEdit = useCallback(
(
targetKey: string | React.MouseEvent | React.KeyboardEvent,
action: 'add' | 'remove',
) => {
if (action === 'remove' && typeof targetKey === 'string') {
tabsManager.removeTab(targetKey);
}
},
[],
);
// 右键菜单配置
const getContextMenuItems = useCallback(
(targetKey: string): MenuProps['items'] => {
const currentIndex = tabs.findIndex((tab) => tab.key === targetKey);
const hasLeftTabs = currentIndex > 0;
const hasRightTabs = currentIndex < tabs.length - 1;
const hasOtherTabs = tabs.length > 1;
return [
{
key: 'refresh',
label: '刷新',
icon: <ReloadOutlined />,
onClick: () => tabsManager.refreshTab(targetKey),
},
{
key: 'close',
label: '关闭',
icon: <CloseOutlined />,
onClick: () => tabsManager.removeTab(targetKey),
},
{
type: 'divider',
},
{
key: 'closeOthers',
label: '关闭其他',
disabled: !hasOtherTabs,
onClick: () => {
tabsManager.closeOtherTabs(targetKey);
message.success('已关闭其他标签页');
},
},
{
key: 'closeLeft',
label: '关闭左侧',
disabled: !hasLeftTabs,
onClick: () => {
tabsManager.closeLeftTabs(targetKey);
message.success('已关闭左侧标签页');
},
},
{
key: 'closeRight',
label: '关闭右侧',
disabled: !hasRightTabs,
onClick: () => {
tabsManager.closeRightTabs(targetKey);
message.success('已关闭右侧标签页');
},
},
];
},
[tabs],
);
// 自定义标签页渲染
const renderTabBar: React.ComponentProps<typeof Tabs>['renderTabBar'] = (
props,
DefaultTabBar,
) => {
return (
<DefaultTabBar {...props}>
{(node) => {
const tabKey = node.key as string;
return (
<Dropdown
menu={{ items: getContextMenuItems(tabKey) }}
trigger={['contextMenu']}
>
<div>{node}</div>
</Dropdown>
);
}}
</DefaultTabBar>
);
};
if (!enableTabs || tabs.length === 0) {
return (
<PageContainer
fixedHeader
header={{
title: title,
style: { backgroundColor: '#FFF' },
}}
token={{
paddingBlockPageContainerContent: 0,
paddingInlinePageContainerContent: 0,
}}
{...rest}
>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
{children}
</Space>
</PageContainer>
);
}
return (
<PageContainer
fixedHeader
// tabList={tabs.map((tab) => ({
// key: tab.key,
// label: tab.label,
// closable: tab.closable,
// }))}
// tabActiveKey={activeKey}
// header={{
// ghost: true,
// }}
// tabProps={{
// type: 'editable-card',
// hideAdd: true,
// onEdit: (e, action) => handleTabEdit(e, action),
// }}
ghost={true}
header={{
ghost: true,
title: (
<Tabs
type="editable-card"
activeKey={activeKey}
onChange={handleTabChange}
onEdit={handleTabEdit}
hideAdd
size="small"
renderTabBar={renderTabBar}
items={tabs.map((tab) => ({
key: tab.key,
label: tab.label,
closable: tab.closable,
}))}
/>
),
style: {
padding: '5px 0 0 0',
background: '#fff',
height: '50px',
},
}}
token={{
paddingBlockPageContainerContent: 0,
paddingInlinePageContainerContent: 0,
}}
{...rest}
>
{/* <Space direction="vertical" size="middle" style={{ width: '100%' }}> */}
{children}
{/* <KeepAlive id={activeKey} name={activeKey} tabName={title}>
{children}
</KeepAlive> */}
{/* </Space> */}
</PageContainer>
);
}
// 导出标签页管理器,供其他组件使用
export { tabsManager };

View File

@ -0,0 +1,220 @@
import { PageContainer, PageContainerProps } from '@ant-design/pro-components';
import { Space } from 'antd';
// import './MyPageContainer.scss';
export interface TabItem {
key: string;
label: string;
path: string;
closable?: boolean;
}
export interface MyPageContainerProps extends PageContainerProps {
enableTabs?: boolean;
tabKey?: string;
tabLabel?: string;
onTabChange?: (activeKey: string) => void;
}
// 注释掉标签页管理相关代码,不再使用
// // 全局标签页状态管理
// class TabsManager {
// private tabs: TabItem[] = [];
// private activeKey: string = '';
// private listeners: Set<() => void> = new Set();
// subscribe(listener: () => void) {
// this.listeners.add(listener);
// return () => this.listeners.delete(listener);
// }
// private notify() {
// this.listeners.forEach((listener) => listener());
// }
// getTabs() {
// return this.tabs;
// }
// getActiveKey() {
// return this.activeKey;
// }
// addTab(tab: TabItem) {
// const existingIndex = this.tabs.findIndex((t) => t.key === tab.key);
// if (existingIndex === -1) {
// // 如果是新标签页,插入到当前激活标签页的右边
// const currentActiveIndex = this.tabs.findIndex(
// (t) => t.key === this.activeKey,
// );
// if (currentActiveIndex !== -1) {
// // 在当前激活标签页的右边插入新标签页
// this.tabs.splice(currentActiveIndex + 1, 0, tab);
// } else {
// // 如果没有当前激活标签页,添加到末尾
// this.tabs.push(tab);
// }
// } else {
// // 如果标签页已存在,更新其信息
// this.tabs[existingIndex] = { ...this.tabs[existingIndex], ...tab };
// }
// this.activeKey = tab.key;
// this.notify();
// }
// addTabNext(tab: TabItem) {
// const existingIndex = this.tabs.findIndex((t) => t.key === tab.key);
// if (existingIndex === -1) {
// // 强制在当前激活标签页的右边插入新标签页
// const currentActiveIndex = this.tabs.findIndex(
// (t) => t.key === this.activeKey,
// );
// if (currentActiveIndex !== -1) {
// this.tabs.splice(currentActiveIndex + 1, 0, tab);
// } else {
// this.tabs.push(tab);
// }
// } else {
// // 如果标签页已存在,移动到当前激活标签页的右边
// const existingTab = this.tabs[existingIndex];
// this.tabs.splice(existingIndex, 1); // 先移除原位置的标签页
// const currentActiveIndex = this.tabs.findIndex(
// (t) => t.key === this.activeKey,
// );
// if (currentActiveIndex !== -1) {
// this.tabs.splice(currentActiveIndex + 1, 0, { ...existingTab, ...tab });
// } else {
// this.tabs.push({ ...existingTab, ...tab });
// }
// }
// this.activeKey = tab.key;
// this.notify();
// }
// removeTab(targetKey: string) {
// const targetIndex = this.tabs.findIndex((tab) => tab.key === targetKey);
// if (targetIndex === -1) return;
// const newTabs = this.tabs.filter((tab) => tab.key !== targetKey);
// if (newTabs.length === 0) {
// this.tabs = [];
// this.activeKey = '';
// history.push('/');
// } else {
// this.tabs = newTabs;
// if (this.activeKey === targetKey) {
// // 如果关闭的是当前激活的标签,激活相邻的标签
// const newActiveKey =
// targetIndex > 0 ? newTabs[targetIndex - 1].key : newTabs[0].key;
// this.activeKey = newActiveKey;
// const targetTab = newTabs.find((tab) => tab.key === newActiveKey);
// if (targetTab) {
// history.push(targetTab.path);
// }
// }
// }
// this.notify();
// }
// setActiveKey(key: string) {
// this.activeKey = key;
// const targetTab = this.tabs.find((tab) => tab.key === key);
// if (targetTab) {
// history.push(targetTab.path);
// }
// this.notify();
// }
// closeOtherTabs(currentKey: string) {
// const currentTab = this.tabs.find((tab) => tab.key === currentKey);
// if (currentTab) {
// this.tabs = [currentTab];
// this.activeKey = currentKey;
// this.notify();
// }
// }
// closeLeftTabs(currentKey: string) {
// const currentIndex = this.tabs.findIndex((tab) => tab.key === currentKey);
// if (currentIndex > 0) {
// this.tabs = this.tabs.slice(currentIndex);
// this.notify();
// }
// }
// closeRightTabs(currentKey: string) {
// const currentIndex = this.tabs.findIndex((tab) => tab.key === currentKey);
// if (currentIndex !== -1) {
// this.tabs = this.tabs.slice(0, currentIndex + 1);
// this.notify();
// }
// }
// refreshTab(key: string) {
// // 通过路由跳转的方式刷新当前标签页,避免整个页面刷新导致标签页状态丢失
// const targetTab = this.tabs.find((tab) => tab.key === key);
// if (targetTab) {
// const originalPath = targetTab.path;
// // 移除可能存在的刷新参数,确保获取干净的原始路径
// const cleanPath = originalPath
// .replace(/[?&]_refresh=\d+/g, '')
// .replace(/\?$/, '');
// // 添加时间戳参数强制刷新
// const refreshPath = cleanPath.includes('?')
// ? `${cleanPath}&_refresh=${Date.now()}`
// : `${cleanPath}?_refresh=${Date.now()}`;
// // 先更新为带刷新参数的路径并跳转
// targetTab.path = refreshPath;
// this.setActiveKey(key);
// // 延迟恢复原始路径,确保路由跳转完成
// setTimeout(() => {
// // 恢复为干净的原始路径
// targetTab.path = cleanPath;
// // 再次跳转到干净路径移除URL中的刷新参数
// history.push(cleanPath);
// this.notify();
// }, 300);
// }
// }
// }
// // 全局标签页管理器实例
// const tabsManager = new TabsManager();
export function MyPageContainer({
title,
children,
enableTabs = false, // 默认关闭多标签页功能
tabKey,
tabLabel,
onTabChange,
...rest
}: MyPageContainerProps) {
// 不再需要标签页相关的状态和逻辑
// 直接返回简化版的PageContainer
return (
<PageContainer
fixedHeader
header={{
title: title,
style: { backgroundColor: '#FFF' },
}}
token={{
paddingBlockPageContainerContent: 0,
paddingInlinePageContainerContent: 0,
}}
{...rest}
>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
{children}
</Space>
</PageContainer>
);
}
// 不再导出标签页管理器
// export { tabsManager };

View File

@ -0,0 +1,19 @@
import { ConfigProvider } from 'antd';
import { ReactNode } from 'react';
import { MyLoading } from './MyLoading';
export function MyRootContainer({ children }: { children: ReactNode }) {
return (
<ConfigProvider
// 输入框圆角
theme={{
token: {
borderRadius: 3,
},
}}
>
<MyLoading />
{/* <AliveScope>{children}</AliveScope> */}
{children}
</ConfigProvider>
);
}

View File

@ -0,0 +1,102 @@
import { useLocation } from '@umijs/max';
import { useEffect } from 'react';
// import { tabsManager } from './MyPageContainer'; // 不再导入tabsManager
export interface UsePageTabsOptions {
/** 标签页的唯一标识 */
tabKey: string;
/** 标签页显示的标题 */
tabLabel: string;
/** 是否可关闭默认为true */
closable?: boolean;
/** 是否自动添加到标签页默认为true */
autoAdd?: boolean;
}
/**
* Hook
*
*
*
* -
* -
* -
* - //
*/
export function usePageTabs(options?: UsePageTabsOptions) {
const location = useLocation();
const { tabKey, tabLabel, closable = true, autoAdd = true } = options || {};
// 不再自动添加标签页
useEffect(() => {
// 多标签页功能已取消,不再执行任何操作
}, [tabKey, tabLabel, closable, autoAdd, location.pathname, location.search]);
return {
/** 添加新标签页 - 多标签页功能已取消 */
addTab: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 在当前标签页右边添加新标签页 - 多标签页功能已取消 */
addTabNext: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 关闭指定标签页 - 多标签页功能已取消 */
removeTab: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 切换到指定标签页 - 多标签页功能已取消 */
switchTab: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 关闭其他标签页 - 多标签页功能已取消 */
closeOtherTabs: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 关闭左侧标签页 - 多标签页功能已取消 */
closeLeftTabs: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 关闭右侧标签页 - 多标签页功能已取消 */
closeRightTabs: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 刷新标签页 - 多标签页功能已取消 */
refreshTab: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 获取当前所有标签页 - 多标签页功能已取消 */
getTabs: () => [],
/** 获取当前激活的标签页key - 多标签页功能已取消 */
getActiveKey: () => '',
};
}
/**
* Hook -
*/
export function useTabsManager() {
return {
// 多标签页功能已取消,返回空对象和空函数
tabsManager: {},
addTab: () => {},
addTabNext: () => {},
removeTab: () => {},
setActiveKey: () => {},
closeOtherTabs: () => {},
closeLeftTabs: () => {},
closeRightTabs: () => {},
refreshTab: () => {},
getTabs: () => [],
getActiveKey: () => '',
};
}

View File

@ -0,0 +1,102 @@
import { useLocation } from '@umijs/max';
import { useEffect } from 'react';
// import { tabsManager } from './MyPageContainer'; // 不再导入tabsManager
export interface UsePageTabsOptions {
/** 标签页的唯一标识 */
tabKey: string;
/** 标签页显示的标题 */
tabLabel: string;
/** 是否可关闭默认为true */
closable?: boolean;
/** 是否自动添加到标签页默认为true */
autoAdd?: boolean;
}
/**
* Hook
*
*
*
* -
* -
* -
* - //
*/
export function usePageTabs(options?: UsePageTabsOptions) {
const location = useLocation();
const { tabKey, tabLabel, closable = true, autoAdd = true } = options || {};
// 不再自动添加标签页
useEffect(() => {
// 多标签页功能已取消,不再执行任何操作
}, [tabKey, tabLabel, closable, autoAdd, location.pathname, location.search]);
return {
/** 添加新标签页 - 多标签页功能已取消 */
addTab: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 在当前标签页右边添加新标签页 - 多标签页功能已取消 */
addTabNext: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 关闭指定标签页 - 多标签页功能已取消 */
removeTab: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 切换到指定标签页 - 多标签页功能已取消 */
switchTab: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 关闭其他标签页 - 多标签页功能已取消 */
closeOtherTabs: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 关闭左侧标签页 - 多标签页功能已取消 */
closeLeftTabs: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 关闭右侧标签页 - 多标签页功能已取消 */
closeRightTabs: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 刷新标签页 - 多标签页功能已取消 */
refreshTab: () => {
// 多标签页功能已取消,不再执行任何操作
},
/** 获取当前所有标签页 - 多标签页功能已取消 */
getTabs: () => [],
/** 获取当前激活的标签页key - 多标签页功能已取消 */
getActiveKey: () => '',
};
}
/**
* Hook -
*/
export function useTabsManager() {
return {
// 多标签页功能已取消,返回空对象和空函数
tabsManager: {},
addTab: () => {},
addTabNext: () => {},
removeTab: () => {},
setActiveKey: () => {},
closeOtherTabs: () => {},
closeLeftTabs: () => {},
closeRightTabs: () => {},
refreshTab: () => {},
getTabs: () => [],
getActiveKey: () => '',
};
}

View File

@ -0,0 +1,11 @@
import { DrawerProps } from 'antd';
export const MyDrawerProps = {
props: {
footer: null,
maskClosable: false,
placement: 'bottom',
width: '80%',
height: 'calc(100% - 50px)',
} as DrawerProps,
};

View File

@ -0,0 +1,13 @@
export const MyModalFormProps = {
props: {
layout: 'vertical',
labelAlign: 'left',
wrapperCol: { span: 20 },
style: { padding: '15px' },
layoutType: 'ModalForm',
grid: true,
modalProps: {
maskClosable: false,
},
} as any,
};

View File

@ -0,0 +1,53 @@
import { ParamsType, ProTableProps } from '@ant-design/pro-components';
import { SortOrder } from 'antd/es/table/interface';
export const MyProTableProps = {
props: {
scroll: { x: 'max-content', scrollToFirstRowOnChange: true },
//修改列表的样式
bordered: true,
size: 'middle',
rowKey: 'id',
pagination: {
showTotal: (total) => `总共${total}`,
showSizeChanger: true,
showQuickJumper: true,
},
search: {
defaultCollapsed: false,
},
} as ProTableProps<Record<string, any>, ParamsType, 'text'>,
request: async (
params: Record<string, any> & {
pageSize?: number;
current?: number;
keyword?: string;
},
sort: Record<string, SortOrder>,
api: (data?: any) => any,
setParams?: (params: any) => void,
setRes?: (res: any) => void,
// defaultParams?: Record<string, any>,
) => {
const sortKeys = Object.keys(sort);
const sorter =
sortKeys.length > 0 ? [sortKeys[0], sort[sortKeys[0]]] : undefined;
const { current, pageSize, ...rest } = params;
const body = {
page: current,
perPage: pageSize,
...rest,
sorter: sorter,
// ...defaultParams,
};
setParams?.(body);
const data = await api(body);
setRes?.(data);
return {
data: data.data,
success: data.success,
total: data.meta?.total,
};
},
};

View File

@ -0,0 +1,330 @@
import { MyResponseType, renderTextHelper } from '@/common';
import { ProColumns } from '@ant-design/pro-components';
import { Image, Popconfirm, Switch, Tag } from 'antd';
type ReturnType = ProColumns<Record<string, any>, 'text'>;
export const MyColumns = {
ID(props?: ReturnType): ReturnType {
// return { title: 'ID', dataIndex: 'id', hideInSearch: true, ...props };
return { title: 'ID', dataIndex: 'id', search: false, ...props };
},
DayStatus: (start: string, end: string) => {
const now = new Date();
const startDate = new Date(start);
const endDate = new Date(end);
// 判断当前时间与开始时间和结束时间的关系
if (now < startDate) {
return (
<Tag color="green" style={{ cursor: 'pointer' }}>
</Tag>
);
} else if (now > endDate) {
return (
<Tag color="default" style={{ cursor: 'pointer' }}>
</Tag>
);
} else {
return (
<Tag color="#f50" style={{ cursor: 'pointer' }}>
</Tag>
);
}
},
Images({ ...rest }: ReturnType): ReturnType {
return {
hideInSearch: true,
renderText: renderTextHelper.Images,
...rest,
};
},
SoftDelete({
onRestore,
onSoftDelete,
setPermissions,
...rest
}: {
onRestore?: (data: any) => Promise<MyResponseType>;
onSoftDelete?: (data: any) => Promise<MyResponseType>;
setPermissions?: any;
} & ReturnType): ReturnType {
return {
title: '启/禁用',
render: (_, item, index, action) => (
<Popconfirm
title={item?.deleted_at ? '启用' : '禁用'}
description={item?.deleted_at ? '您确认启用吗?' : '您确认禁用吗?'}
onConfirm={() => {
if (item?.deleted_at) {
onRestore?.({ id: item.id, is_enabled: 1 }).then(() =>
action?.reload(),
);
} else {
onSoftDelete?.({ id: item.id, is_enabled: 0 }).then(() =>
action?.reload(),
);
}
}}
okText="是"
cancelText="否"
>
{setPermissions?.length ? (
<Switch
disabled={!setPermissions?.length}
checked={!item?.deleted_at}
size="default"
/>
) : (
<Switch checked={!item?.deleted_at} size="default" />
)}
</Popconfirm>
),
search: false,
...rest,
};
},
ToggleEnabled({
onToggleEnabled,
setPermissions,
...rest
}: {
onToggleEnabled?: (data: any) => Promise<MyResponseType>;
onSoftDelete?: (data: any) => Promise<MyResponseType>;
setPermissions?: any;
} & ReturnType): ReturnType {
return {
title: '启/禁用',
render: (_, item, index, action) => (
<Popconfirm
title={!item?.is_enabled ? '启用' : '禁用'}
description={!item?.is_enabled ? '您确认启用吗?' : '您确认禁用吗?'}
onConfirm={() => {
onToggleEnabled?.({
id: item.id,
is_enabled: !item?.is_enabled,
}).then(() => action?.reload());
}}
okText="是"
cancelText="否"
>
{setPermissions?.length ? (
<Switch
disabled={!setPermissions?.length}
checked={item?.is_enabled}
size="default"
/>
) : (
<Switch checked={item?.is_enabled} size="default" />
)}
</Popconfirm>
),
search: false,
...rest,
};
},
IsEnabled({
onRestore,
onSoftDelete,
...rest
}: {
onRestore?: (data?: any) => Promise<MyResponseType>;
onSoftDelete?: (data?: any) => Promise<MyResponseType>;
} & ReturnType): ReturnType {
return {
title: '启/禁用',
render: (_, item, index, action) => (
<Popconfirm
title={!item?.is_enabled ? '启用' : '禁用'}
description={!item?.is_enabled ? '您确认启用吗?' : '您确认禁用吗?'}
onConfirm={() => {
if (!item?.is_enabled) {
onRestore?.({ id: item.id, is_enabled: 1 }).then(() =>
action?.reload(),
);
} else {
onSoftDelete?.({ id: item.id, is_enabled: 0 }).then(() =>
action?.reload(),
);
}
}}
okText="是"
cancelText="否"
>
<Switch checked={item?.is_enabled} size="default" />
</Popconfirm>
),
search: false,
...rest,
};
},
CreatedAt(props?: ReturnType): ReturnType {
return {
title: '创建时间',
dataIndex: 'created_at',
hideInSearch: true,
valueType: 'dateTime',
sorter: true,
align: 'right',
...props,
};
},
UpdatedAt(): ReturnType {
return {
title: '最近修改',
dataIndex: 'updated_at',
hideInSearch: true,
valueType: 'dateTime',
sorter: true,
align: 'right',
};
},
FinishedAt(): ReturnType {
return {
title: '完成时间',
dataIndex: 'finished_at',
hideInSearch: true,
valueType: 'dateTime',
sorter: true,
align: 'right',
};
},
Boolean({ label, ...rest }: { label?: string[] } & ReturnType): ReturnType {
const option: { value: boolean; label: string; color: string }[] = [
{ value: false, label: label?.[0] ?? '否', color: 'gray' },
{ value: true, label: label?.[1] ?? '是', color: 'green' },
];
return {
align: 'center',
request: async () => option,
renderText(text: boolean) {
console.log(Boolean(text), 'text');
const item = option.find((item) => item.value === Boolean(text));
return <Tag color={item?.color}>{item?.label}</Tag>;
},
...rest,
};
},
DeletedAt({ label, ...rest }: { label?: string[] } & ReturnType): ReturnType {
const option: { value: boolean; label: string; color: string }[] = [
{ value: false, label: label?.[0] ?? '禁用', color: 'gray' },
{ value: true, label: label?.[1] ?? '启用', color: 'green' },
];
return {
align: 'center',
request: async () => option,
renderText(text: string) {
console.log(text, 'text');
const item = text ? option[0] : option[1];
return <Tag color={item?.color}>{item?.label}</Tag>;
},
...rest,
};
},
YesOrNo({
yes = '已',
no = '未',
...rest
}: {
yes?: string;
no?: string;
} & ReturnType): ReturnType {
return {
align: 'center',
renderText(text) {
return (
<Tag bordered={false} color={text ? 'processing' : 'error'}>
{text ? yes : no}
</Tag>
);
},
...rest,
};
},
EnumTag({ ...rest }: ReturnType): ReturnType {
return {
align: 'left',
renderText(text: any) {
const _enum: any = rest?.valueEnum ?? {};
if (!_enum) return <>-</>;
const item = _enum[text] ?? undefined;
if (!item) return <>-</>;
// return <Tag style={{ color: item?.color }}>{item.text}</Tag>;
//颜色底,白字
return <Tag color={item?.color}>{item.text}</Tag>;
//灰底,颜色字
// return <Tag style={{ backgroundColor: item?.color }}>{item.text}</Tag>;
//灰底,黑字
// return <Tag style={{ color: '#333' }}>{item.text}</Tag>;
//修改列表的标签样式
},
...rest,
};
},
Option({ ...rest }: ReturnType): ReturnType {
return {
title: '操作',
valueType: 'option',
align: 'right',
fixed: 'right',
...rest,
};
},
Token({ ...rest }) {
return {
title: 'Token',
renderText(text: string) {
return <Tag color="blue">{text}</Tag>;
},
...rest,
};
},
Image({ ...rest }) {
return {
search: false,
renderText(text: { url: string }[]) {
return <Image src={text[0]?.url} width={50} height={50} />;
},
...rest,
};
},
Ffdefault({
Ffdefault,
dataIndex,
...rest
}: {
Ffdefault: (data: any) => Promise<MyResponseType>;
dataIndex: any;
} & ReturnType): ReturnType {
return {
title: '是否默认',
renderText(text: boolean, record: any, index, action: any) {
let form: any = { id: record.id };
form[dataIndex] = record[dataIndex] ? 0 : 1;
return (
<Popconfirm
title="提示"
description="您确认要修改默认吗?"
onConfirm={() => {
Ffdefault?.(form).then(() => action?.reload());
}}
okText="是"
cancelText="否"
>
<Tag
color={record[dataIndex] ? 'green' : 'gray'}
style={{ cursor: 'pointer' }}
>
{record[dataIndex] ? '是' : '否'}
</Tag>
</Popconfirm>
);
},
...rest,
};
},
};

View File

@ -0,0 +1,184 @@
import {
MyColorPicker,
MyIconSelect,
MyMoneyInput,
MyPercentInput,
MyUploadImages,
rulesHelper,
} from '@/common';
import { ProFormColumnsType } from '@ant-design/pro-components';
// import { MyRichText } from '../components/FormFields/MyRichText';
type ReturnType = ProFormColumnsType<any, 'text'>;
type PropsType = { required?: boolean } & ReturnType;
export const MyFormItems = {
Text(props: PropsType): ReturnType {
return {
formItemProps: {
...(props?.required ? rulesHelper.text : {}),
},
...props,
};
},
Integer(props: { min?: number } & PropsType): ReturnType {
return {
valueType: 'digit',
formItemProps: {
...(props?.required ? rulesHelper.number : {}),
},
fieldProps: {
min: props.min ?? 1,
},
...props,
};
},
Numeric(props: PropsType): ReturnType {
return {
valueType: 'digit',
formItemProps: {
...(props?.required ? rulesHelper.number : {}),
},
fieldProps: {
precision: 2,
min: 0,
},
...props,
};
},
UploadImages({
max = 1,
help,
uploadType = 'image',
accept = '*',
...rest
}: {
max?: number;
help?: string;
accept?: string;
uploadType?: 'image' | 'file' | 'video' | 'audio' | undefined;
} & PropsType): ReturnType {
return {
renderFormItem() {
return (
<MyUploadImages max={max} accept={accept} uploadType={uploadType} />
);
},
formItemProps: {
help,
...(rest.required ?? false ? rulesHelper.upload({ max }) : {}),
},
...rest,
};
},
EnumRadio(props: PropsType): ReturnType {
return {
valueType: 'radioButton',
proFieldProps: {
placeholder: `请选择${props.title}`,
},
formItemProps: {
...(props?.required ? rulesHelper.text : {}),
},
fieldProps: {
buttonStyle: 'solid',
},
...props,
};
},
EnumCheckbox(props: PropsType): ReturnType {
return {
valueType: 'checkbox',
proFieldProps: {
placeholder: `请选择${props.title}`,
},
formItemProps: {
...(props?.required ? rulesHelper.array : {}),
},
...props,
};
},
EnumSelect(props: PropsType): ReturnType {
return {
valueType: 'select',
proFieldProps: {
placeholder: `请选择`,
},
formItemProps: {
...(props?.required ? rulesHelper.text : {}),
},
...props,
};
},
Switch(props: PropsType): ReturnType {
return {
valueType: 'switch',
initialValue: props.initialValue ?? false,
...props,
};
},
DatePicker(props: PropsType): ReturnType {
return {
valueType: 'date',
formItemProps: {
...(props?.required ? rulesHelper.text : {}),
},
...props,
};
},
Select(props: PropsType): ReturnType {
return {
valueType: 'select',
proFieldProps: {
placeholder: `请选择${props.title}`,
},
...props,
};
},
// EnumSelect(props: PropsType): ReturnType {
// const valueType = props.valueType ?? 'radioButton';
// const buttonStyle =
// valueType === 'radioButton'
// ? {
// buttonStyle: 'solid',
// }
// : {};
// },
IconSelect(props?: PropsType): ReturnType {
return {
key: 'icon',
title: '图标',
renderFormItem: () => <MyIconSelect />,
...props,
};
},
ColorPicker(props?: PropsType): ReturnType {
return {
key: 'color',
dataIndex: 'color',
title: '颜色',
renderFormItem: () => <MyColorPicker />,
...props,
};
},
Percent(props?: PropsType): ReturnType {
return {
renderFormItem: () => <MyPercentInput />,
formItemProps: {
...(props?.required ? rulesHelper.number : {}),
},
...props,
};
},
Money(props?: PropsType): ReturnType {
return {
renderFormItem: () => <MyMoneyInput />,
formItemProps: {
...(props?.required ? rulesHelper.number : {}),
},
...props,
};
},
};

45
src/common/index.tsx Normal file
View File

@ -0,0 +1,45 @@
// components
export * from './components/formFields/MyColorPicker';
export * from './components/formFields/MyIconSelect';
export * from './components/formFields/MyMoneyInput';
export * from './components/formFields/MyPercentInput';
export * from './components/formFields/MyTreeCheckable';
export * from './components/formFields/MyUploadImages';
export * from './components/layout/MyCommonModal';
export * from './components/layout/MyImportModal';
export * from './components/layout/MyPageContainer';
export * from './components/layout/MyRootContainer';
export * from './components/layout/usePageTabs';
export * from './components/props/MyDrawerProps';
export * from './components/props/MyModalFormProps';
export * from './components/props/MyProTableProps';
export * from './components/schema/MyColumns';
export * from './components/schema/MyFormItems';
export * from './components/MyButtons';
export * from './components/MyIcons';
export * from './components/MyStatistics';
// libs
export * from './libs/umi/layoutConfig';
export * from './libs/umi/requestConfig';
export * from './libs/valtio/actions';
export * from './libs/valtio/state';
// pages
export * from './pages/MyLoginPage';
// utils
export * from './utils/renderTextHelper';
export * from './utils/rulesHelper';
// typings
export * from './typings.d';
//按钮权限
export * from './components/GetRPermission';

View File

@ -0,0 +1,5 @@
import { ReactNode } from 'react';
export default function MyAccess({ children }: { children: ReactNode }) {
return <div>MyAccess</div>;
}

View File

@ -0,0 +1,13 @@
.quick_link {
font-size: 14;
color: #448ef7;
// background-color: #efefef;
font-weight: 500;
// padding: 5px 8px;
border-radius: 6px;
// border: 1px solid #448ef7;
}
.headerContentRender {
display: flex;
justify-content: space-between;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

View File

@ -0,0 +1,313 @@
import { MyIcons, MyIconsType, PermissionsType, useMyState } from '@/common';
import AvatarProps from '@/common/components/layout/AvatarProps';
import {
BellOutlined,
SettingOutlined,
TabletOutlined,
} from '@ant-design/icons';
import { Link, RuntimeConfig, history, useNavigate } from '@umijs/max';
import {
AutoComplete,
Button,
Image,
Input,
Menu,
MenuProps,
Popover,
Space,
} from 'antd';
import { useState } from 'react';
import './allConfig.scss';
import ImgCustomerWxApp from './customer_wx_app.jpg';
import ImgEmployeeWxApp from './employee_wx_app.jpg';
// import Logo from './logo.png';
interface LevelKeysProps {
key?: string;
children?: LevelKeysProps[];
}
const loopMenu = (permissions: PermissionsType[] | undefined) => {
let tree: PermissionsType[] = [];
let map: Record<number, PermissionsType> = {};
// 过滤掉Button和Tab类型的权限这些不应该显示在菜单中
const menuPermissions = permissions?.filter(
(p) => p.type !== 'Button' && p.type !== 'Tab',
);
menuPermissions?.forEach((permission) => {
map[permission.id] = {
path: permission.path,
name: permission.name,
icon: permission.icon && MyIcons[permission.icon as MyIconsType],
label: permission.name,
key: permission.path || permission.id.toString(),
hideInMenu: false, // 已经过滤过类型这里可以设置为false
};
});
menuPermissions?.forEach((permission) => {
let node = map[permission.id];
const parentId = permission?.parent_id;
if (parentId !== null && parentId !== undefined) {
const parentNode = map[parentId];
if (parentNode) {
// 初始化 children 如果不存在
if (!Array.isArray(parentNode.children)) {
parentNode.children = [];
}
parentNode.children.push(node);
} else {
// 父节点不存在,作为根节点处理
console.warn(
`Parent node with id ${parentId} not found for permission ${permission.id}`,
);
tree.push(node);
}
} else {
tree.push(node);
}
});
return tree?.[0]?.children;
};
export const LayoutConfig: RuntimeConfig['layout'] = () => {
const { snap } = useMyState();
const navigate = useNavigate();
const permissionsList = (snap.session.permissions || [])
.filter((p: any) => p.type !== 'Button' && p.path)
.sort((a: any, b: any) => a._lft - b._lft)
.map((p: any) => ({ value: p.path, label: p.name }));
const quickLinks = [
{ label: '工单Bi', path: '/work_order/work_bi' },
{ label: '合同Bi', path: '/contract/contracts_bi' },
{ label: '收费Bi', path: '/charge/charge_bi' },
{ label: '项目Bi', path: '/asset/asset_bi' },
];
const [stateOpenKeys, setStateOpenKeys] = useState(['2', '23']);
const getLevelKeys: any = (items1: LevelKeysProps[]) => {
const key: Record<string, number> = {};
const func = (items2: LevelKeysProps[], level = 1) => {
console.log(items2, 'level');
items2?.forEach((item) => {
if (item.key) {
key[item.key] = level;
}
if (item.children) {
func(item.children, level + 1);
}
});
};
func(items1);
return key;
};
return {
title: '',
// 首页 logo
logo: (
<div style={{ width: 181 }}>
{/* <img src={Logo} style={{ height: '42px' }} /> */}
</div>
),
layout: 'mix',
siderWidth: 180,
colorPrimary: '#1890ff',
pure: history.location.pathname === '/login',
avatarProps: {
render: () => <AvatarProps user={snap.session.user} />,
},
headerContentRender: () => (
<div className="headerContentRender">
<div style={{ display: 'flex', alignItems: 'center', gap: 20 }}>
<HeaderSearch permissionsList={permissionsList} />
<Space size={20} style={{ color: '#666' }}>
:
{quickLinks.map((q) => (
<a
key={q.path}
onClick={() => history.push(q.path)}
className="quick_link"
>
{q.label}
</a>
))}
</Space>
</div>
<Space size={10}>
<Popover
placement="bottom"
title="小程序二维码"
content={
<Space style={{ textAlign: 'center' }} size="large">
<div>
<Image src={ImgEmployeeWxApp} style={{ height: '120px' }} />
<div style={{ marginTop: 10 }}></div>
</div>
<div>
<Image src={ImgCustomerWxApp} style={{ height: '120px' }} />
<div style={{ marginTop: 10 }}></div>
</div>
</Space>
}
>
<Button type="default" shape="circle" icon={<TabletOutlined />} />
</Popover>
<Button type="default" shape="circle" icon={<BellOutlined />} />
<Button
type="default"
shape="circle"
icon={<SettingOutlined />}
onClick={() => history.push('/system/sys_permissions')}
/>
</Space>
</div>
),
//水印设置
waterMarkProps: {
content: snap.session.user?.username,
},
defaultCollapsed: true,
collapsedButtonRender: false,
token: {
bgLayout: '#f6f6f6',
// header: {
// colorBgHeader: '#1B1F3B',
// colorHeaderTitle: '#f8f8f8',
// colorTextRightActionsItem: '#FFF',
// heightLayoutHeader: 50,
// },
//菜单背景色
sider: {
colorMenuBackground: '#fff',
colorTextMenuSelected: '#1890ff', // 菜单激活项字体颜色设置为蓝色
},
},
// 上下菜单
menuItemRender: (item, dom) => <Link to={item.path || '/'}>{dom}</Link>,
//点击目录,收起其他菜单
menuCollapse: true,
// //左右菜单
menuRender: () => {
let objjs: any = [];
snap.session.permissions?.forEach((res: any) => {
objjs.push(res);
});
let data = objjs.sort((a: any, b: any) => {
return a._lft - b._lft;
});
const menus = loopMenu(data);
const levelKeys: any = getLevelKeys(menus as LevelKeysProps[]);
const onOpenChange: MenuProps['onOpenChange'] = (openKeys) => {
const currentOpenKey = openKeys.find(
(key) => !stateOpenKeys.includes(key),
);
// open
if (currentOpenKey !== undefined) {
const repeatIndex = openKeys
.filter((key) => key !== currentOpenKey)
.findIndex((key) => levelKeys[key] === levelKeys[currentOpenKey]);
setStateOpenKeys(
openKeys
// remove repeat key
.filter((_, index) => index !== repeatIndex)
// remove current level all child
.filter((key) => levelKeys[key] <= levelKeys[currentOpenKey]),
);
} else {
// close
setStateOpenKeys(openKeys);
}
};
return (
<div
style={{
position: 'relative',
width: 180,
top: 0,
left: 0,
bottom: 0,
height: '100vh',
zIndex: 100,
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.1)',
}}
>
<div
style={{
position: 'fixed',
paddingTop: 60,
top: 0,
left: 0,
bottom: 0,
backgroundColor: '#fff',
overflowY: 'auto',
}}
>
<Menu
style={{ width: '165px' }}
mode="inline"
defaultSelectedKeys={[history.location.pathname]}
theme="light"
items={menus}
openKeys={stateOpenKeys}
onOpenChange={onOpenChange}
onClick={({ key }) => {
sessionStorage.setItem('breadcrumbs', '');
navigate(key);
console.log(key, 'key2');
}}
onSelect={({ key }) => {
console.log(key, 'key');
}}
/>
</div>
</div>
);
},
menuMode: 'inline',
menu: {
params: snap.session.permissions,
mode: 'inline',
request: async () => {
let objjs: any = [];
snap.session.permissions?.forEach((res: any) => {
objjs.push(res);
});
let data = objjs.sort((a: any, b: any) => {
return a._lft - b._lft;
});
const menus = loopMenu(data);
return Promise.resolve(menus);
},
},
// unAccessible: <div>unAccessible</div>,
};
};
const HeaderSearch = ({ permissionsList }: { permissionsList: any[] }) => {
const [value, setValue] = useState<string>('');
return (
<AutoComplete
value={value}
options={permissionsList}
style={{ width: 280 }}
placeholder="~输入关键字,搜索系统功能"
filterOption={(inputValue, option) =>
(option?.label as string)
?.toLowerCase()
.includes(inputValue.toLowerCase())
}
onChange={(v) => setValue(v)}
onSelect={(v) => {
setValue('');
history.push(v as string);
}}
>
:
<Input allowClear size="middle" />
</AutoComplete>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View File

@ -0,0 +1,87 @@
import type { RequestConfig } from '@umijs/max';
import { message } from 'antd';
import { history } from 'umi';
import { stateActions } from '../valtio/actions';
import { state } from '../valtio/state';
const downloadFile = (disposition: string, data: any) => {
const blob = new Blob([data]);
const start = "filename*=utf-8''";
let fileName = '';
let dis = disposition.replace('UTF-8', 'utf-8');
// console.log(dis, 'dis');
if (dis.includes(start)) {
fileName = dis.substr(dis.indexOf(start) + start.length);
fileName = decodeURI(fileName);
}
if ('download' in document.createElement('a')) {
// 非IE下载
const elink: any = document.createElement('a');
elink.download = fileName;
elink.style.display = 'none';
elink.href = URL.createObjectURL(blob);
document.body.appendChild(elink);
elink.click();
URL.revokeObjectURL(elink.href); // 释放URL 对象
document.body.removeChild(elink);
} else {
// IE10+下载
// navigator.msSaveBlob(blob, fileName);
}
};
export const requestConfig: RequestConfig = {
baseURL: '/api/',
timeout: 1000 * 60,
method: 'POST',
errorConfig: {
errorThrower: (res) => {
console.log('errorThrower', res);
},
// 错误接收及处理
errorHandler: (error: any) => {
if (error) {
message.error(error.errorMessage);
switch (error.errorCode) {
case 10000:
if (history.location.pathname !== '/login') history.push('/login');
break;
}
}
},
},
// 请求拦截器
requestInterceptors: [
(url: string, options: any) => {
stateActions.addLoading();
return {
url: url,
options: {
...options,
headers: {
authorization: 'Bearer ' + state.storage.access_token,
},
},
};
},
],
// 响应拦截器
responseInterceptors: [
(response: any) => {
stateActions.subLoading();
const { headers } = response;
// console.log(headers['content-disposition'], 'response');
if (headers['content-disposition']) {
downloadFile(headers['content-disposition'], response.data);
return false;
}
const { data = {} as any } = response;
if (!data.success && data.type !== 'application/vnd.ms-excel') {
return Promise.reject(response.data);
}
return response;
},
],
};

View File

@ -0,0 +1,50 @@
import { MyResponseType } from '@/common';
import { Apis } from '@/gen/Apis';
import { state } from './state';
export const stateActions = {
addLoading() {
state.session.loading++;
},
subLoading() {
state.session.loading--;
},
setReady(val: boolean) {
state.session.ready = val;
},
setLogin(res: MyResponseType) {
state.session.user = res.data.user;
state.session.campus = res.data.campus;
state.session.permissions = res.data.permissions;
if (res.data?.token?.access_token)
state.storage.access_token = res.data?.token?.access_token;
// 解析apis
const apiKeys: string[] = [];
res.data.permissions.forEach((permission: any) => {
if (permission.key) {
apiKeys.push(permission.key);
}
});
// console.log('apis', apis);
state.session.apiKeys = apiKeys;
},
setLogout() {
state.session.user = undefined;
state.session.campus = undefined;
state.session.permissions = undefined;
state.storage.access_token = undefined;
},
me: async () => {
const res = await Apis.Common.Auth.Me();
if (res.success) {
stateActions.setLogin(res);
return {
name: state.session.user.username,
apiKeys: state.session.apiKeys,
// permissions: res.data.permissions,
};
} else {
return { name: '未登录', apiKeys: [] };
}
},
};

View File

@ -0,0 +1,60 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio';
function proxyWithPersistant<V>(
val: V,
opts: {
key: string;
},
) {
const local = localStorage.getItem(opts.key);
const state = proxy(local ? JSON.parse(local) : val);
subscribe(state, () => {
localStorage.setItem(opts.key, JSON.stringify(snapshot(state)));
});
return state;
}
type StorageType = {
access_token: string | undefined;
};
const storage: StorageType = proxyWithPersistant(
{
access_token: undefined,
},
{
key: process.env.TOKEN_NAME as string,
},
);
type SessionType = {
ready: boolean;
user?: any;
campus?: {
id: number;
name: string;
};
permissions?: any;
apiKeys: string[];
loading: number;
};
const session: SessionType = proxy({
ready: false,
user: undefined,
campus: undefined,
permissions: undefined,
apiKeys: [],
loading: 0,
});
export const state = proxy({
storage,
session,
});
export function useMyState() {
const snap = useSnapshot(state);
return { snap };
}

View File

@ -0,0 +1,36 @@
import { ModalForm, ProFormText } from '@ant-design/pro-components';
import { message } from 'antd';
export default function ForgotPasswordModal({
open,
onCancel,
}: {
open: boolean;
onCancel: () => void;
}) {
return (
<ModalForm
open={open}
wrapperCol={{ span: 24 }}
width="500px"
title="忘记密码"
onFinish={async (values) => {
// 在此处调用发送重置密码邮件或短信的API
// Apis.Common.Auth.ForgotPassword(values).then(() => {
// message.success('重置密码邮件已发送,请查收');
// onCancel();
// }).catch(() => false);
message.success('模拟发送成功,请检查您的邮箱或手机');
onCancel();
return Promise.resolve(true);
}}
modalProps={{
onCancel: onCancel,
}}
>
<ProFormText name="email" label="注册邮箱" required />
{/* 或者手机号 */}
{/* <ProFormText name="phone" label="注册手机号" required /> */}
</ModalForm>
);
}

View File

@ -0,0 +1,151 @@
import { Apis } from '@/gen/Apis';
import {
FieldTimeOutlined,
LockOutlined,
UserOutlined,
} from '@ant-design/icons';
import {
LoginFormPage,
ProConfigProvider,
ProFormText,
} from '@ant-design/pro-components';
import { theme } from 'antd';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { stateActions } from '..';
import gcLogo from './gclogo.png';
import logingBg from './loginBgImg.jpg';
export function MyLoginPage() {
const navigate = useNavigate();
const [getCaptcha, setCaptcha] = useState<any>({});
const { token } = theme.useToken();
const methods = {
getCaptcha: () => {
Apis.Common.Auth.Captcha().then(async (res) => {
setCaptcha(res?.data);
console.log(res, 'res');
});
},
};
useEffect(() => {
methods?.getCaptcha();
}, []);
return (
<ProConfigProvider>
<div
style={{
// backgroundColor: '#f8f8f8',
height: '100vh',
position: 'relative',
}}
>
<LoginFormPage<ApiTypes.Common.Auth.Login>
title={
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<img src={gcLogo} style={{ height: 30, width: 'auto' }} />
<span style={{ color: '#b1b1b1ff', fontSize: 20 }}>|</span>
<span style={{ color: token.colorTextBase, fontSize: 21 }}>
</span>
</div>
}
backgroundImageUrl={logingBg}
subTitle=" "
submitter={{ searchConfig: { submitText: '登录' } }}
onFinish={async (values: any) => {
Apis.Common.Auth.Login({
...values,
...{ captcha_key: getCaptcha?.key },
})
.then(async (res) => {
await stateActions.setLogin(res);
navigate('/');
})
.catch((err: any) => {
methods?.getCaptcha();
console.log(err, 'rr');
});
}}
>
<>
<ProFormText
name="username"
fieldProps={{
size: 'large',
prefix: <UserOutlined className={'prefixIcon'} />,
}}
placeholder={'请输入手机号码'}
rules={[
{
required: true,
message: '请输入手机号码!',
},
]}
/>
<ProFormText.Password
name="password"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={'prefixIcon'} />,
}}
placeholder={'请输入密码'}
rules={[
{
required: true,
message: '请输入密码!',
},
]}
/>
</>
<div style={{ display: 'flex' }}>
<ProFormText
name="captcha"
fieldProps={{
size: 'large',
prefix: <FieldTimeOutlined className={'prefixIcon'} />,
}}
placeholder={'验证码'}
rules={[
{
required: true,
message: '请输入验证码!',
},
]}
/>
<div
style={{
height: '40px',
border: '1px solid #eee',
padding: '0 4px',
marginLeft: 8,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '3px',
cursor: 'pointer',
}}
onClick={() => {
methods?.getCaptcha();
}}
>
<img height={32} width={100} src={getCaptcha?.img} />
</div>
</div>
{/* <div
style={{
display: 'flex',
justifyContent: 'space-between',
// marginTop: 12,
}}
>
<ProFormCheckbox name="autoLogin"></ProFormCheckbox>
<a></a>
</div> */}
</LoginFormPage>
</div>
</ProConfigProvider>
);
}

BIN
src/common/pages/gclogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

72
src/common/typings.d.ts vendored Normal file
View File

@ -0,0 +1,72 @@
import { ProColumns, ProFormColumnsType } from '@ant-design/pro-components';
type MyColumnsType =
| ProFormColumnsType<any, 'text'>
| ProColumns<any, FormFieldType | 'text' | any>;
type MyResponseType = {
success?: boolean;
data?: any;
errorCode?: number;
errorMessage?: string;
showType?: ErrorShowType;
meta?: MyPaginationMetaType;
is_enabled?: number;
};
type MyPaginationMetaType = {
current_page: number;
last_page: number;
per_page: number;
total: number;
};
type MyParamsType<T> = {
q: T;
p?: { page: number; perPage: number };
t?: { sorter: { sort: string; order: string } };
};
type PermissionsType = MenuDataItem & {
parent_id: number;
};
type MyModalRefType = {
showModal: (data: MyModalProps) => void;
hideModal: () => void;
};
// export type MyModalFormProps = {
// refresh: () => void;
// item?: Record<string, any>;
// title: string;
// } & ModalFormProps;
type MyProFormFieldProps<T> = {
value?: T;
onChange?: (value: T) => void;
};
// type MyEnumItemProps = {
// label: string;
// value: string | number;
// color: string;
// textColor: string;
// };
export type MyBetaModalFormProps = {
title?: string;
// action: ActionType | undefined;
reload?: () => void;
item?: Record<string, any>;
extra?: React.ReactNode;
onOpenChange?: (open: boolean) => void;
open?: boolean;
};
export type MyProEnumItemProps = {
[key: string]: {
text: string;
color?: string;
};
};

13
src/common/utils/day.ts Normal file
View File

@ -0,0 +1,13 @@
import dayjs from 'dayjs';
export const isInTimeRange = (startTime?: string, endTime?: string) => {
const now = dayjs();
const start = dayjs(startTime);
const end = dayjs(endTime);
return now.isAfter(start) && now.isBefore(end);
};
export function showTime(time?: string, num?: number) {
return time?.substring(0, num || 5) || '';
}

View File

@ -0,0 +1,341 @@
// export function flattenToMultiLevelFormatWithRowSpanAdvanced(data) {
// const result = [];
// const rowSpanMap = new Map();
// const firstIndexMap = new Map();
// // 预处理:构建完整的层级关系
// const organizeButtonsToPages = (nodes) => {
// // 第一步收集所有节点建立ID到节点的映射
// const allNodes = new Map();
// const collectAllNodes = (nodeList, parentPath = []) => {
// nodeList.forEach((node) => {
// // 构建当前节点的完整路径
// const currentPath = [
// ...parentPath,
// {
// id: node.id,
// name: node.name,
// type: node.type,
// icon: node.icon,
// },
// ];
// // 存储节点信息,包含层级路径
// allNodes.set(node.id, {
// ...node,
// path: currentPath,
// level: currentPath.length,
// });
// if (node.children && node.children.length > 0) {
// collectAllNodes(node.children, currentPath);
// }
// });
// };
// collectAllNodes(nodes);
// // 第二步:为每个节点构建完整的层级字段
// const processedNodes = Array.from(allNodes.values()).map((node) => {
// const processedNode = { ...node };
// // 添加层级字段 (name1, name2, name3, id1, id2, id3, parent_id1, parent_id2, ...)
// node.path.forEach((pathNode, index) => {
// const level = index + 1;
// processedNode[`name${level}`] = pathNode.name;
// processedNode[`id${level}`] = pathNode.id;
// processedNode[`type${level}`] = pathNode.type;
// processedNode[`icon${level}`] = pathNode.icon;
// // 添加父级ID字段
// if (index > 0) {
// processedNode[`parent_id${level}`] = node.path[index - 1].id;
// }
// });
// return processedNode;
// });
// // 第三步根据parent_id构建children关系
// const buildHierarchy = (nodeList) => {
// const nodesById = new Map();
// const rootNodes = [];
// // 建立ID到节点的映射
// nodeList.forEach((node) => {
// nodesById.set(node.id, { ...node, children: [] });
// });
// // 构建父子关系
// nodeList.forEach((node) => {
// if (node.parent_id) {
// const parentNode = nodesById.get(node.parent_id);
// if (parentNode) {
// parentNode.children.push(nodesById.get(node.id));
// } else {
// // 如果没有找到父节点,作为根节点
// rootNodes.push(nodesById.get(node.id));
// }
// } else {
// // 没有parent_id的作为根节点
// rootNodes.push(nodesById.get(node.id));
// }
// });
// return rootNodes;
// };
// return buildHierarchy(processedNodes);
// };
// // 预处理数据
// const processedData = organizeButtonsToPages(data);
// console.log('预处理后的数据:', processedData);
// // 计算每个节点的叶子节点数量(按钮也算作叶子节点)
// const calculateChildrenCount = (node) => {
// let leafCount = 0;
// if (node.children && node.children.length > 0) {
// node.children.forEach((child) => {
// if (
// child.type === 'Button' ||
// !child.children ||
// child.children.length === 0
// ) {
// leafCount += 1;
// } else {
// leafCount += calculateChildrenCount(child);
// }
// });
// }
// rowSpanMap.set(node.id, leafCount);
// return leafCount;
// };
// // 计算所有节点
// processedData.forEach((root) => {
// calculateChildrenCount(root);
// });
// console.log('跨行配置:', rowSpanMap);
// // 扁平化并记录首次出现索引
// const traverse = (node, currentPath = []) => {
// const newPath = [
// ...currentPath,
// {
// name: node.name,
// id: node.id,
// type: node.type,
// icon: node.icon,
// },
// ];
// // 记录每个节点第一次出现的索引
// newPath.forEach((pathNode) => {
// if (!firstIndexMap.has(pathNode.id)) {
// firstIndexMap.set(pathNode.id, result.length);
// }
// });
// // 生成数据的条件:叶子节点 或 按钮
// const shouldGenerateData =
// !node.children || node.children.length === 0 || node.type === 'Button';
// if (shouldGenerateData) {
// const item = {
// id: node.id,
// name: node.name,
// guard_name: node.guard_name,
// created_at: node.created_at,
// updated_at: node.updated_at,
// key: node.key,
// icon: node.icon,
// type: node.type,
// backend_apis: node.backend_apis,
// path: node.path,
// _lft: node._lft,
// _rgt: node._rgt,
// parent_id: node.parent_id,
// level: newPath.length,
// is_leaf: true,
// // 跨行配置
// row_spans: {},
// };
// // 添加层级字段和跨行信息
// newPath.forEach((pathNode, index) => {
// const level = index + 1;
// item[`name${level}`] = pathNode.name;
// item[`id${level}`] = pathNode.id;
// item[`type${level}`] = pathNode.type;
// item[`icon${level}`] = pathNode.icon;
// // 存储跨行信息
// item.row_spans[`row_span${level}`] = rowSpanMap.get(pathNode.id) || 0;
// item.row_spans[`first_index${level}`] = firstIndexMap.get(pathNode.id);
// });
// // 添加父级ID字段
// for (let i = 2; i <= newPath.length; i++) {
// item[`parent_id${i}`] = newPath[i - 2].id;
// }
// result.push(item);
// }
// // 递归处理子节点(按钮不继续递归)
// if (node.children && node.children.length > 0 && node.type !== 'Button') {
// node.children.forEach((child) => {
// traverse(child, newPath);
// });
// }
// };
// processedData.forEach((item) => {
// traverse(item, []);
// });
// console.log('最终处理后的数据:', result);
// return result;
// }
// export function flattenToMultiLevelFormatWithRowSpanAdvancedNew(data: any[]) {
// let result: any = [];
// let length = 0;
// data?.forEach((res: any, idx: number) => {
// let row: any = {};
// res?.children?.forEach((child: any, index: number) => {
// row.id = `${res?.id}_${child.id}`;
// row.name2 = child.name;
// row.path2 = child.path;
// row.icon2 = child.icon;
// row.id1 = res.id;
// row.id2 = child.id;
// if (child.children?.length) {
// row.buttonList = child.children;
// child.children?.forEach((child2: any) => {
// if (child2?.children?.length) {
// row.buttonList = [...row.buttonList, ...child2?.children];
// }
// });
// } else {
// row.buttonList = null;
// }
// row.row_spans = {
// rowSpan: !index ? res?.children?.length : 0,
// firstIndex: idx > 0 && index === 0 ? length : 0,
// };
// result.push({ name: res?.name, path: res?.path, ...row });
// });
// length += res?.children?.length;
// });
// console.log('最终处理后的数据:', result);
// return result;
// }
export function flattenToMultiLevelFormatWithRowSpanAdvancedNew(data: any) {
if (!data || !Array.isArray(data)) {
console.log('数据为空或不是数组');
return [];
}
let result: any[] = [];
let totalRows = 0; // 记录已处理的总行数
data.forEach((res: any, parentIndex: number) => {
// 检查父级数据是否有效
if (!res || typeof res !== 'object') {
console.warn(`${parentIndex}个父级数据无效,跳过处理`);
return;
}
const parentId = res?.id || `parent_${parentIndex}`;
const parentName = res?.name || '';
const parentPath = res?.path || '';
// 获取子级数据
const children = res?.children || [];
const childrenLength = children.length;
// 情况1: 如果没有子级,直接添加父级信息到结果
if (!childrenLength) {
const row = {
id: parentId,
name: parentName,
path: parentPath,
id1: parentId,
buttonList: null,
row_spans: {
rowSpan: 1, // 没有子级,父级占一行
firstIndex: parentIndex > 0 ? totalRows : 0,
},
};
result.push(row);
totalRows += 1; // 父级占据一行
return;
}
// 情况2: 有子级,为每个子级创建一行
children.forEach((child: any, childIndex: number) => {
// 检查子级数据是否有效
if (!child || typeof child !== 'object') {
console.warn(`父级${parentId}的第${childIndex}个子级数据无效,跳过`);
return;
}
const childId = child?.id || `${parentId}_child_${childIndex}`;
// 收集所有按钮(第三级和更深层级的子级)
let buttonList: any[] = [];
// 处理直接子级的children第三级
if (Array.isArray(child.children) && child.children.length > 0) {
child.children.forEach((child2: any) => {
if (child2 && typeof child2 === 'object') {
// 添加第三级
buttonList.push(child2);
// 处理第四级及更深层级
if (Array.isArray(child2.children) && child2.children.length > 0) {
buttonList = buttonList.concat(
child2.children.filter(
(item: any) => item && typeof item === 'object',
),
);
}
}
});
}
// 创建行数据
const row: any = {
id: `${parentId}_${childId}`,
name: parentName, // 父级名称
path: parentPath, // 父级路径
name2: child.name || '', // 子级名称
path2: child.path || '', // 子级路径
icon2: child.icon || null, // 子级图标
id1: parentId, // 父级ID
id2: childId, // 子级ID
buttonList: buttonList.length > 0 ? buttonList : null, // 按钮列表
row_spans: {
// 只有第一个子级显示rowSpan其他子级为0
rowSpan: childIndex === 0 ? childrenLength : 0,
// 只有第一个子级需要设置firstIndex
firstIndex: parentIndex > 0 && childIndex === 0 ? totalRows : 0,
},
};
result.push(row);
});
totalRows += childrenLength; // 更新总行数
});
console.log('最终处理后的数据:', result);
return result;
}

View File

@ -0,0 +1,14 @@
export function error(msg: string = '系统错误', code: number = 999) {
return {
errorCode: code,
success: false,
errorMessage: msg,
};
}
export function mockSuccess(data: any) {
return {
success: true,
data: data,
};
}

View File

@ -0,0 +1,54 @@
import { Image, Space, Tag } from 'antd';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
export const renderTextHelper = {
TagList(items: { label: string; value: string; color: string }[]) {
return (
<Space>
{items?.map((item) => (
<Tag color={item.color} key={item.value}>
{item.label}
</Tag>
))}
</Space>
);
},
Tag({
Enums,
value,
isColor = false,
}: {
Enums?: any;
value?: any;
isColor?: any;
}) {
let item: any = Object.values(Enums).find((data: any) => {
return data.value === '' + value;
});
return isColor ? (
<Tag color={item?.color}>{item?.text}</Tag>
) : (
<Tag>{item?.text}</Tag>
);
// return <Tag style={{ color: item.color }}>{item.text}</Tag>;
},
Images(images: string[]) {
return (
<Image.PreviewGroup>
{images?.map((img: any) => (
<Image
key={img.uid}
width={100}
// height={100}
src={img.url}
/>
))}
</Image.PreviewGroup>
);
},
RelativeTo(value: string) {
return dayjs().to(dayjs(value));
},
};

View File

@ -0,0 +1,165 @@
import { Rule } from 'antd/es/form';
export const rulesHelper = {
text: {
required: true,
rules: [
() => ({
validator(_, value) {
return value && value !== ''
? Promise.resolve()
: Promise.reject(new Error('不能为空'));
},
}),
] as Rule[],
},
phone: {
required: true,
rules: [
() => ({
validator(_, value) {
let phoneRegex = /^1[3-9]\d{9}$/;
if (!value) {
return Promise.reject(new Error('请输入手机号'));
}
if (value?.length !== 11) {
return Promise.reject(new Error('手机号至少11位'));
}
if (!phoneRegex.test(value)) {
return Promise.reject(new Error('请输入正确的手机号'));
}
return Promise.resolve();
},
}),
] as Rule[],
},
upload(props?: { max?: number; errMsg?: string }) {
return {
required: true,
rules: [
() => ({
validator(_, value) {
if (!value) return Promise.reject(new Error('请上传'));
let doneUploads = value?.filter(
(item: { status: string }) => item.status === 'done',
);
if (!doneUploads) return Promise.reject(new Error('请上传'));
return doneUploads.length > (props?.max ?? 1)
? Promise.reject(new Error(props?.errMsg ?? '请上传'))
: Promise.resolve();
},
}),
] as Rule[],
};
},
number: {
required: true,
rules: [
() => ({
validator(_, value) {
if (value !== undefined && value !== '' && !isNaN(Number(value))) {
return Promise.resolve();
} else {
return Promise.reject(new Error('请输入数字,且不能为空'));
}
},
}),
] as Rule[],
},
boolean: {
required: true,
rules: [
() => ({
validator(_, value) {
return value !== undefined && value !== ''
? Promise.resolve()
: Promise.reject(new Error('不能为空'));
},
}),
] as Rule[],
},
richtext: {
required: true,
rules: [
() => ({
validator(_, value) {
if (value && value !== '<p><br></p>') {
return Promise.resolve();
} else {
return Promise.reject(new Error('不能为空'));
}
},
}),
] as Rule[],
},
array: {
required: true,
rules: [
() => ({
validator(_, value) {
if (value && value.length > 0) {
return Promise.resolve();
} else {
return Promise.reject(new Error('不能为空'));
}
},
}),
] as Rule[],
},
cascader: {
required: true,
rules: [
() => ({
validator(_, value) {
if (value && value.length > 0) {
value.forEach((item: any) => {
if (item === undefined) {
return Promise.reject(new Error('不能为空'));
}
});
return Promise.resolve();
} else {
return Promise.reject(new Error('不能为空'));
}
},
}),
] as Rule[],
},
getMonthStartDate(date: string) {
//获取当月的开始日期
if (date) {
const targetDate = new Date(date);
// 设置为当月第一天
targetDate.setDate(1);
return targetDate.toISOString().split('T')[0];
} else {
return undefined;
}
},
getMonthEndDate(date: string) {
//获取当月的结束日期
if (date) {
const targetDate = new Date(date);
// 设置为下个月第一天,然后减去一天得到当月最后一天
targetDate.setMonth(targetDate.getMonth() + 1, 1);
targetDate.setDate(targetDate.getDate() - 1);
return targetDate.toISOString().split('T')[0];
} else {
return undefined;
}
},
getDifference(startNum: number, endNum: number) {
//获取2 个数据之间的差值 单位:天
if (startNum && endNum) {
const start = startNum;
const end = endNum;
const diffDays = end - start;
return diffDays;
} else {
return undefined;
}
},
};

127
src/components/Address.tsx Normal file
View File

@ -0,0 +1,127 @@
import {
ProColumns,
ProFormCascader,
ProFormColumnsType,
ProFormText,
} from '@ant-design/pro-components';
import { rulesHelper } from '@/common';
import { DefaultOptionType } from 'antd/es/cascader';
import data from './city.json';
const request = async () => Promise.resolve(data as Record<string, any>[]);
type ReturnAddressType = ProColumns<Record<string, any>, 'text'>;
type ReturnType = ProFormColumnsType<any, 'text'>;
type PropsType = { required?: boolean } & ReturnType;
const filter = (inputValue: string, path: DefaultOptionType[]) =>
path.some(
(option) =>
(option.label as string).toLowerCase().indexOf(inputValue.toLowerCase()) >
-1,
);
export const Address = {
Cascader: (
props: { keys: string[]; max?: number } & PropsType,
): PropsType => {
const { required, ...rest } = props;
return {
valueType: 'cascader',
request,
transform: (value: string[]) => {
// console.log('transform', value);
let root = data as Record<string, any> | undefined;
return props.keys.reduce((accumulator, currentKey, index) => {
if (root) {
const node = root.find(
(item: { value: number | string }) =>
item.value === value?.[index],
);
if (node) {
accumulator[currentKey] = node.label;
root = node.children;
} else {
root = undefined;
}
}
accumulator[currentKey + '_id'] = value?.[index];
return accumulator;
}, {} as Record<string, string>);
},
formItemProps: {
...(required ? rulesHelper.array : {}),
},
fieldProps: {
showSearch: true,
changeOnSelect: true,
placeholder: '请选择 / 输入名称搜索',
fieldNames: {
label: 'label',
value: 'value',
children: 'children',
},
},
...rest,
};
},
};
export function FormAddress(props: any) {
let root = data as any;
return (
<>
<div
style={{
position: 'relative',
zIndex: 10,
}}
>
<ProFormCascader
name="city_ids"
fieldProps={{
options: root,
changeOnSelect: true,
showSearch: { filter },
onChange: (value: string, selectedOptions: { label: string }[]) => {
props?.form?.setFieldsValue({
province: selectedOptions?.[0]?.label || '',
city: selectedOptions?.[1]?.label || '',
area: selectedOptions?.[2]?.label || '',
});
},
}}
label="所在城市"
/>
</div>
<div
style={{
display: 'flex',
position: 'absolute',
top: 0,
left: 0,
zIndex: 1,
opacity: 0,
}}
>
<ProFormText name="province" />
<ProFormText name="city" />
<ProFormText name="area" />
</div>
</>
);
}
export const MyAddressColumns = {
Address({ ...rest }: ReturnAddressType): ReturnAddressType {
let root = data as any;
return {
title: '所在城市',
valueType: 'cascader',
...rest,
fieldProps: {
options: root,
changeOnSelect: true,
showSearch: { filter },
...rest?.fieldProps,
},
};
},
};

View File

@ -0,0 +1,143 @@
import {
MyBetaModalFormProps,
MyButtons,
MyColumns,
MyProTableProps,
} from '@/common';
import { MyModal } from '@/components/MyModal';
import { Apis } from '@/gen/Apis';
import {
AssetItemsEntryTypeEnum,
AssetItemsManageStatusEnum,
AssetItemsStatusEnum,
} from '@/gen/Enums';
import { ProTable } from '@ant-design/pro-components';
import { type TableProps } from 'antd';
import { useRef, useState } from 'react';
interface DataType {
key: React.Key;
id: React.Key;
is_enabled: boolean;
}
export default function ModalsHouseSelectList(
props: MyBetaModalFormProps & {
onChange?: (selectedRows: DataType[]) => void;
onCancel?: () => void;
},
) {
const modalRef = useRef<any>();
// const [selectedDataRow, setSelectedDataRow] = useState<any>({});
const [getSelectedRow, setSelectedRow] = useState<any>([]);
const rowSelection: TableProps<any>['rowSelection'] = {
onChange: (selectedRowKeys: React.Key[], selectedRows: DataType[]) => {
setSelectedRow(selectedRows);
},
defaultSelectedRowKeys: props?.item?.id ? [props?.item?.id] : [],
};
return (
<MyModal
title={'选择资产'}
width="1000px"
myRef={modalRef}
size="middle"
onOpen={() => {
setSelectedRow(props?.item);
}}
node={
<ProTable
{...MyProTableProps.props}
request={async (params, sort) =>
MyProTableProps.request(params, sort, Apis.Asset.AssetItems.List)
}
params={props?.item}
headerTitle={`资产列表`}
rowSelection={{
type: props?.item?.mode || 'checkbox',
...rowSelection,
}}
tableAlertOptionRender={() => {
return (
<MyButtons.Default
key="okSelect"
size="middle"
type="primary"
onClick={() => {
let res: any = getSelectedRow;
props?.onChange?.(res);
modalRef.current?.close();
}}
title="确定选项"
/>
);
}}
columns={[
MyColumns.ID({
search: false,
}),
{
title: '名称',
dataIndex: 'name',
},
MyColumns.EnumTag({
title: '管理状态',
dataIndex: 'manage_status',
valueEnum: AssetItemsManageStatusEnum,
search: !props?.item?.manage_status,
}),
MyColumns.EnumTag({
title: '资产状态',
dataIndex: 'status',
valueEnum: AssetItemsStatusEnum,
// search: false,
}),
MyColumns.EnumTag({
title: '入库方式',
dataIndex: 'entry_type',
valueEnum: AssetItemsEntryTypeEnum,
search: false,
}),
{
title: '资产分类',
search: false,
render: (_, item: any) => {
return `${item?.asset_item_category_level1?.name}-${item?.asset_item_category_level2?.name}-${item?.asset_item_category_level3?.name}`;
},
},
{
title: '仓库',
dataIndex: ['asset_item_warehouse', 'name'],
search: false,
},
{
title: '项目',
dataIndex: ['asset_project', 'name'],
search: false,
},
{
title: '资产归属',
dataIndex: 'ownership',
search: false,
},
{
title: '品牌',
dataIndex: 'brand',
search: false,
},
{
title: '型号',
dataIndex: 'specification',
search: false,
},
{
title: '管理人员',
dataIndex: ['company_employee', 'name'],
search: false,
},
MyColumns.CreatedAt(),
]}
/>
}
></MyModal>
);
}

View File

@ -0,0 +1,109 @@
import {
MyBetaModalFormProps,
MyButtons,
MyColumns,
MyProTableProps,
} from '@/common';
import { MyModal } from '@/components/MyModal';
import { Apis } from '@/gen/Apis';
import { AssetProjectsPropertyTypeEnum } from '@/gen/Enums';
import { ProTable } from '@ant-design/pro-components';
import { useRef, useState } from 'react';
interface DataType {
key?: React.Key;
id?: React.Key;
}
export default function SurveysSelectList(
props: MyBetaModalFormProps & {
onChange?: (selectedRows: DataType[]) => void;
type?: 'checkbox' | 'radio';
},
) {
const modalRef = useRef<any>();
// const [selectedDataRow, setSelectedDataRow] = useState<any>({});
const [getSelectedRow, setSelectedRow] = useState<any>([]);
const rowSelection: any = {
onChange: (selectedRowKeys: React.Key[], selectedRows: DataType[]) => {
console.log(selectedRows, 'selectedRows[0]');
setSelectedRow(selectedRows);
},
getCheckboxProps: (record: any) => ({
disabled: record.deleted_at,
checked: props?.item?.some((item: any) => {
console.log(item, record);
return item?.id === record?.id;
}),
}),
defaultSelectedRowKeys: props?.item?.map((item: any) => item?.id) || [],
};
return (
<MyModal
title={'选择关联项目'}
width="1000px"
myRef={modalRef}
size="middle"
onOpen={() => {
setSelectedRow(props?.item);
console.log(props?.item, 'props?.item?.id');
}}
node={
<ProTable
{...MyProTableProps.props}
request={async (params, sort) =>
MyProTableProps.request(params, sort, Apis.Asset.AssetProjects.List)
}
rowSelection={{
type: props?.type ? props?.type : 'checkbox',
...rowSelection,
}}
options={false}
tableAlertOptionRender={() => {
return (
<MyButtons.Default
key="okSelect"
size="middle"
type="primary"
onClick={() => {
let res: any = getSelectedRow;
props?.onChange?.(res);
modalRef.current?.close();
}}
title="确定选项"
/>
);
}}
columns={[
MyColumns.ID({
search: false,
}),
{
title: '项目名称',
dataIndex: 'name',
},
MyColumns.EnumTag({
title: '类型',
dataIndex: 'property_type',
valueEnum: AssetProjectsPropertyTypeEnum,
search: false,
}),
{
title: '地址',
render: (_, i: any) => {
return `${i?.province || ''} ${i?.city || ''} ${
i?.district || ''
}${i?.address || ''}`;
},
search: false,
},
MyColumns.DeletedAt({
title: '启/禁用',
dataIndex: 'deleted_at',
search: false,
}),
]}
/>
}
></MyModal>
);
}

View File

@ -0,0 +1,98 @@
import {
MyBetaModalFormProps,
MyButtons,
MyColumns,
MyProTableProps,
} from '@/common';
import { MyModal } from '@/components/MyModal';
import { Apis } from '@/gen/Apis';
import { ApprovalTemplatesTypeEnum } from '@/gen/Enums';
import { ProTable } from '@ant-design/pro-components';
import { type TableProps } from 'antd';
import { useRef, useState } from 'react';
interface DataType {
key: React.Key;
id: React.Key;
is_enabled: boolean;
}
export default function ModalsHouseSelectList(
props: MyBetaModalFormProps & {
onChange?: (selectedRows: DataType[]) => void;
onCancel?: () => void;
},
) {
const modalRef = useRef<any>();
// const [selectedDataRow, setSelectedDataRow] = useState<any>({});
const [getSelectedRow, setSelectedRow] = useState<any>([]);
const rowSelection: TableProps<any>['rowSelection'] = {
onChange: (selectedRowKeys: React.Key[], selectedRows: DataType[]) => {
console.log(selectedRows[0], 'selectedRows[0]');
setSelectedRow(selectedRows[0]);
},
defaultSelectedRowKeys: props?.item?.id ? [props?.item?.id] : [],
};
return (
<MyModal
title={'选择审批模版'}
width="1000px"
myRef={modalRef}
size="middle"
onOpen={() => {
setSelectedRow(props?.item);
}}
node={
<ProTable
{...MyProTableProps.props}
request={async (params, sort) =>
MyProTableProps.request(
{
...params,
...props?.item,
},
sort,
Apis.Approval.ApprovalTemplates.List,
)
}
rowSelection={{ type: 'radio', ...rowSelection }}
tableAlertOptionRender={() => {
return (
<MyButtons.Default
key="okSelect"
size="middle"
type="primary"
onClick={() => {
let res: any = getSelectedRow;
props?.onChange?.(res);
modalRef.current?.close();
}}
title="确定选项"
/>
);
}}
columns={[
MyColumns.ID({ search: false }),
{
title: '模版名称',
dataIndex: 'name',
},
{
title: '模版编码',
dataIndex: 'code',
},
MyColumns.EnumTag({
title: '业务类型',
dataIndex: 'type',
valueEnum: ApprovalTemplatesTypeEnum,
search: false,
}),
MyColumns.Boolean({
title: '是否启用',
search: false,
}),
]}
/>
}
></MyModal>
);
}

View File

@ -0,0 +1,138 @@
import {
MyBetaModalFormProps,
MyButtons,
MyColumns,
MyProTableProps,
} from '@/common';
import { MyModal } from '@/components/MyModal';
import { Apis } from '@/gen/Apis';
import {
ContractsContractNatureEnum,
ContractsStatusEnum,
ContractTemplatesIncomeExpenseTypeEnum,
} from '@/gen/Enums';
import { ProTable } from '@ant-design/pro-components';
import dayjs from 'dayjs';
import { useRef, useState } from 'react';
interface DataType {
key?: React.Key;
id?: React.Key;
}
export default function CompanySealsSelectList(
props: MyBetaModalFormProps & {
onChange?: (selectedRows: DataType[]) => void;
},
) {
const modalRef = useRef<any>();
// const [selectedDataRow, setSelectedDataRow] = useState<any>({});
const [getSelectedRow, setSelectedRow] = useState<any>([]);
const rowSelection: any = {
onChange: (selectedRowKeys: React.Key[], selectedRows: DataType[]) => {
console.log(selectedRows, 'selectedRows[0]');
setSelectedRow(selectedRows);
},
getCheckboxProps: (record: any) => ({
disabled: record.deleted_at,
checked: props?.item?.some((item: any) => {
console.log(item, record);
return item?.id === record?.id;
}),
}),
defaultSelectedRowKeys: props?.item?.map((item: any) => item?.id) || [],
};
return (
<MyModal
title={'选择合同'}
width="1000px"
myRef={modalRef}
size="middle"
onOpen={() => {
setSelectedRow(props?.item);
console.log(props?.item, 'props?.item?.id');
}}
node={
<ProTable
{...MyProTableProps.props}
request={async (params, sort) =>
MyProTableProps.request(params, sort, Apis.Contract.Contracts.List)
}
rowSelection={{ type: 'radio', ...rowSelection }}
options={false}
tableAlertOptionRender={() => {
return (
<MyButtons.Default
key="okSelect"
size="middle"
type="primary"
onClick={() => {
let res: any = getSelectedRow;
props?.onChange?.(res);
modalRef.current?.close();
}}
title="确定选项"
/>
);
}}
columns={[
MyColumns.ID({ search: false }),
{
title: '合同编号',
dataIndex: 'code',
},
{
title: '合同名称',
dataIndex: 'name',
},
{
title: '签订部门',
dataIndex: 'sign_department',
},
MyColumns.EnumTag({
title: '状态',
dataIndex: 'status',
valueEnum: ContractsStatusEnum,
}),
MyColumns.EnumTag({
title: '收支类型',
dataIndex: 'income_expense_type',
valueEnum: ContractTemplatesIncomeExpenseTypeEnum,
}),
MyColumns.EnumTag({
title: '合同性质',
dataIndex: 'contract_nature',
valueEnum: ContractsContractNatureEnum,
}),
{
title: '合同总价',
search: false,
render: (_, item: any) => {
return `¥${item.total_amount}`;
},
},
{
title: '有效期',
search: false,
render: (_, item: any) => {
return `${dayjs(item.start_time).format('YYYY-MM-DD')}${dayjs(
item.end_time,
).format('YYYY-MM-DD')}`;
},
},
{
title: '开始时间',
dataIndex: 'start_time',
hidden: true,
},
{
title: '结束时间',
dataIndex: 'end_time',
hidden: true,
},
]}
/>
}
></MyModal>
);
}

View File

@ -0,0 +1,109 @@
import {
MyBetaModalFormProps,
MyButtons,
MyColumns,
MyProTableProps,
} from '@/common';
import { MyModal } from '@/components/MyModal';
import { Apis } from '@/gen/Apis';
import { AssetHousesUsageEnum } from '@/gen/Enums';
import { ProTable } from '@ant-design/pro-components';
import { type TableProps } from 'antd';
import { useRef, useState } from 'react';
interface DataType {
key: React.Key;
id: React.Key;
is_enabled: boolean;
}
export default function ModalsHouseSelectList(
props: MyBetaModalFormProps & {
onChange?: (selectedRows: DataType[]) => void;
},
) {
const modalRef = useRef<any>();
// const [selectedDataRow, setSelectedDataRow] = useState<any>({});
const [getSelectedRow, setSelectedRow] = useState<any>([]);
const rowSelection: TableProps<any>['rowSelection'] = {
onChange: (selectedRowKeys: React.Key[], selectedRows: DataType[]) => {
console.log(selectedRows[0], 'selectedRows[0]');
setSelectedRow(selectedRows[0]);
},
defaultSelectedRowKeys: props?.item?.id ? [props?.item?.id] : [],
};
return (
<MyModal
title={'选择房屋'}
width="1000px"
myRef={modalRef}
size="middle"
onOpen={() => {
setSelectedRow(props?.item);
}}
node={
<ProTable
{...MyProTableProps.props}
request={async (params, sort) =>
MyProTableProps.request(params, sort, Apis.Asset.AssetHouses.List)
}
rowSelection={{ type: 'radio', ...rowSelection }}
options={false}
tableAlertOptionRender={() => {
return (
<MyButtons.Default
key="okSelect"
size="middle"
type="primary"
onClick={() => {
let res: any = getSelectedRow;
props?.onChange?.(res);
modalRef.current?.close();
}}
title="确定选项"
/>
);
}}
columns={[
MyColumns.ID({ search: false }),
{
title: '项目名称',
dataIndex: ['asset_project', 'name'],
search: {
transform: (value) => {
return { project_name: value };
},
},
},
{
title: '房屋名称',
dataIndex: 'full_name',
},
{
title: '楼栋名称',
dataIndex: 'building_name',
hidden: true,
},
{
title: '单元名称',
dataIndex: 'unit_name',
hidden: true,
},
MyColumns.EnumTag({
title: '用途',
dataIndex: 'usage',
valueEnum: AssetHousesUsageEnum,
}),
{
title: '楼层',
dataIndex: 'floor',
render(_, record) {
return `${record?.floor || '-'}`;
},
search: false,
},
]}
/>
}
></MyModal>
);
}

View File

@ -0,0 +1,715 @@
import { MyBetaModalFormProps } from '@/common';
import { MyModal } from '@/components/MyModal';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { useEffect, useRef, useState } from 'react';
// 修复 Leaflet 图标问题
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
});
// 创建自定义图标
const createCustomIcon = (color = 'red') => {
return L.divIcon({
className: 'custom-marker',
html: `
<div style="
background-color: ${color};
width: 20px;
height: 20px;
border-radius: 50%;
border: 3px solid white;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
"></div>
`,
iconSize: [20, 20],
iconAnchor: [10, 10],
});
};
interface LocationResult {
lat: number;
lng: number;
address: string;
name?: string;
}
export default function ModalsMapLeaflet(
props: MyBetaModalFormProps & {
onChange?: (location?: LocationResult) => void;
},
) {
const modalRef = useRef<any>();
const mapRef = useRef<L.Map | null>(null);
const markersRef = useRef<L.LayerGroup | null>(null);
const [searchText, setSearchText] = useState('');
const [searchResults, setSearchResults] = useState<LocationResult[]>([]);
const [selectedLocation, setSelectedLocation] =
useState<LocationResult | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [mapReady, setMapReady] = useState(false);
const mapInitializedRef = useRef(false);
// 天地图配置
const TIANDITU_KEY = '4ce26ecef55ae1ec47910a72a098efc0';
const tiandituUrls = {
vector: `https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_KEY}`,
label: `https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_KEY}`,
};
// 清理地图
useEffect(() => {
return () => {
if (mapRef.current) {
mapRef.current.remove();
mapRef.current = null;
mapInitializedRef.current = false;
setMapReady(false);
}
};
}, []);
// 初始化地图
const initializeMap = () => {
// 确保DOM已经渲染
setTimeout(() => {
const mapContainer = document.getElementById('map');
if (!mapContainer) {
console.error('地图容器不存在');
return;
}
// 如果地图已经存在,先清理
if (mapRef.current) {
mapRef.current.remove();
mapRef.current = null;
mapInitializedRef.current = false;
}
// 清理容器
while (mapContainer.firstChild) {
mapContainer.removeChild(mapContainer.firstChild);
}
try {
console.log('开始初始化地图...');
const map = L.map('map', {
center: [30.258134, 120.19382669582967],
zoom: 12,
zoomControl: true,
attributionControl: false,
});
mapRef.current = map;
mapInitializedRef.current = true;
// 添加天地图图层
const baseLayer = L.tileLayer(tiandituUrls.vector, {
attribution: '&copy; 天地图',
minZoom: 3,
maxZoom: 18,
}).addTo(map);
const labelLayer = L.tileLayer(tiandituUrls.label, {
minZoom: 3,
maxZoom: 18,
}).addTo(map);
// 创建标记图层组
markersRef.current = L.layerGroup().addTo(map);
// 添加地图点击事件
map.on('click', (e: L.LeafletMouseEvent) => {
handleMapClick(e.latlng.lat, e.latlng.lng);
});
// 添加缩放和比例尺控件
L.control.scale({ imperial: false }).addTo(map);
// 地图加载完成
map.whenReady(() => {
console.log('地图加载完成');
setMapReady(true);
});
console.log('地图初始化成功');
} catch (error) {
console.error('地图初始化失败:', error);
mapInitializedRef.current = false;
setMapReady(false);
}
}, 300);
};
// 使用天地图 REST API 进行搜索
const searchWithTiandituAPI = async (
keyword: string,
): Promise<LocationResult[]> => {
try {
const url = `https://api.tianditu.gov.cn/v2/search?postStr={
"keyWord": "${encodeURIComponent(keyword)}",
"level": "12",
"mapBound": "115.38333,39.36667,117.68333,41.23333",
"queryType": "1",
"start": "0",
"count": "20"
}&type=query&tk=${TIANDITU_KEY}`;
console.log('搜索URL:', url);
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('天地图搜索返回数据:', data);
// 根据不同的结果类型处理
const resultType = data.resultType;
switch (resultType) {
case 1: // POI点数据
if (data.pois && Array.isArray(data.pois)) {
return data.pois.map((poi: any) => ({
name: poi.name,
address: poi.address,
lng: parseFloat(poi.lonlat.split(',')[0]),
lat: parseFloat(poi.lonlat.split(',')[1]),
}));
}
break;
case 2: // 推荐城市
// 这里可以处理推荐城市,暂时返回空数组
console.log('推荐城市结果:', data.prompt);
return [];
case 3: // 行政区划
if (data.area) {
// 将行政区划转换为一个位置结果
const area = data.area;
const [lng, lat] = area.lonlat.split(',').map(parseFloat);
return [
{
name: area.name,
address: area.name,
lng: lng,
lat: lat,
},
];
}
break;
case 4: // 建议词
console.log('建议词结果:', data.prompt);
return [];
case 5: // 公交信息
console.log('公交信息结果:', data.lineData);
return [];
default:
console.log('未知结果类型:', resultType);
}
// 如果没有找到有效结果,尝试使用提示信息
if (data.prompt && data.prompt.length > 0) {
const prompt = data.prompt[0];
if (prompt.admins && prompt.admins.length > 0) {
const admin = prompt.admins[0];
return [
{
name: admin.name,
address: admin.name,
lng: parseFloat(admin.lonlat.split(',')[0]),
lat: parseFloat(admin.lonlat.split(',')[1]),
},
];
}
}
throw new Error('未找到相关地点');
} catch (error) {
console.error('天地图搜索API调用失败:', error);
throw error;
}
};
// 搜索地点 - 使用天地图 REST API
const handleSearch = async () => {
if (!searchText.trim()) {
alert('请输入搜索关键词');
return;
}
// 检查天地图密钥
if (!TIANDITU_KEY) {
alert('请配置有效的天地图密钥');
return;
}
if (!mapReady) {
alert('地图尚未准备好,请稍后重试');
return;
}
setIsLoading(true);
try {
console.log('开始天地图搜索:', searchText);
// 清空之前的结果
setSearchResults([]);
if (markersRef.current) {
markersRef.current.clearLayers();
}
// 执行搜索
const results = await searchWithTiandituAPI(searchText);
console.log('搜索成功,结果数量:', results.length);
if (results.length === 0) {
alert('未找到相关地点');
return;
}
setSearchResults(results);
// 在地图上显示搜索结果
const bounds = L.latLngBounds([]);
results.forEach((result, index) => {
bounds.extend([result.lat, result.lng]);
addSearchResultMarker(result, index, results.length);
});
// 调整地图视野
if (mapRef.current) {
mapRef.current.fitBounds(bounds.pad(0.1));
}
} catch (error: any) {
console.error('搜索失败:', error);
alert(`搜索失败: ${error.message || '未知错误'}`);
} finally {
setIsLoading(false);
}
};
// 添加搜索结果标记
const addSearchResultMarker = (
location: LocationResult,
index: number,
total: number,
) => {
if (!markersRef.current) return;
const marker = L.marker([location.lat, location.lng], {
icon: createCustomIcon(index === 0 ? '#1890ff' : '#52c41a'),
}).addTo(markersRef.current);
marker.bindPopup(`
<div style="min-width: 220px;">
<h4 style="margin: 0 0 8px 0; color: #1890ff; font-size: 14px;">${
location.name
}</h4>
<p style="margin: 4px 0; font-size: 12px;"><strong>:</strong> ${
location.address
}</p>
<p style="margin: 4px 0; font-size: 12px;"><strong>:</strong> ${location.lat.toFixed(
6,
)}, ${location.lng.toFixed(6)}</p>
<div style="margin-top: 10px; display: flex; gap: 8px;">
<button onclick="window.selectSearchResult(${index})" style="
padding: 6px 12px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
flex: 1;
"></button>
</div>
</div>
`);
// 第一个结果自动打开弹窗
if (index === 0) {
setTimeout(() => marker.openPopup(), 500);
}
// 将选择函数挂载到window对象
(window as any).selectSearchResult = (index: number) => {
console.log(location, searchResults, 'selectSearchResult');
if (searchResults && searchResults[index]) {
handleSelectResult(searchResults[index]);
} else {
handleConfirmLocation(location);
}
};
};
// 地图点击事件处理
const handleMapClick = async (lat: number, lng: number) => {
try {
setIsLoading(true);
// 使用天地图逆地理编码获取真实地址
const address = await reverseGeocodeWithTianditu(lat, lng);
const location: LocationResult = {
lat,
lng,
address,
name: '点击选择的位置',
};
setSelectedLocation(location);
addMarkerToMap(location);
} catch (error) {
console.error('获取地址信息失败:', error);
} finally {
setIsLoading(false);
}
};
// 使用天地图逆地理编码服务
const reverseGeocodeWithTianditu = async (
lat: number,
lng: number,
): Promise<string> => {
try {
const url = `https://api.tianditu.gov.cn/geocoder?postStr={'lon':${lng},'lat':${lat},'ver':1}&type=geocode&tk=${TIANDITU_KEY}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error('逆地理编码请求失败');
}
const data = await response.json();
if (data.status === '0' && data.result) {
return data.result.formatted_address;
}
return `位置 ${lat.toFixed(6)}, ${lng.toFixed(6)}`;
} catch (error) {
console.error('天地图逆地理编码失败:', error);
return `位置 ${lat.toFixed(6)}, ${lng.toFixed(6)}`;
}
};
// 添加标记到地图
const addMarkerToMap = (location: LocationResult) => {
if (!mapRef.current || !markersRef.current) return;
markersRef.current.clearLayers();
const marker = L.marker([location.lat, location.lng], {
icon: createCustomIcon('#1890ff'),
}).addTo(markersRef.current);
marker
.bindPopup(
`
<div style="min-width: 200px;">
<h4 style="margin: 0 0 8px 0; color: #1890ff;"></h4>
<p style="margin: 4px 0;"><strong>:</strong> ${location.address}</p>
<p style="margin: 4px 0;"><strong>:</strong> ${location.lat.toFixed(
6,
)}, ${location.lng.toFixed(6)}</p>
<button onclick="window.confirmSelection()" style="
margin-top: 8px;
padding: 6px 12px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
"></button>
</div>
`,
)
.openPopup();
(window as any).confirmSelection = () => {
console.log(location, 'confirmSelection');
handleConfirmLocation(location);
};
mapRef.current.setView([location.lat, location.lng], 15);
};
// 选择搜索结果
const handleSelectResult = (result: LocationResult) => {
setSelectedLocation(result);
addMarkerToMap(result);
setSearchResults([]);
setSearchText(result.name || result.address);
};
// 确认选择位置
const handleConfirmLocation = (location?: LocationResult) => {
const finalLocation = location || selectedLocation;
if (!finalLocation) return;
props?.onChange?.(location);
// if (props.onConfirm) {
// props.onConfirm(finalLocation);
// }
if (modalRef.current) {
modalRef.current.close();
}
};
// 清除选择
const handleClearSelection = () => {
setSelectedLocation(null);
setSearchText('');
setSearchResults([]);
if (markersRef.current) {
markersRef.current.clearLayers();
}
if (mapRef.current) {
mapRef.current.setView([30.258134, 120.19382669582967], 12);
}
};
return (
<MyModal
title={'获取经纬度'}
width="1000px"
myRef={modalRef}
size="middle"
onOpen={() => {
console.log('模态框打开,初始化地图...');
setTimeout(initializeMap, 500);
}}
node={
<div style={{ padding: '20px' }}>
{/* 状态提示 */}
{!mapReady && (
<div
style={{
padding: '10px',
backgroundColor: '#fffbe6',
border: '1px solid #ffe58f',
borderRadius: '4px',
marginBottom: '10px',
textAlign: 'center',
}}
>
...
</div>
)}
{/* 搜索区域 */}
<div style={{ marginBottom: '20px' }}>
<div style={{ display: 'flex', gap: '10px', marginBottom: '10px' }}>
<input
type="text"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="请输入地点名称进行搜索(如:杭州中大广场..."
style={{
flex: 1,
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
/>
<button
onClick={handleSearch}
disabled={isLoading || !mapReady}
style={{
padding: '8px 16px',
background: !isLoading && mapReady ? '#1890ff' : '#ccc',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: isLoading || !mapReady ? 'not-allowed' : 'pointer',
}}
>
{isLoading ? '搜索中...' : !mapReady ? '地图加载中' : '搜索'}
</button>
</div>
{/* 搜索结果 */}
{searchResults.length > 0 && (
<div
style={{
border: '1px solid #e8e8e8',
borderRadius: '4px',
maxHeight: '200px',
overflowY: 'auto',
backgroundColor: '#fafafa',
}}
>
<div
style={{
padding: '8px',
fontSize: '12px',
color: '#666',
borderBottom: '1px solid #e8e8e8',
}}
>
{searchResults.length}
</div>
{searchResults.map((result, index) => (
<div
key={index}
onClick={() => handleSelectResult(result)}
style={{
padding: '10px',
borderBottom: '1px solid #f0f0f0',
cursor: 'pointer',
backgroundColor: index === 0 ? '#e6f7ff' : '#fafafa',
}}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = '#e6f7ff')
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor =
index === 0 ? '#e6f7ff' : '#fafafa')
}
>
<div
style={{
fontWeight: 'bold',
marginBottom: '4px',
color: '#1890ff',
}}
>
{result.name || '未知地点'}
</div>
<div style={{ fontSize: '12px', color: '#666' }}>
{result.address}
</div>
<div
style={{
fontSize: '11px',
color: '#999',
marginTop: '2px',
}}
>
: {result.lat.toFixed(6)}, {result.lng.toFixed(6)}
</div>
</div>
))}
</div>
)}
</div>
{/* 地图容器 */}
<div
id="map"
style={{
height: '400px',
border: '1px solid #e8e8e8',
borderRadius: '4px',
}}
/>
{/* 选择信息显示 */}
{selectedLocation && (
<div
style={{
marginTop: '20px',
padding: '15px',
backgroundColor: '#f6ffed',
border: '1px solid #b7eb8f',
borderRadius: '4px',
}}
>
<h4 style={{ margin: '0 0 10px 0', color: '#52c41a' }}>
</h4>
<p style={{ margin: '4px 0' }}>
<strong>:</strong> {selectedLocation.address}
</p>
<p style={{ margin: '4px 0' }}>
<strong>:</strong> {selectedLocation.lat.toFixed(6)},{' '}
{selectedLocation.lng.toFixed(6)}
</p>
<div style={{ marginTop: '10px', display: 'flex', gap: '10px' }}>
<button
onClick={() => handleConfirmLocation(selectedLocation)}
style={{
padding: '8px 16px',
background: '#52c41a',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontWeight: 'bold',
}}
>
</button>
<button
onClick={handleClearSelection}
style={{
padding: '8px 16px',
background: '#ff4d4f',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
</button>
</div>
</div>
)}
{/* 使用说明 */}
<div
style={{
marginTop: '15px',
padding: '10px',
backgroundColor: '#f0f8ff',
border: '1px solid #91d5ff',
borderRadius: '4px',
fontSize: '12px',
color: '#666',
}}
>
<strong>使:</strong>
<ul style={{ margin: '8px 0', paddingLeft: '20px' }}>
<li></li>
<li>使API</li>
<li>使</li>
<li></li>
<li></li>
<li>14</li>
</ul>
{!TIANDITU_KEY && (
<div style={{ color: '#ff4d4f', marginTop: '8px' }}>
</div>
)}
</div>
</div>
}
></MyModal>
);
}

View File

@ -0,0 +1,122 @@
import {
MyBetaModalFormProps,
MyButtons,
MyColumns,
MyProTableProps,
} from '@/common';
import { MyModal } from '@/components/MyModal';
import { Apis } from '@/gen/Apis';
import { ResourceTypesCategoryEnum } from '@/gen/Enums';
import { ProTable } from '@ant-design/pro-components';
import { type TableProps, Image } from 'antd';
import { useRef, useState } from 'react';
interface DataType {
key: React.Key;
id: React.Key;
is_enabled: boolean;
}
export default function ModalsResourceSelectList(
props: MyBetaModalFormProps & {
onChange?: (selectedRows: DataType[]) => void;
},
) {
const modalRef = useRef<any>();
// const [selectedDataRow, setSelectedDataRow] = useState<any>({});
const [getSelectedRow, setSelectedRow] = useState<any>([]);
const rowSelection: TableProps<any>['rowSelection'] = {
onChange: (selectedRowKeys: React.Key[], selectedRows: DataType[]) => {
console.log(selectedRows[0], 'selectedRows[0]');
setSelectedRow(selectedRows[0]);
},
getCheckboxProps: (record: any) => ({
disabled: record.deleted_at,
checked: props?.item?.resources_id === record?.id,
}),
defaultSelectedRowKeys: [props?.item?.resources_id],
};
return (
<MyModal
title={'选择资源'}
width="1000px"
myRef={modalRef}
size="middle"
onOpen={() => {
setSelectedRow(props?.item);
}}
node={
<ProTable
{...MyProTableProps.props}
request={async (params, sort) =>
MyProTableProps.request(
{
...params,
...props?.item,
},
sort,
Apis.Resource.Resources.UsableResourceList,
)
}
rowSelection={{ type: 'radio', ...rowSelection }}
options={false}
tableAlertOptionRender={() => {
return (
<MyButtons.Default
key="okSelect"
size="middle"
type="primary"
onClick={() => {
let res: any = getSelectedRow;
props?.onChange?.(res);
modalRef.current?.close();
}}
title="确定选项"
/>
);
}}
columns={[
MyColumns.ID({
search: false,
}),
{
title: '名称',
dataIndex: 'name',
},
MyColumns.EnumTag({
title: '类型',
dataIndex: 'category',
valueEnum: ResourceTypesCategoryEnum,
}),
// MyColumns.EnumTag({
// title: '状态',
// dataIndex: 'reservation_status',
// valueEnum: ResourcesReservationStatusEnum,
// }),
{
title: '项目',
search: false,
dataIndex: ['asset_project', 'name'],
},
{
title: '楼栋',
search: false,
dataIndex: ['asset_building', 'name'],
},
{
title: '单元',
search: false,
dataIndex: ['asset_unit', 'name'],
},
{
title: '封面',
search: false,
render: (_, item: any) => {
return <Image src={item?.cover_image?.[0]?.url} height={50} />;
},
},
]}
/>
}
></MyModal>
);
}

View File

@ -0,0 +1,59 @@
import { MyColumnsType } from '@/common';
import { Apis } from '@/gen/Apis';
import { ProColumns, ProFormColumnsType } from '@ant-design/pro-components';
type ReturnType = ProColumns<any, 'text'> & ProFormColumnsType<any, 'text'>;
type PropsType = { required?: boolean } & ReturnType;
export const MomentSelect = {
MomentCategoriesTree(props?: PropsType): ReturnType {
const { ...rest } = props ?? {};
return {
key: 'parent_id',
title: '上级分类',
valueType: 'treeSelect',
request: async () => {
return Apis.Customer.CustomerMomentCategories.SelectTree().then(
(res) => res.data,
);
},
fieldProps: {
allowClear: true,
autoClearSearchValue: true,
bordered: true,
fieldNames: {
label: 'name',
value: 'id',
},
filterTreeNode: true,
showSearch: true,
treeNodeFilterProp: 'title',
treeDefaultExpandAll: true,
},
...rest,
};
},
MomentCategoriesSelect(props?: PropsType): MyColumnsType {
const { ...rest } = props ?? {};
return {
title: '内容分类',
dataIndex: 'moment_categories_ids',
valueType: 'cascader',
request: async () => {
return Apis.Customer.CustomerMomentCategories.SelectTree().then(
(res) => res.data,
);
},
hideInTable: true,
fieldProps: {
showSearch: true,
placeholder: '请选择 / 输入名称搜索',
fieldNames: {
label: 'name',
value: 'id',
children: 'children',
},
},
...rest,
};
},
};

View File

@ -0,0 +1,45 @@
import { Button, Dropdown, MenuProps } from 'antd';
export const MyExport = (props: any) => {
const items: MenuProps['items'] = [
{
key: '1',
label: '当前页(含查询条件)',
onClick: () =>
props?.keyParams
? props?.download?.({ download_type: 'page', ...props?.item })
: props?.download?.Export?.({
download_type: 'page',
...props?.item,
}),
},
{
key: '2',
label: '所有页(含查询条件)',
onClick: () =>
props?.keyParams
? props?.download?.({ download_type: 'query', ...props?.item })
: props?.download?.Export?.({
download_type: 'query',
...props?.item,
}),
},
{
key: '3',
label: '所有记录',
onClick: () =>
props?.keyParams
? props?.download?.({ download_type: 'all', ...props?.item })
: props?.download?.Export?.({
download_type: 'all',
...props?.item,
}),
},
];
return (
<Dropdown menu={{ items }} placement="bottomLeft">
<Button type="primary">{props?.title || '导出'}</Button>
</Dropdown>
);
};

View File

@ -0,0 +1,60 @@
import { MyButtons } from '@/common';
import { Modal } from 'antd';
import { useEffect, useImperativeHandle, useState } from 'react';
export function MyModal(props?: any) {
const { mode = 'btn' } = props || {};
const [open, setOpen] = useState(false);
const close = () => {
setOpen(false);
};
useEffect(() => {
if (open) {
props?.onOpen?.(true);
}
}, [open]);
useEffect(() => {
setOpen(props?.defaultOpen);
console.log(props?.defaultOpen, 'props?.open');
}, [props?.defaultOpen]);
useImperativeHandle(props.myRef, () => ({
close,
}));
return (
<>
{mode === 'btn' ? (
props?.trigger ? (
<div onClick={() => setOpen(true)}>{props?.trigger}</div>
) : (
<MyButtons.View
title={props.title || '详情'}
type={props.type || 'primary'}
size={props.size || 'small'}
onClick={() => setOpen(true)}
/>
)
) : (
''
)}
<Modal
title={props?.modal?.title || '标题'}
open={open}
onOk={() => {
setOpen(false);
props?.handleOk?.();
}}
onCancel={() => {
setOpen(false);
props?.onCancel?.();
}}
footer={props?.modal?.footer || false}
{...props}
>
{props?.node}
</Modal>
</>
);
}

1403
src/components/Select.tsx Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,276 @@
import { rulesHelper } from '@/common';
import { Apis } from '@/gen/Apis';
import { CompanySealsTypeEnum } from '@/gen/Enums';
import { ProColumns, ProFormColumnsType } from '@ant-design/pro-components';
type ReturnType = ProColumns<any, 'text'> & ProFormColumnsType<any, 'text'>;
type PropsType = { required?: boolean } & ReturnType;
export const SelectContract = {
//选择合同的企业主体
SupplierName(props?: PropsType): ReturnType {
const {
title = '选择企业主体',
key = 'company_suppliers_id',
required = false,
hideInTable = true,
...rest
} = props ?? {};
return {
title: title,
key: key,
valueType: 'select',
hideInTable: hideInTable,
formItemProps: { ...(required ? rulesHelper.text : {}) },
request: async (params) => {
let res = await Apis.Company.CompanySuppliers.Select({
...params,
keyWords: params?.keyWords,
});
res?.data?.forEach((l: any) => {
l.label = l.name;
l.value = l.name;
});
return res?.data;
},
...rest,
fieldProps: {
showSearch: true,
fieldNames: {
label: 'label',
value: 'value',
},
...rest?.fieldProps,
},
};
},
//组织树 - 确保只返回最后一个选中的值
OrganizationsName(props?: PropsType): ReturnType {
const {
title = '组织',
key = 'organizations_id',
required = false,
hideInTable = true,
...rest
} = props ?? {};
return {
title: title,
key: key,
valueType: 'cascader',
hideInTable: hideInTable,
formItemProps: { ...(required ? rulesHelper.text : {}) },
request: async (params) =>
(
await Apis.Company.Organizations.SelectTree({
keywords: params?.keyWords,
companies_id: params?.companies_id,
...params,
})
).data,
...rest,
fieldProps: {
showSearch: true,
changeOnSelect: true,
fieldNames: {
label: 'name',
value: 'name',
children: 'children',
},
...rest?.fieldProps,
},
};
},
//获取合同类型
ContractTypes(props?: PropsType): ReturnType {
const {
title = '合同类型',
key = 'contract_types_id',
required = false,
hideInTable = true,
...rest
} = props ?? {};
return {
title: title,
key: key,
valueType: 'select',
hideInTable: hideInTable,
formItemProps: { ...(required ? rulesHelper.number : {}) },
request: async (params) => {
let res = await Apis.Contract.ContractTypes.Select({
...params,
name: params?.keyWords || undefined,
});
return res?.data;
},
...rest,
fieldProps: {
showSearch: true,
fieldNames: {
label: 'label',
value: 'value',
},
...rest?.fieldProps,
},
};
},
//项目id
AssetProjects(props?: PropsType): ReturnType {
const {
title = '选择项目',
key = 'asset_projects_id',
required = false,
hideInTable = true,
...rest
} = props ?? {};
return {
title: title,
key: key,
valueType: 'select',
hideInTable: hideInTable,
formItemProps: { ...(required ? rulesHelper.number : {}) },
request: async (params) =>
(
await Apis.Asset.AssetProjects.Select({
keywords: params?.keyWords,
...params,
})
).data,
...rest,
fieldProps: {
showSearch: true,
placeholder: '请选择(支持关键字搜索)',
fieldNames: {
label: 'label',
value: 'value',
},
...rest?.fieldProps,
},
};
},
//员工
EmployeeName(props?: PropsType): ReturnType {
const {
title = '员工',
key = 'contract_liaison',
required = false,
hideInTable = true,
...rest
} = props ?? {};
return {
title: title,
key: key,
valueType: 'select',
hideInTable: hideInTable,
formItemProps: { ...(required ? rulesHelper.text : {}) },
request: async (params) =>
(
await Apis.Company.CompanyEmployees.Select({
keywords: params?.keyWords,
...params,
})
).data,
...rest,
fieldProps: {
showSearch: true,
fieldNames: {
label: 'label',
value: 'label',
},
...rest?.fieldProps,
},
};
},
//审核人
ExamineEmployees(props?: PropsType): ReturnType {
const {
title = '审核人',
key = 'company_employees_id',
required = false,
hideInTable = true,
...rest
} = props ?? {};
return {
title: title,
key: key,
valueType: 'select',
hideInTable: hideInTable,
formItemProps: { ...(required ? rulesHelper.number : {}) },
request: async (params) => {
let value = {
keywords:
params?.keyWords === undefined ? params?.name : params?.keyWords,
};
console.log(params, 'Employees');
return (
await Apis.Company.CompanyEmployees.Select({
...params,
...value,
})
).data;
},
...rest,
fieldProps: {
showSearch: true,
fieldNames: {
label: 'label',
value: 'value',
},
...rest?.fieldProps,
},
};
},
// 合同拟制,选择印章
ContractSeals(props?: PropsType): ReturnType {
const {
title = '选择印章',
key = 'id',
required = false,
hideInTable = true,
...rest
} = props ?? {};
return {
title: title,
key: key,
valueType: 'select',
hideInTable: hideInTable,
formItemProps: { ...(required ? rulesHelper.array : {}) },
request: async (params) => {
let res = await Apis.Company.CompanySeals.List({
...params,
keyWords: params?.keyWords,
});
res?.data?.forEach((l: any) => {
// 获取印章类型的友好显示名称
let typeText = l.type;
if (typeof l.type === 'string') {
const typeKey = l.type as keyof typeof CompanySealsTypeEnum;
if (CompanySealsTypeEnum[typeKey]?.text) {
typeText = CompanySealsTypeEnum[typeKey].text;
}
}
l.label = `${l.company_supplier.name} (${typeText})`;
l.value = l.id;
});
return res?.data;
},
...rest,
fieldProps: {
showSearch: true,
fieldNames: {
label: 'label',
value: 'value',
},
...rest?.fieldProps,
},
};
},
};

View File

@ -0,0 +1,53 @@
import { MyBetaModalFormProps } from '@/common';
import { ProCard } from '@ant-design/pro-components';
export default function Attachments(props: MyBetaModalFormProps) {
return (
<ProCard title="合同附件">
{props?.item?.map((item: any) => {
const handleDownload = async (e: React.MouseEvent) => {
e.preventDefault();
try {
const response = await fetch(item.url);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = item.name;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('下载失败:', error);
// 如果下载失败,则在新窗口打开
window.open(item.url, '_blank');
}
};
return (
<div key={item.url}>
<a href={item.url} onClick={handleDownload}>
{item.name}
</a>
</div>
);
})}
{/* {props?.item?.map((res: any, index: number) => {
return res?.type?.indexOf('image') > -1 ? (
<Image
src={res?.url}
alt={res?.name || '文件'}
key={`item_${index}`}
width={100}
height={100}
/>
) : (
<a href={res?.url} key={`item_${index}`}>
{res?.name || '文件'}
</a>
);
})} */}
</ProCard>
);
}

View File

@ -0,0 +1,90 @@
import { rulesHelper } from '@/common';
import { Apis } from '@/gen/Apis';
import { ProColumns, ProFormColumnsType } from '@ant-design/pro-components';
type ReturnType = ProColumns<any, 'text'> & ProFormColumnsType<any, 'text'>;
type PropsType = { required?: boolean } & ReturnType;
export const SysSelects = {
Api(props?: PropsType): ReturnType {
const { required = false, ...rest } = props ?? {};
return {
title: '后端API',
dataIndex: 'backend_apis',
valueType: 'treeSelect',
hideInTable: true,
formItemProps: { ...(required ? rulesHelper.number : {}) },
fieldProps: {
showSearch: true,
allowClear: true,
treeDefaultExpandAll: true,
dropdownStyle: { maxHeight: 400, overflow: 'auto' },
multiple: true,
fieldNames: {
label: 'value',
value: 'value',
children: 'children',
},
},
request: async () =>
(await Apis.Permission.SysPermissions.SelectApi()).data?.children,
...rest,
};
},
SysPermissionsTree(props?: { guard_name: string } & PropsType): ReturnType {
const { guard_name = 'Admin', ...rest } = props ?? {};
return {
key: 'parent_id',
title: '上级功能',
valueType: 'treeSelect',
request: async () => {
return Apis.Permission.SysPermissions.Tree({
guard_name: guard_name,
}).then((res) => res.data);
},
fieldProps: {
allowClear: true,
autoClearSearchValue: true,
bordered: true,
fieldNames: {
label: 'name',
value: 'id',
},
filterTreeNode: true,
showSearch: true,
treeNodeFilterProp: 'title',
treeDefaultExpandAll: true,
},
...rest,
};
},
SysRoles(props?: PropsType): ReturnType {
const {
title = '角色',
key = 'roles_id',
required = true,
hideInTable = true,
...rest
} = props ?? {};
return {
title: title,
key: key,
valueType: 'select',
hideInTable: hideInTable,
formItemProps: { ...(required ? rulesHelper.number : {}) },
fieldProps: {
mode: 'multiple',
showSearch: false,
fieldNames: {
label: 'name',
value: 'id',
},
},
request: async () => (await Apis.Permission.SysRoles.Select()).data,
...rest,
};
},
};

View File

@ -0,0 +1,76 @@
import { Apis } from '@/gen/Apis';
import { Transfer } from 'antd';
import { useEffect, useState } from 'react';
const MyTransferProject = (props: any) => {
const [getLoading, setLoading] = useState(false);
const [dataSource, setTransferData] = useState<any[]>([]);
const [targetKeys, setTargetKeys] = useState<string[]>([]);
useEffect(() => {
Apis.Asset.AssetProjects.Select({})
.then((res) => {
setLoading(true);
const data =
res.data?.map((item: any) => ({
key: item.value?.toString(),
title: item.label,
})) || [];
setTransferData(data);
})
.catch(() => {
setTransferData([]);
});
}, []);
useEffect(() => {
if (props.value?.length) {
setTargetKeys(props.value);
}
}, [props.value]);
return (
getLoading && (
<Transfer
dataSource={dataSource}
targetKeys={targetKeys}
onChange={(targetKeys) => {
let dataIds: any = [];
console.log(targetKeys, 'targetKeys', props.value);
targetKeys?.forEach((res: any) => {
dataSource?.forEach((k: any) => {
if (res === k.key) {
dataIds?.push(k?.key);
}
});
});
setTargetKeys(targetKeys as string[]);
props?.onChange?.(dataIds);
}}
render={(item) => item.title}
titles={['可选项目', '已选项目']}
showSearch
listStyle={{
width: 250,
height: 300,
}}
operations={['选择', '移除']}
operationStyle={{ marginTop: 20 }}
locale={{
itemUnit: '项',
itemsUnit: '项',
searchPlaceholder: '请输入搜索内容',
notFoundContent: '列表为空',
}}
onSelectChange={(sourceSelectedKeys, targetSelectedKeys) => {
console.log(
sourceSelectedKeys,
targetSelectedKeys,
'sourceSelectedKeys',
);
// 处理选择变化,但不触发表单提交
}}
/>
)
);
};
export default MyTransferProject;

View File

@ -0,0 +1,109 @@
import { Apis } from '@/gen/Apis';
import { Transfer } from 'antd';
import { useEffect, useState } from 'react';
const MyTransferUnits = (props: any) => {
const [getLoading, setLoading] = useState(false);
const [dataSource, setTransferData] = useState<any[]>([]);
const [targetKeys, setTargetKeys] = useState<string[]>([]);
useEffect(() => {
if (props?.item?.asset_projects_id) {
Apis.Asset.AssetUnits.GridSelect({
asset_projects_id: props?.item?.asset_projects_id,
type: props?.item?.type,
})
.then((res) => {
setLoading(true);
const data =
res.data?.map((item: any) => ({
key: item.value?.toString(),
title: item?.asset_building?.name + item.label,
asset_buildings_id: item.asset_buildings_id,
})) || [];
setTransferData(data);
})
.catch(() => {
setTransferData([]);
});
}
}, [props?.item?.asset_projects_id]);
useEffect(() => {
console.log(props.value, 'props.value');
if (props.value?.length && !props.value[0]?.asset_units_id) {
let dataIds: any = [];
props.value?.forEach((res: any) => {
dataIds?.push(res?.toString());
});
setTargetKeys(dataIds);
}
}, [props.value]);
useEffect(() => {
if (dataSource?.length && props?.value?.length) {
let dataIds: any = [];
props.value?.forEach((res: any) => {
dataSource?.forEach((k: any) => {
if (res?.toString() === k.key) {
dataIds?.push({
asset_projects_id: props?.item?.asset_projects_id,
asset_buildings_id: k?.asset_buildings_id,
asset_units_id: k?.key,
});
}
});
});
props?.onChange?.(dataIds);
}
}, [getLoading]);
return (
getLoading && (
<Transfer
dataSource={dataSource}
targetKeys={targetKeys}
onChange={(targetKeys) => {
let dataIds: any = [];
console.log(targetKeys, 'targetKeys', props.value);
targetKeys?.forEach((res: any) => {
dataSource?.forEach((k: any) => {
if (res === k.key) {
dataIds?.push({
asset_projects_id: props?.item?.asset_projects_id,
asset_buildings_id: k?.asset_buildings_id,
asset_units_id: k?.key,
});
}
});
});
setTargetKeys(targetKeys as string[]);
props?.onChange?.(dataIds);
}}
render={(item) => item.title}
titles={['可选单元', '已选单元']}
showSearch
listStyle={{
width: 250,
height: 300,
}}
operations={['选择', '移除']}
operationStyle={{ marginTop: 20 }}
locale={{
itemUnit: '项',
itemsUnit: '项',
searchPlaceholder: '请输入搜索内容',
notFoundContent: '列表为空',
}}
onSelectChange={(sourceSelectedKeys, targetSelectedKeys) => {
console.log(
sourceSelectedKeys,
targetSelectedKeys,
'sourceSelectedKeys',
);
// 处理选择变化,但不触发表单提交
}}
/>
)
);
};
export default MyTransferUnits;

View File

@ -0,0 +1,79 @@
import { rulesHelper } from '@/common';
import { Apis } from '@/gen/Apis';
import { ProColumns, ProFormColumnsType } from '@ant-design/pro-components';
type ReturnType = ProColumns<any, 'text'> & ProFormColumnsType<any, 'text'>;
type PropsType = { required?: boolean } & ReturnType;
export const TreeSelects = {
TreeSelectDepartment(props?: PropsType): ReturnType {
const { required = false, ...rest } = props ?? {};
return {
title: '签约部门',
dataIndex: 'sign_department_id',
valueType: 'treeSelect',
hideInTable: true,
formItemProps: { ...(required ? rulesHelper.text : {}) },
request: async () => (await Apis.Company.Organizations.SelectTree()).data,
...rest,
fieldProps: {
showSearch: true,
allowClear: true,
treeDefaultExpandAll: true,
fieldNames: {
label: 'name',
value: 'id',
children: 'children',
},
...rest?.fieldProps,
},
};
},
TreeSelectEventCategories(props?: PropsType): ReturnType {
const { required = false, ...rest } = props ?? {};
return {
title: '上级分类',
dataIndex: 'parent_id',
valueType: 'treeSelect',
hideInTable: true,
formItemProps: { ...(required ? rulesHelper.text : {}) },
request: async () =>
(await Apis.Emergency.EmergencyEventCategories.SelectTree()).data,
...rest,
fieldProps: {
showSearch: true,
allowClear: true,
treeDefaultExpandAll: true,
fieldNames: {
label: 'name',
value: 'id',
children: 'children',
},
...rest?.fieldProps,
},
};
},
TreeSelecTassetItemCategories(props?: PropsType): ReturnType {
const { required = false, ...rest } = props ?? {};
return {
title: '上级分类',
dataIndex: 'parent_id',
valueType: 'treeSelect',
hideInTable: true,
formItemProps: { ...(required ? rulesHelper.text : {}) },
request: async () =>
(await Apis.Asset.AssetItemCategories.SelectTree()).data,
...rest,
fieldProps: {
showSearch: true,
allowClear: true,
treeDefaultExpandAll: true,
fieldNames: {
label: 'name',
value: 'id',
children: 'children',
},
...rest?.fieldProps,
},
};
},
};

222935
src/components/city.json Normal file

File diff suppressed because it is too large Load Diff

4527
src/gen/ApiTypes.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More