3
.eslintrc.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: require.resolve('@umijs/max/eslint'),
|
||||
};
|
||||
75
.github/workflows/develop.yml
vendored
Normal 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
@ -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
@ -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
@ -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"
|
||||
]
|
||||
}
|
||||
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
.umi
|
||||
.umi-production
|
||||
8
.prettierrc
Normal 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
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: require.resolve('@umijs/max/stylelint'),
|
||||
};
|
||||
27
.umirc.ts
Normal 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
@ -0,0 +1 @@
|
||||
{ "projectName": "trae_pay-admin_tj5u" }
|
||||
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
15
Dockerfile
Normal 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
@ -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
|
After Width: | Height: | Size: 161 KiB |
8
gencode.json
Normal 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
49
package.json
Normal 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
@ -0,0 +1 @@
|
||||
c6c4bcb00af97510754b9d56def22047
|
||||
1
public/WW_verify_2hQvP05DYNSk0Krf.txt
Normal file
@ -0,0 +1 @@
|
||||
2hQvP05DYNSk0Krf
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 161 KiB |
10
src/access.ts
Normal 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
@ -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
BIN
src/assets/bitcoin.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
14
src/common/components/Editor/custom-types.d.ts
vendored
Normal 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[];
|
||||
}
|
||||
}
|
||||
113
src/common/components/Editor/index.tsx
Normal 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;
|
||||
118
src/common/components/GetRPermission.tsx
Normal 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;
|
||||
};
|
||||
13
src/common/components/MyAccess.tsx
Normal 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>;
|
||||
}
|
||||
147
src/common/components/MyButtons.tsx
Normal 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() {},
|
||||
};
|
||||
391
src/common/components/MyIcons.tsx
Normal 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 />,
|
||||
};
|
||||
18
src/common/components/MyStatistics.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
16
src/common/components/MyTag.tsx
Normal 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>;
|
||||
}
|
||||
58
src/common/components/formFields/MyColorPicker.tsx
Normal 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());
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
21
src/common/components/formFields/MyEnumRadioGroup.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
19
src/common/components/formFields/MyIconSelect.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
src/common/components/formFields/MyMoneyInput.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
5
src/common/components/formFields/MyPercentInput.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { InputNumber } from 'antd';
|
||||
|
||||
export function MyPercentInput(props: any) {
|
||||
return <InputNumber addonAfter="%" precision={2} {...props} />;
|
||||
}
|
||||
55
src/common/components/formFields/MyTreeCheckable.tsx
Normal 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}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
40
src/common/components/formFields/MyUploadImages.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
175
src/common/components/formFields/MyUploadImages.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
BIN
src/common/components/layout/AvatarIcon.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
68
src/common/components/layout/AvatarProps.tsx
Normal 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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
39
src/common/components/layout/ChangePassword.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
40
src/common/components/layout/MyCommonModal.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
149
src/common/components/layout/MyImportModal.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
31
src/common/components/layout/MyLoading.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
0
src/common/components/layout/MyLoginPage.tsx
Normal file
305
src/common/components/layout/MyPageContainer.tsx
Normal 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 };
|
||||
410
src/common/components/layout/MyPageContainerMore.tsx
Normal 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 };
|
||||
220
src/common/components/layout/MyPageContainerOne.tsx
Normal 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 };
|
||||
19
src/common/components/layout/MyRootContainer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
102
src/common/components/layout/usePageTabs.ts
Normal 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: () => '',
|
||||
};
|
||||
}
|
||||
102
src/common/components/layout/usePageTabsMore.ts
Normal 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: () => '',
|
||||
};
|
||||
}
|
||||
11
src/common/components/props/MyDrawerProps.ts
Normal 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,
|
||||
};
|
||||
13
src/common/components/props/MyModalFormProps.ts
Normal 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,
|
||||
};
|
||||
53
src/common/components/props/MyProTableProps.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
};
|
||||
330
src/common/components/schema/MyColumns.tsx
Normal 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,
|
||||
};
|
||||
},
|
||||
};
|
||||
184
src/common/components/schema/MyFormItems.tsx
Normal 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
@ -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';
|
||||
5
src/common/libs/umi/access.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export default function MyAccess({ children }: { children: ReactNode }) {
|
||||
return <div>MyAccess</div>;
|
||||
}
|
||||
13
src/common/libs/umi/allConfig.scss
Normal 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;
|
||||
}
|
||||
BIN
src/common/libs/umi/customer_wx_app.jpg
Normal file
|
After Width: | Height: | Size: 263 KiB |
BIN
src/common/libs/umi/employee_wx_app.jpg
Normal file
|
After Width: | Height: | Size: 263 KiB |
313
src/common/libs/umi/layoutConfig.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
BIN
src/common/libs/umi/logo.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
87
src/common/libs/umi/requestConfig.ts
Normal 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;
|
||||
},
|
||||
],
|
||||
};
|
||||
50
src/common/libs/valtio/actions.ts
Normal 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: [] };
|
||||
}
|
||||
},
|
||||
};
|
||||
60
src/common/libs/valtio/state.ts
Normal 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 };
|
||||
}
|
||||
36
src/common/pages/ForgotPasswordModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
151
src/common/pages/MyLoginPage.tsx
Normal 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
|
After Width: | Height: | Size: 172 KiB |
BIN
src/common/pages/loginBgImg.jpg
Normal file
|
After Width: | Height: | Size: 162 KiB |
72
src/common/typings.d.ts
vendored
Normal 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
@ -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) || '';
|
||||
}
|
||||
341
src/common/utils/flattenIterative.ts
Normal 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;
|
||||
}
|
||||
14
src/common/utils/mockHelper.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
54
src/common/utils/renderTextHelper.tsx
Normal 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));
|
||||
},
|
||||
};
|
||||
165
src/common/utils/rulesHelper.ts
Normal 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
@ -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,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
143
src/components/ModalsAssetItemsSelectList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
109
src/components/ModalsAssetsProjectSelectList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
138
src/components/ModalsContractsSelectList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
109
src/components/ModalsHouseSelectList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
715
src/components/ModalsMapLeaflet.tsx
Normal 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: '© 天地图',
|
||||
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>
|
||||
);
|
||||
}
|
||||
122
src/components/ModalsResourceSelectList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
59
src/components/MomentCategories.tsx
Normal 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,
|
||||
};
|
||||
},
|
||||
};
|
||||
45
src/components/MyExport.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
60
src/components/MyModal.tsx
Normal 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
276
src/components/SelectContract.tsx
Normal 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,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
53
src/components/ShowAttachments.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
90
src/components/SysSelects.tsx
Normal 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,
|
||||
};
|
||||
},
|
||||
};
|
||||
76
src/components/TransferProject.tsx
Normal 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;
|
||||
109
src/components/TransferUnits.tsx
Normal 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;
|
||||
79
src/components/TreeSelect.tsx
Normal 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,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||