first commit

This commit is contained in:
ylj20011123 2025-08-20 09:34:05 +08:00
commit 52d3d768e3
74 changed files with 18150 additions and 0 deletions

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

8
.eslintignore Normal file
View File

@ -0,0 +1,8 @@
/lambda/
/scripts
/config
.history
public
dist
.umi
mock

124
.eslintrc.json Normal file
View File

@ -0,0 +1,124 @@
{
"env": {
"browser": true,
"es2021": true
},
"settings": {
"react": {
"version": "detect"
}
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react",
"react-hooks",
"@typescript-eslint"
],
"rules": {
//eslint
//: https://eslint.org/docs/latest/
"indent": [
"error",
2,
{
"SwitchCase": 1,
"ignoredNodes": [ "ConditionalExpression" ]
}
],
"linebreak-style": [ "error", "windows" ],
"quotes": [ "error", "single" ],
"semi": [ "error", "always" ],
"arrow-parens": [ "error", "always" ],
"array-bracket-spacing": [
"error",
"always",
{
"singleValue": true,
"objectsInArrays": true,
"arraysInArrays": true
}
],
"object-curly-spacing": [
"error",
"always",
{
"arraysInObjects": true,
"objectsInObjects": true
}
],
"no-lonely-if": "error",
"comma-dangle": [
"error",
{
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "always-multiline"
}
],
"eol-last": [ "error", "always" ],
"no-multiple-empty-lines": [
"error",
{
"max": 1,
"maxEOF": 0
}
],
"space-before-function-paren": [
"error",
{
"named": "never",
"anonymous": "always",
"asyncArrow": "always"
}
],
"keyword-spacing": [
"error",
{
"before": true,
"after": true
}
],
"space-before-blocks": "error",
//eslint-plugin-react
//: https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules
"react/sort-comp": [ 2 ],
"react/jsx-curly-newline": [
"error",
{
"multiline": "consistent",
"singleline": "consistent"
}
],
"react/jsx-max-props-per-line": [
2,
{
"maximum": 1,
"when": "always"
}
],
"react/jsx-first-prop-new-line": [
"error",
"multiline-multiprop"
],
"react/self-closing-comp": [
"error",
{
"component": true,
"html": true
}
]
}
}

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
/node_modules
/.env.local
/.umirc.local.ts
/config/config.local.ts
/src/.umi
/src/.umi-production
/src/.umi-test
/dist
.swc
*.md
!README.md
!README-en_US.md
*.log
.env

201
README-en_US.md Normal file
View File

@ -0,0 +1,201 @@
[简体中文](./README.md) | English
The recommended `node` version is `18.14.2`, and the recommended `yarn` version is `1.22.x`
> October 8, 2024: Today, all dependencies were updated to the latest versions. The Node.js version used is the most recent one, 20.18.0. If you encounter issues with installing dependencies or running the program using Node.js 18.14.2, you can try using 20.18.0, or attempt using other versions.
# Overview
A lightweight development framework built with [umi4](https://umijs.org/), integrated conventional routing, custom layout(including custom routing rendering), retrieve menu data from server side, and global state management and permission verification, newest pkg and the best development experience, clone to use
# Git Work Flow
```
Legal format of commit log were shown as bellow(emoji and module are optional):
[<emoji>] [revert: ?]<type>[(scope)?]: <message>
💥 feat(module): added a great feature
🐛 fix(module): fixed some bugs
📝 docs(module): updated the documentation
🌷 UI(module): modified the style
🏰 chore(module): made some changes to the scaffold
🌐 locale(module): made some small contributions to i18n
Other commit types: refactor, perf, workflow, build, CI, typos, tests, types, wip, release, dep
```
# Mock
Built in `mock service`, start development even backend is not ready~~they always do that~~
## Interface Response Format
```
/**
* response body
* @description data
* @description code 0 success, other failure
* @description message
*/
type ResponstBody<T> = {
data: T;
code: number;
message: string;
};
```
This is a common response format, it's suit for most developer, and i choose it as a conventional response format
# Proxy
Nothing much to say, `proxy` is nessary for morden web app development
# Environment Variable
Custom environment variable should start with `UMI_APP_`, and write to `.env` file, so we can visit it via `process.env.UMI_APP_xxx`
Should i commmit `.env` file to my `git` repository? good question, refer to these 2 articles:
[Should I commit my .env file?](https://github.com/motdotla/dotenv#should-i-commit-my-env-file)
[Should I have multiple .env files?](https://github.com/motdotla/dotenv#should-i-have-multiple-env-files)
I agree not to commit the `.env` file to the repo and to have only one `.env` file, cuz the config in it is not safe for commit to repo, additionally, each environment has different config, we can share `.env` file separately with ohter collaborators via email or IM sorftware if we need to collaborate on development
In other words: one `.env` file on the developer's computer for development, and one `.env` file on the test server and one on the online server for testing and production packaging respectively.
# Route
Conventional routing. And which specific route is valid is depends on the data returned from menu interface, the data returned from menu interface will be displayed in the left menu bar. While a route(menu data) has been returned by an interface, it's considered valid since it's provided by the interface. However, due to the use of conventional routing, the route will be rendered correctlly only when it has been created in the project directory
Additionally, the route of login page doesn't need to be returned by menu interface(otherwise it will displayed in the left menu bar), it doesn't use route-based logic to determine it's location (cuz it's not returned by the menu interface). The specific routing and redirection logic can be viewed in `/src/utils/handleRedirect.ts`
# Menu
The menu is returned by server side, and stored in global state, the format of return data should like this: [ItemType](https://ant.design/components/menu#itemtype) which can be consumed by[Menu](https://ant.design/components/menu), and don't include `access` field any more. The menu data of current user is what can be accessed by current user, but not all operations within the page may be accessible to the current user. Page authentication mainly prevents the current user from opening other users' routes (such as opening bookmarks saved by other users). The permission content will be described later, and the `ts` definition for the menu is as bellow:
```
/**
* @description id Data's id
* @description pid Data's parent id
* @description key The unique identifier of the menu item, use `string` type instead `React.Key`:
https://ant.design/components/menu-cn#itemtype,
otherwise there may be a problem where the menu item can't be selected due to an incorrect key type
* @description lable Menu's title
* @description hideInMenu Should hidden in menu bar or not
* @description path Route path, each menu will have this field whether it has `children` field or not,
for menus without `children` field, it will redirect to this value, while for menus with `children` field,
it will redirect to `redirect` field. Cuz having `children` field which means this menu is expandable,
at this time, the `path` with `children` only represents it's position, rather than a truly valid route
* @description redirect Redirect route path, only the menu which has `children` field has this field,
when selectable menu whithin the `children` field of this menu, this value will be the
1st selectable menu's `path`, when there are no selectable menu in `children` field of this menu,
but still have `children` field, the value is the path of the first selectable menu item in its
children's children, that's say no matter how, this value is always the 1st valid route,
check menu data in the mock data out for specifics. Also, this field should be an optional field
theoretically, however, in order to make it easier for the backend to process, it's written as a
fixed field over here, in data where this field is not necessary, the backend can return an
empty string
* @description children Sub menu
*/
type MenuItem = {
id: number;
pid?: number;
key: string;
path: string;
redirect: string;
hideInMenu?: boolean;
label: React.ReactElement | string;
children?: MenuItem[];
}
```
# Layout
Abandoned the default `/src/app.tsx` layout and switched to using a custom layout: `/src/layouts/index.tsx`
## Implement Custom Rendering In Custom Layout
`/src/layouts/index.tsx` won't disturb the login page, beside: when user is login already, if user access login page again at this time(e.g. user manually enter the login page or access it through a bookmark) it will be redirected to a non-login page(if there is a `redirect` field then redirect to `redirect`, or redirect to index page), check this file out for specific codes: `/src/components/LayoutWrapper.tsx`
# Page Title
Using `umi4`'s global `title` config, each page's `location` info and menu data returned from interface implemented dynamic page title for each page, check this out for details: `/src/components/LayoutWrapper.tsx`
# Data Flow(State Management)
Using [dva](https://dvajs.com/) instead of `initial-state` of `umi4` ~~i won't tell u that it's because i abandoed the default `/src/app.tsx` layout, which resulted in the inability to use `initial-state` and `dva` is the only `react` state management solution i know how to use ;)~~ and we can consider combining [dva_umi4](https://umijs.org/docs/max/dva) and [@umijs/plugin-dva](https://v3.umijs.org/plugins/plugin-dva) together
# Request
The request lib is [axios](https://github.com/axios/axios) of course, and the structure of request codes are as bellow:
```
//...
.
├── src
│ ├── layouts
│ │ ├── index.tsx
│ │ ├── index.less
│ ├── services
│ │ └── user.ts
│ ├── pages
│ │ ├── index.tsx
│ │ ├── index.less
│ │ ├── pageA
│ │ │ └── index.tsx
│ │ │ └── index.less
│ │ │ ├── services.ts
//...
```
The request codes for `pageA` are all in `serveces.ts`
# Permission
```
/**
* Page permission type
* @description key is route's path, value is permisson array
*/
type PageAuthority = {
[path: string]: string[];
}
```
```
/**
* permission type for elements within a page
* @description key is permission name, value is a specific permission string
*/
type Authority = {
[key: string]: string;
}
```
The details of permission logic:
1. page permission(for entire page): `/src/components/PageAccess.tsx`
1. have permission: render page normally(`children`)
2. don't have permission: return[result_antd](https://ant.design/components/result-cn)component's[403](https://ant.design/components/result-cn#components-result-demo-403)result
The processing of page authorization has been placed in `/src/layouts/index.tsx`, cuz this component is the parent of all pages that require authorization processing, making it the most suitable location for handing this
2. page internal(for certain elements in the page): `/src/components/Access.tsx`
1. have permission: render elements normally(`children`)
2. don't have permission:
1. dont't have `fallback`: render nothing
2. have `fallback`: render `fallback`
The processing of certain elements's permission in the page requires using the `<Access />` component to be separately processed in each individual page
Both the user info and user permission returned by backend after login are stored in global state, which is `dva`
# Lint
This part refer to [12 essential ESLint rules for React](https://blog.logrocket.com/12-essential-eslint-rules-react/), and i have made some additional configurations as well:
```
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
```
And the shortcut config of course:
```
{
"key": "alt+f",
"command": "eslint.executeAutofix"
}
```
For me, this can imporve experience of programming greatly, i think it should be the same for you all, so i recommend everyone to config it this way
***Finally, happy hacking my friend ;)***

331
README.md Normal file
View File

@ -0,0 +1,331 @@
简体中文 | [English](./README-en_US.md)
`node`版本推荐使用`18.14.2`, `yarn`版本推荐使用`1.22.x`
> 2024年10月08日: 今天更新了所有依赖到最新, 使用的node是最新版的20.18.0, 如果18.14.2安装依赖或者运行的时候有问题可以尝试使用20.18.0, 或者尝试其他版本
# 概览
[umi4](https://umijs.org/)搭建的轻量级开发框架, 参考了如下项目框架/文档搭建而成:
1. `umi4`脚手架生成的`simple`, `antd pro`项目模板以及`umi4`的文档: [umi4](https://umijs.org/)
2. [antd pro](https://pro.ant.design/zh-CN)的脚手架工具`@ant-design/pro-cli`生成的`umi3``antd pro`项目, 选`umi4`提示无法安装全部区块, 因此这里我选的是`umi3`, 以及`antd pro`的文档: [antd pro](https://pro.ant.design/zh-CN/docs/overview)
3. 2018, 2019年使用`umi2`, `umi3`搭建的项目以及`umi3`的文档: [umi3](https://v3.umijs.org/zh-CN)
4. `procomponents`的文档: [procomponents](https://procomponents.ant.design/components)
在使用自带布局`/src/app.tsx`的时候传`headerRender`设置自定义头部, 结果实际渲染出来的自定义头部`position: fixed;`在原本的头部上方, 将原本的头部覆盖了, 原来的头部还在, 我以为是替换, 没想到是覆盖在上面, 然后由于是固定定位, 导致自定义头部不但覆盖住了原来的头部, 还将左侧的菜单和中间的内容也覆盖住了, 需要去`/src/global.less`中写样式覆盖, 这让我百思不得其解, 对布局有一些高度定制化的需求因此最终使用了自定义布局的方案
`antd pro`全量区块不支持`umi4`, 想着`umi4`出来了, 肯定有比`umi2` `umi3`优越的地方, 打算直接使用`umi4`
约定式路由是因为我从`umi2`开始就一直使用的约定式的路由, 配置式路由用起来比较繁琐, 各种配置
而至于数据流(状态管理)方案的选择, 主要还是因为自定义布局方案导致, 次要原因是我用`dva`也很久了, 觉得挺顺手, 但如果没有高度自定义的布局的需求, `umi4`官方提供的[数据流](https://umijs.org/docs/max/data-flow)方案是非常棒的, 轻量级的全局状态管理方案, 使用起来也很方便: 按照约定的方式书写代码, 以自定义`hooks`的形式创建`store`, 使用的时候用`umi4`提供的`api`: `useModel`就可以了, 还做了类似[reselect](https://github.com/reduxjs/reselect)的性能优化, 个人觉得这个方式摒弃了稍有门槛的`redux`的写法, 而是对用户侧做了一个收敛, 使得用户使用起来更方便, 也更易理解, 但依旧是我们所熟悉的`flux`的思想, 是`flux`思想的另一种实现, 这里要给`umi`团队一个大大的赞, 云谦大佬[sorrycc](https://github.com/sorrycc), 虎哥[xiaohuoni](https://github.com/xiaohuoni), 我尤其对虎哥的这个帖子印象深刻: [开发中遇到的问题,已经处理的,在这里记录一下。给朋友们一个参考](https://github.com/umijs/umi/issues/246), 想当年我刚开始用`umi`的时候是之前公司的一个大佬郭老师[dxcweb](https://github.com/dxcweb)推荐我用的, 而虎哥的这个帖子给了我很大的帮助, 瑞思拜
以及控制台打开看到`antd`的各种已经废弃的报错, 虽然不影响使用, 但多了很多不必要的`error`, 严重影响开发时候的调试工作, 这是由于`antd`废弃了一些`api`, 而项目中还在使用导致的, 这个问题修改起来比较繁琐, 同时还有上面提到的几个点, 于是就有了这个项目
# Git工作流
这个地方用的依旧是[husky](https://github.com/typicode/husky)还有[lint-staged](https://github.com/okonet/lint-staged), 但只保留了提交的消息格式的校验(个人喜欢在开发的时候规范代码风格并调整, 在提交之前做校验体验不是太好, 这个可以因人而异进行调整), 这里我用的是[@umijs/fabric](https://github.com/umijs/fabric)工具集, `@umijs/fabric`也是从上面的参考中找到的. 以及提交消息格式也是参考了上面的项目([antd pro](https://pro.ant.design/zh-CN)), 详情可查看[Git Commit Message Convention](https://github.com/vuejs/core/blob/main/.github/commit-convention.md), 常用的提交格式如下:
```
合法的提交日志格式如下(emoji 和 模块可选填)
[<emoji>] [revert: ?]<type>[(scope)?]: <message>
💥 feat(模块): 添加了个很棒的功能
🐛 fix(模块): 修复了一些 bug
📝 docs(模块): 更新了一下文档
🌷 UI(模块): 修改了一下样式
🏰 chore(模块): 对脚手架做了些更改
🌐 locale(模块): 为国际化做了微小的贡献
其他提交类型: refactor, perf, workflow, build, CI, typos, tests, types, wip, release, dep
```
也可以看看这个: [How to Write Better Git Commit Messages A Step-By-Step Guide](https://www.freecodecamp.org/news/how-to-write-better-git-commit-messages/)
# Umi4配置
默认用的是`.umirc.ts`, 但还需要代理配置, 因此就放`config`目录了, 这样比较清晰, 也方便维护, 至于项目中获取配置, 我这里采用的是没用`defineConfig`的方式, 也可以将配置提取出来, 这样其他地方就都能用了, 参考这个: [有人知道什么方法能获取defineConfig的配置?](https://github.com/umijs/umi/discussions/7534#discussioncomment-4858138)
## 插件
这里并未使用[Umi Max](https://umijs.org/docs/max/introduce), 而是只使用了`umi4``dva`插件, 同时项目里也安装了, 根据文档[@umijs/plugin-dva](https://v3.umijs.org/zh-CN/plugins/plugin-dva)可知最终会优先使用项目中依赖的版本
# Mock
`/mock`这里配置的是`mock数据的服务`, 即本地的`express`接口服务, 详情可查看官方文档: [mock_umi4](https://umijs.org/docs/guides/mock), 只要项目根目录中有`/mock`目录且里面有[mock文件](https://umijs.org/docs/guides/mock#mock-%E6%96%87%E4%BB%B6), 那么该功能就会自动启动, 该服务和前端应用运行在同一个域名(不会引起`跨域`的问题)下, 因此当该服务启动了, 请求工具的`baseUrl``/`, 此时我们请求`/xxx`就会先到这个服务中进行检索, 匹配到了就走这个服务的接口, 否则就看代理: 如果代理功能开启, 且匹配到了代理服务就走代理, 否则就`404`, 以及`mock`服务的配置和开启不需要额外装`express`, 为了让`ts`类型检查不报错, 可以装个`@types/express`: `$ yarn add @types/express --dev`
## 接口返回格式
这里和[统一接口规范_antd pro](https://pro.ant.design/zh-CN/docs/request#%E7%BB%9F%E4%B8%80%E6%8E%A5%E5%8F%A3%E8%A7%84%E8%8C%83)有一定出入, 但也可以因人而异做修改, 这是项目中的定义:
```
/**
* 响应体
* @description data 数据
* @description code 返回码: 0 成功, 其他 失败
* @description message 消息: 返回的消息
*/
type ResponstBody<T> = {
data: T;
code: number;
message: string;
};
```
`T`类型变量因实际返回的数据类型不同而不同, `message`字段告知前端该操作的一个结果描述, 成功或者失败的描述信息都使用这个字段
这个格式的数据需要在使用`antd``Table`或者`ProTable`的时候做一下处理, 也可以直接使用这个格式:
```
{
list: any[],
current?: number,
pageSize?: number,
total?: number,
}
```
这样更方便`antd``Table`使用, 但个人还是更倾向于一开始的格式, 所有数据格式都统一了, 而且也不是所有页面都有表格, 况且转换的操作也容易, 不过这个也因人而异, 可以自行修改
# 代理
详情可查看官方文档: [proxy_umi4](https://umijs.org/docs/guides/proxy#%E4%BB%A3%E7%90%86), 这里主要提一下: 当后端接口还没写好的时候我们单独使用`mock`功能即可, 不需要用代理, 因为最终的`target`不可用, 是`404`, 毕竟还没开发好, 以及如`mock`部分所述, `mock`和代理都启用, 优先级是: `mock` > 代理
# 环境变量
自定义环境变量应以`UMI_APP_`开头, 并写到`.env`中, 这样才能在代码中通过`process.env.UMI_APP_xxx`访问到, 项目中的`.env`文件目前只有一个值: `UMI_APP_BASEURL=/`, 克隆之后需要在项目中创建一个`.env`文件并在其中写入`UMI_APP_BASEURL=/`
如果不想以`UMI_APP_`开头, 则需要在`.env`中写完之后在配置的`define`中做配置, 比如:
`.env`:
```
domain=http://example.com
```
`/config/config.ts`:
```
//...
define: {
"process.env": {
domain: process.env.domain
}
}
//...
```
这样项目中才能访问到`process.env.domain`, 以及如果配置中的`define`做了如上那样`process.env`的配置, 那所有环境变量(包括以`UMI_APP_`开头的环境变量)都要配置到其中, 不然访问`process.env`的时候将只能访问到`define`中配置的值, 因为这样配置之后`process.env`被覆盖了
更便捷的配置可以这样来:
```
//...
define: {
"process.env": process.env
}
//...
```
这样所有环境变量都能通过`process.env`访问了, 无论是自带的还是自定义的
有需要的朋友还可以看看这两个`issue`:
[无法配置自定义的环境变量](https://github.com/umijs/umi/issues/7799)
[config中设置define后命令行中设置的环境变量在app.tsx中无法找到](https://github.com/umijs/umi/issues/8329)
以及, 关于是否应该提交`.env`文件和是否应该有多个`.env`文件的问题可以看看这两个描述:
[Should I commit my .env file?](https://github.com/motdotla/dotenv#should-i-commit-my-env-file)
[Should I have multiple .env files?](https://github.com/motdotla/dotenv#should-i-have-multiple-env-files)
个人觉得也不应该提交`.env`以及只有一个`.env`即可, 因为里面的配置提交到库中不安全, 同时每个部署环境都有不同的配置, 协作开发的话单独发即可, 也就是说: 开发者电脑中放一个`.env`文件用于开发, 测试服务器和线上服务器也各放一个`.env`文件, 分别用于测试环境打包和生产环境打包
但这个情况也不绝对, 需要在不同环境中使用不同配置, 推荐通过`umi`自带的环境变量`UMI_ENV`来完成, 详情可以查看官方文档: [UMI_ENV](https://umijs.org/docs/guides/env-variables#umi_env), 也可以结合这两个来看:
[umi_env 目前好像是覆盖方式,可以支持合并方式吗](https://github.com/umijs/umi/issues/2050)
[请问umi4还支持多config目录下多环境配置吗](https://github.com/umijs/umi/discussions/8341)
以及`config`目录下的多环境配置我试了下暂时不行, 也可能是我姿势不对: [请问umi4还支持多config目录下多环境配置吗#discussioncomment-4807605](https://github.com/umijs/umi/discussions/8341#discussioncomment-4807605), 根目录下`.umirc.ts`的我也没能成功进行多环境的配置, 了解用法的朋友希望能不吝赐教
[为什么运行的是 prod 配置文件而不是 stage 配置文件?"build:stage": "UMI_ENV=stage max build"](https://github.com/umijs/umi/discussions/11636)
# 路由
使用的是约定式路由. 路由功能的提供, `umi4`使用的是`react-router6`, 官方文档是这个: [React Router](https://reactrouter.com/en/main), 关于约定式路由的嵌套问题可以看这个: [约定式路由无法生成嵌套路由!!](https://github.com/umijs/umi/issues/8850)
以及具体哪一条路由有效, 是由菜单接口返回的数据决定的, 菜单接口返回的数据会显示在左侧菜单栏中, 当一条路由(菜单数据)被接口返回了, 也就是由接口提供了, 那它就是有效的, 但由于使用了约定式路由, 只有当这条路由同时还在项目目录中被创建了, 它才能正常渲染
默认情况下, 一条路由哪怕菜单接口没提供, 但在项目中被创建了, 那它也能被正常渲染, 只是左侧菜单栏中就无法显示了, 但这不符合逻辑: 一个用户能访问的路由应该在该用户登录之后由菜单接口返回, 并且在菜单栏中显示(一些需要在菜单栏中隐藏的菜单除外), 除去需要隐藏在菜单栏中的菜单之外, 其他没在菜单栏中显示的菜单表示该用户无法访问, 即使是项目中创建了, 也就是说此时手动输入`url`或者更常见的是通过收藏栏访问都无法访问, 都应该显示`404`, 而这个功能项目中也做了处理, 简单来说就是:
- 菜单中的数据: 用来显示到左侧菜单栏中, 表示当前登录用户所能访问的页面, 菜单数据中没有的页面, 哪怕实际存在但都会显示`404`, 因为菜单数据中没有表示该页面无法被当前登录用户访问到
- 项目中的目录(约定式路由): 通过目录和文件及其命名分析出路由配置从而使得路由能正常渲染页面, 目录数量要`>=`菜单数据, 这才能保证菜单能符合预期地渲染或者不渲染
当然了, 还有一种情况就是菜单接口返回了某条路由, 而项目中没有对应的页面, 此时也是`404`, 这是`umi4`自带的`404`功能, 这里只能处理项目中有, 而菜单数据中有(渲染页面)或者没有(渲染`404`页面)的情况
另外登录页的路由不需要菜单接口返回(不然登录页就会显示在左侧菜单位置了), 登录页建好就行, 它不走路由判断逻辑(因为它不由菜单接口返回), 具体的路由判断跳转的逻辑可以查看`/src/utils/handleRedirect.ts`
## 在react组件之外进行跳转操作
这里`umi4`依旧保留了原来的`history` `api`: [history_umi4](https://umijs.org/docs/api/api#history), 详细的`api`的使用可以看这个: [history API Reference](https://github.com/remix-run/history/blob/main/docs/api-reference.md)
# 菜单
菜单由服务端返回, 也是存到全局状态中, 返回的数据的结构要是[Menu](https://ant.design/components/menu-cn)能消费的[ItemType](https://ant.design/components/menu-cn#itemtype), 同时不再包含`access`字段, 当前用户的菜单就是当前用户能访问的了, 只是页面内的操作不全是当前用户都能操作的, 页面鉴权主要是防止当前用户打开其他用户的路由(比如打开了其他用户存的书签)这样的情况, 权限内容在后面有描述, 以及菜单的`ts`定义如下:
```
/**
* 菜单项
* @description id 数据库中数据的id
* @description pid 数据库中数据的id(父级的id)
* @description key 菜单项的唯一标志, 使用string类型代替React.Key:
* https://ant.design/components/menu-cn#itemtype, 不然会出现key类型不对导致的菜单项无法被选中的问题
* @description lable 菜单的标题
* @description hideInMenu 在菜单中隐藏
* @description path 路由路径,
* 有无children的菜单都会有这个字段, 无children的菜单跳转这个值, 有children的跳redirect,
* 因为有children表示这个菜单是可展开的, 此时有children的path只是表示它的一个位置, 而非真正有效的路由
* @description redirect 重定向路由路径,
* 只有有children的菜单有, 当这个菜单的children中有可选中的菜单时, 这个值为第一个可选中的菜单的path,
* 当这个菜单的children中没可以选中的菜单, 而是还有children时,
* 该值就是它children中的children的第一个可选中的菜单的path,
* 就是无论如何, 这个值都是第一个有效路由, 具体可看mock数据中的菜单数据
* 以及这个字段理论上来说应该是可选的字段, 但为了让后端容易处理, 这里写成固定有的字段,
* 在不需要这个字段的数据中后端返回空串即可
* @description children 子菜单
*/
type MenuItem = {
id: number;
pid?: number;
key: string;
path: string;
redirect: string;
hideInMenu?: boolean;
label: React.ReactElement | string;
children?: MenuItem[];
}
```
## 菜单图标
关于菜单图标的显示问题, 有需要的朋友可以参考如下几个`issue`:
[从服务端请求菜单时 icon 和 access 不生效](https://github.com/ant-design/ant-design-pro/issues/8101)
[菜单栏的三级菜单使用自定义icon无法显示一二级菜单显示正常](https://github.com/ant-design/ant-design-pro/issues/9267)
[关于V5动态菜单图标的优雅解决问题](https://github.com/ant-design/ant-design-pro/issues/10158)
[从服务端请求菜单一级菜单icon生效二级菜单不生效](https://github.com/ant-design/ant-design-pro/issues/10178)
# 布局
舍弃了自带的`/src/app.tsx`布局, 转而使用自定义的布局: `/src/layouts/index.tsx`, 这个方式比较符合我这边项目的需求, 而且`issue`里面看到也有不少小伙伴有需求, 同时也是因为没使用自带的`layout`, `404`页面需要自己实现一下, 这个比较简单, 就不展开了
## 自定义布局中做自定义渲染
在布局中有些根据路由信息做自定义渲染的需求可以看看这个: [自定义layout组件props里拿不到config的routes没办法自己实现菜单的渲染](https://github.com/umijs/umi/issues/10177), 其中我个人也回复了一下, 大意是使用`useLocation`来实现, 这是它的官方文档: [useLocation_React Router](https://reactrouter.com/en/main/hooks/use-location), 项目中我也是这么处理的
这里除了登录页不走`/src/layouts`之外, 我还做了额外的处理: 当用户已经登录, 此时如果再次访问登录页(比如用户手动输入或者通过书签进入登录页)会做重定向到非登录页(有`redirect`则重定向到`redirect`, 没有则到首页)的操作, 具体代码可以查看这个文件: `/src/components/LayoutWrapper.tsx`
# 标题
配置文件中有`title`配置项: [title_umi4](https://umijs.org/docs/api/config#title), 这个配置的是全局的标题, 每个页面都会使用这个标题, 如果需要动态配置标题, 每个页面不同, 则需要使用`Helmet`: [helmet_umi4](https://umijs.org/docs/api/api#helmet), 再配合当前页面的`location`信息和接口返回的菜单数据就可以了, 详情可以查看: `/src/components/LayoutWrapper.tsx`
# 数据流(状态管理)
由于舍弃了自带的`/src/app.tsx`布局使用自定义布局, 因此就没法使用自带的`initial-state`方案了, 作为从`umi`刚问世不久就开始使用`umi`的用户, 我个人更倾向于[dva](https://dvajs.com/), 这个方案需要配置开启, 并新建`/src/models`目录, 关于`dva`的解释除了`dva`官方文档之外, `umi4`的这个文档解释的也很清楚, 可以结合起来看: [dva_umi4](https://umijs.org/docs/max/dva), 同时还有`umi3`的文档可供参考: [@umijs/plugin-dva](https://v3.umijs.org/zh-CN/plugins/plugin-dva)
# 请求
这里的请求库用的是[axios](https://github.com/axios/axios), 这个库我从`umi2`一直用到现在, 之前写过几个`vue`的项目, 使用的也是这个, 由于之前项目的`axios`的配置可以直接复制过来, 以及这里没用`@umijs/max`, `umi`自带的请求方案无法发挥它的长处, 因此就直接使用`axios`
请求代码的组织是`umi`一直保留的一个特性, 也是个人觉得很棒的一个设计, 就是将所有的请求都放到`/src/services`中, 确切的说是全局的(比如`登录`, `登出`, `请求菜单`等)放到`/src/services`目录中, 其余各个页面独有的请求则和页面文件放到一起, 比如:
```
//...
.
├── src
│ ├── layouts
│ │ ├── index.tsx
│ │ ├── index.less
│ ├── services
│ │ └── user.ts
│ ├── pages
│ │ ├── index.tsx
│ │ ├── index.less
│ │ ├── pageA
│ │ │ └── index.tsx
│ │ │ └── index.less
│ │ │ ├── services
│ │ │ │ └── pageA.ts
//...
```
或者直接用文件而不是目录:
```
//...
.
├── src
│ ├── layouts
│ │ ├── index.tsx
│ │ ├── index.less
│ ├── services
│ │ └── user.ts
│ ├── pages
│ │ ├── index.tsx
│ │ ├── index.less
│ │ ├── pageA
│ │ │ └── index.tsx
│ │ │ └── index.less
│ │ │ ├── services.ts
//...
```
或者叫其他名字也行(比如`api.ts`), 建议就按照`umi`的约定使用`services`, 这样更统一, 也更易维护, 以及请求文件虽以`.ts`结尾, 但里面不包含`jsx`元素, 不会被注册为路由, 因此这么写没问题, 关于约定式路由的判断规则可以看这个: [约定式路由_umi3](https://v3.umijs.org/zh-CN/docs/convention-routing#%E7%BA%A6%E5%AE%9A%E5%BC%8F%E8%B7%AF%E7%94%B1)
# 权限
这个地方是个重点, 同时也是一个需要自己实现的地方, 因为自带的权限控制需要`initial-state`, 而这个`initial-state`又依赖自带的`/src/app.tsx`布局, 刚好这里使用的是自定义的布局, 因此最终只能自己实现
这个逻辑在后端自然是`RBAC`的方案, 而前端关注的主要则是具体的权限, 具体逻辑如下:
1. 前后端约定每个页面的权限, 这里包括页面访问权限, 就是路由的权限和页面内各个操作元素的权限, 并在页面上写好, 代码里是写在`authority.ts`中(以`object`的形式定义), 以`_`开头是因为这样才不会被算作一个路由
2. 后端返回当前登录用户的所有权限(类型是`string[]`), 前端取到之后和`authority.ts`中的做对比, 从而达到鉴权的目的
权限我分成了页面和页面内元素的权限, 具体代码在这: 页面权限: `/src/components/PageAccess.tsx`, 页面内元素的权限: `/src/components/Access.tsx`, 前端权限的声明, 页面权限在这: `/src/pages/authority.ts`, 各个页面内权限写在各个页面的目录中, 比如: `/src/pages/about/m/authority.ts`
页面权限需要根据不同的路由来决定, 因此它的类型定义如下:
```
/**
* 页面权限类型
* @description key是路由path, value是权限数组
*/
type PageAuthority = {
[path: string]: string[];
}
```
而页面内元素的权限又有所不同, 一个个元素, 需要一个个独立的权限, 它的类型定义如下:
```
/**
* 权限类型
* @description key是权限名称, value是具体的权限字符串
*/
type Authority = {
[key: string]: string;
}
```
参考`umi4`的[access_umi4](https://umijs.org/docs/max/access)文档自己实现了一个鉴权的组件:
1. 页面权限: `/src/components/PageAccess.tsx`
1. 有权限: 正常渲染页面(`children`)
2. 没权限: 返回[result_antd](https://ant.design/components/result-cn)组件的[403](https://ant.design/components/result-cn#components-result-demo-403)结果
页面鉴权的处理放到了`/src/layouts/index.tsx`中, 因为这个组件是所有需要做鉴权处理的页面的父级, 在这处理最合适不过了
2. 页面内部: `/src/components/Access.tsx`
1. 有权限: 正常渲染元素(`children`)
2. 没权限:
1. 没`fallback`: 什么都不渲染
2. 有`fallback`: 渲染`fallback`
页面内权限的处理需要使用`<Access />`组件在各个页面中单独处理
登录之后后端返回的用户信息和用户权限都放到全局状态也就是`dva`
# TS Config
这个来自[antd pro](https://pro.ant.design/zh-CN)的脚手架工具`@ant-design/pro-cli`生成的`umi3``antd pro`项目当中, 功能完备, 只做了一个修改: 将`ts`的类型声明文件的路径写到了里面的`include`字段中, 也就是将全局的`d.ts`文件放到了根目录
# Lint
这部分参考了[12 essential ESLint rules for React](https://blog.logrocket.com/12-essential-eslint-rules-react/), 最终配置和`umi`自带的有很大的不同, 这里我只使用了`eslint`, 并且做了配置:
```
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
```
以及快捷键的设置:
```
//eslint格式化代码快捷键
{
"key": "alt+f",
"command": "eslint.executeAutofix"
}
```
`alt+f`就自动修复问题, `ctrl+s`保存的时候自动修复问题并保存, 极大提升编码体验, 但我个人的习惯是按`alt+f`修复问题, 写完了再保存, 修复是修复, 保存是保存, 快捷键是否设置看个人喜好, 但保存的时候自动修复问题建议设置一下
`eslint`我调了两天才生效, 而且是莫名其妙就生效了, 不知道为何...
配置是通过`$ eslint --init`生成, 并且添加了热门流行的`eslint` `react`插件, `ts`插件, 以及个人习惯的一些规则, 这些规则可以自行调整, 同时需要留意的是, 如果在`lint`代码的时候报如下的错误:
`Failed to apply ESLint fixes to the document. Please consider opening an issue with steps to reproduce.`
这大概率是`eslint`的规则配置错了, 目前该仓库的`eslint`是没问题的, 当修改或者添加规则之后报错, 则应仔细检查规则是否修改/添加正确, 这里附上`eslint`的官方文档以供查阅: [ESLint](https://eslint.org/), 其他插件的文档可通过[npm](https://www.npmjs.com/)搜索之后在右侧`Homepage`位置找到官方文档

26
cloudNew/199.async.js Normal file

File diff suppressed because one or more lines are too long

51
cloudNew/322.async.js Normal file

File diff suppressed because one or more lines are too long

1
cloudNew/390.async.js Normal file
View File

@ -0,0 +1 @@
"use strict";(self.webpackChunk=self.webpackChunk||[]).push([[390],{96390:function(e,n,t){t.r(n),t.d(n,{default:function(){return s}});var o=t(67294),u=t(30518),E=t(85893);function s(){var O=(0,u.useOutletContext)();return(0,E.jsx)(u.Outlet,{context:O})}}}]);

295
cloudNew/800.async.js Normal file

File diff suppressed because one or more lines are too long

BIN
cloudNew/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
cloudNew/favicon.svg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

15
cloudNew/index.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="shortcut icon" href="/favicon.ico">
<title>驿商云平台</title>
<link rel="stylesheet" href="/cloudMenu/umi.css">
</head>
<body>
<div id="root"></div>
<script src="/cloudMenu/umi.js"></script>
</body>
</html>

View File

@ -0,0 +1 @@
"use strict";(self.webpackChunk=self.webpackChunk||[]).push([[717],{52936:function(se,T,t){t.r(T),t.d(T,{default:function(){return te}});var O=t(97857),f=t.n(O),F=t(5574),C=t.n(F),g=t(67294),A=t(26058),Z=t(92398),u=t(30518),B=t(22181),L=t(83622),R={"/test/index":["/test/index"],"/Invoicing/index":["/Invoicing/index"]},M=R,a=t(85893),H=function(e){var n,s=e.user,o=e.children,l=s.authority,c=(0,u.useLocation)(),i=c.pathname,p=(n=M[i])===null||n===void 0?void 0:n.some(function(y){return l.includes(y)});console.log("authority",M),console.log("userAuthority",l),console.log("children",o);var d=o;return p||(d=(0,a.jsx)(B.ZP,{title:"403",status:"403",subTitle:"\u5BF9\u4E0D\u8D77, \u60A8\u6CA1\u6709\u8BBF\u95EE\u6B64\u9875\u9762\u7684\u6743\u9650",extra:(0,a.jsx)(L.ZP,{type:"primary",onClick:function(){return u.history.push("/")},children:"\u8FD4\u56DE\u9996\u9875"})})),d},K=(0,u.connect)(function(r){var e=r.user;return{user:e}})(H),N=t(74330),V={"/auth":{target:"https://es.robot-z.cn",changeOrigin:!0,secure:!1,pathRewrite:{"^/auth":""},onProxyRes:function(e){e.headers["Access-Control-Allow-Origin"]="*",e.headers["Access-Control-Allow-Methods"]="GET,PUT,POST,DELETE,OPTIONS",e.headers["Access-Control-Allow-Headers"]="Content-Type, Authorization",e.headers["Access-Control-Allow-Credentials"]="true"}}},W=V,z=[{path:"/",routes:[{path:"/test/index",name:"\u6D4B\u8BD5\u9875\u9762",component:"@/pages/test/index"},{path:"/Invoicing/index",name:"\u5F00\u7968\u7BA1\u7406",component:"@/pages/Invoicing/index"}]}],G={plugins:["@umijs/plugins/dist/dva"],proxy:W,dva:{},base:"/cloudMenu/",publicPath:"/cloudMenu/",outputPath:"dist",title:"\u9A7F\u5546\u4E91\u5E73\u53F0",favicons:["/favicon.ico"],routes:z,history:{type:"browser"}},U=function(e){var n,s=(0,u.useLocation)(),o=s.pathname,l=e.dispatch,c=e.children,i=e.user,p=i.isLogin,d=i.layoutWrapperLoading,y=i.indexValidMenuItemByPath,P=(n=y[o])===null||n===void 0?void 0:n.label,I=G.title,E=P?"".concat(P," - ").concat(I):I;(0,g.useEffect)(function(){l==null||l({type:"user/getUserInfoAuthorityMenu",payload:{type:"relay"}})},[l]);var h=(0,a.jsx)(N.Z,{size:"large",style:{width:"100vw",height:"100vh",display:"flex",alignItems:"center",justifyContent:"center"}});return d||(p?o!=="/user/login"&&(h=c):h=(0,a.jsx)("div",{})),(0,a.jsxs)(g.Fragment,{children:[(0,a.jsx)(u.Helmet,{children:(0,a.jsx)("title",{children:E})}),h]})},$=(0,u.connect)(function(r){var e=r.user;return{user:e}})(U),Y=function(){return(0,a.jsx)(B.ZP,{title:"404",status:"404",subTitle:"\u5BF9\u4E0D\u8D77, \u60A8\u8BBF\u95EE\u7684\u9875\u9762\u4E0D\u5B58\u5728",extra:(0,a.jsx)(L.ZP,{type:"primary",onClick:function(){return u.history.push("/")},children:"\u8FD4\u56DE\u9996\u9875"})})},J=Y,Q=t(64599),X=t.n(Q),k=function r(e,n){var s=[],o=X()(e),l;try{for(o.s();!(l=o.n()).done;){var c=l.value;c.hideInMenu||(c.children?s.push(f()(f()({},c),{},{children:r(c.children,n)})):s.push(n(c)))}}catch(i){o.e(i)}finally{o.f()}return s},w=k,q=t(60199),ue=A.Z.Header,_=A.Z.Content,oe=Z.Z.TabPane,ie=function(e){if(!e.length)return[""];if(e.length===1)return e.map(function(o){return"".concat(o.key)});for(var n=[],s=0;s<e.length-1;s++)n.push("".concat(e[s].key));return n},ee=function(e){var n=(0,g.useState)(!1),s=C()(n,2),o=s[0],l=s[1],c=(0,u.useLocation)(),i=c.pathname;console.log("pathname",i);var p=e.dispatch,d=e.user,y=d.menu,P=d.rootSubmenuKeys,I=d.indexAllMenuItemById,E=d.indexValidMenuItemByPath,h=e.global.tabsRoutes,m=E[i],ae=(0,g.useState)((m==null?void 0:m.path)||"/"),b=C()(ae,2),le=b[0],ne=b[1],ce=w(y,function(v){return f()(f()({},v),{},{name:v.path?(0,a.jsx)(u.Link,{to:v.path,style:{color:i===v.path?"#1890ff":"inherit"},className:i===v.path?"ant-menu-item-selected":"",children:v.name}):v.name})}),de=(0,u.useLocation)(),he=function(x){p({type:"global/changeTabsRoutes",payload:{data:x,action:"add"}})},ve=function(x,D){if(D==="remove"&&p){var j=h.findIndex(function(re){return re.path===x}),S=h[j+1]?h[j+1].path:h[j-1]?h[j-1].path:"";u.history.push(S||"/"),ne(S),p({type:"global/changeTabsRoutes",payload:{data:[x],action:D}})}},pe=[{SYSTEMMODULE_DESC:"",guid:"1",hideInMenu:!1,name:"\u6D4B\u8BD5",path:"/test/index"}];return(0,a.jsx)($,{children:(0,a.jsx)(q.f,{menuRender:!1,children:(0,a.jsx)(A.Z,{children:(0,a.jsx)(_,{children:m?(0,a.jsx)(K,{children:(0,a.jsx)(u.Outlet,{})}):(0,a.jsx)(J,{})})})})})},te=(0,u.connect)(function(r){var e=r.user,n=r.global;return{user:e,global:n}})(ee)}}]);

View File

@ -0,0 +1 @@
.pageLayout .ant-layout .ant-layout-sider{background-color:#021529!important}.pageLayout .ant-layout .ant-layout-sider .ant-layout-sider-children{padding:0}.pageLayout .ant-layout .ant-layout-sider .ant-layout-sider-children .ant-pro-sider-logo{display:flex;align-items:center}.pageLayout .ant-layout .ant-layout-sider .ant-layout-sider-children .ant-pro-sider-logo a{display:block}.pageLayout .ant-layout .ant-layout-sider .ant-layout-sider-children .ant-pro-sider-logo a h1{color:#fff;font-weight:600;font-size:18px;vertical-align:none;margin-left:12px}.pageLayout .ant-layout .ant-layout-sider .ant-layout-sider-children ul li span,.pageLayout .ant-layout .ant-layout-sider .ant-layout-sider-children ul li i{font-size:14px;color:#ffffffa6}.pageLayout .pageLayout :global .ant-pro-menu-item.ant-menu-item-selected{background-color:#1890ff!important}.pageLayout .pageLayout :global .ant-pro-menu-item.ant-menu-item-selected .ant-pro-menu-item-title{color:#fff!important}.pageLayout .pageLayout :global .ant-pro-menu-item.ant-menu-item-selected:hover{background-color:#1890ff!important}.pageLayout .pageLayout :global .ant-menu-item-selected{background-color:#1890ff!important;color:#fff!important}.pageLayout .pageLayout :global .ant-menu-item-selected:hover{background-color:#1890ff!important;color:#fff!important}.pageLayout .pageLayout :global .ant-menu-item-selected a{color:#fff!important}.pageLayout .pageLayout :global .ant-menu-submenu-selected>.ant-menu-submenu-title{color:#1890ff}.pageLayout .pageLayout :global .ant-menu-item:hover{background-color:#1890ff1a}.pageLayout .ant-pro-layout-container{background-color:#f0f2f5}.pageLayout .ant-pro-layout-container main{padding:0}.pageLayout .tab-extra{width:13px;height:15px;padding:0 26px 16px 0}.pageLayout .ant-modal-header{background-color:#ecf2f7!important;border-bottom:none!important}.pageLayout body *::-webkit-scrollbar{width:8px;height:8px}.pageLayout body *::-webkit-scrollbar-thumb{background-color:#d1cfcf;border-radius:10px}.pageLayout body *::-webkit-scrollbar-track{background:#ededed;border-radius:10px;box-shadow:inset 0 0 5px #00000003}.pageLayout .pageTable-leftnav .ant-tree-list-holder{height:calc(100vh - 257px);padding-right:0;padding-bottom:0;overflow-y:auto}.pageLayout .leftHeight .ant-tree-list-holder{height:100%!important}.pageLayout .leftHeight .ant-tree-list{height:560px}.pageLayout .pageTable-leftnavs .ant-tree-list-holder{height:580px;padding-right:0;padding-bottom:0;overflow-y:auto}.pageLayout .pageTable-leftnav .ant-pro-card-header.ant-pro-card-header-border{padding-top:29px}.pageLayout .pageTable-leftnav .ant-pro-card-title{font-size:14px}.pageLayout .pageTable-leftnav .ant-pro-card-split-vertical{transition:width .3s}.pageLayout .pageTable-leftnav .ant-tree .ant-tree-treenode,.pageLayout .pageTable-leftnavs .ant-tree .ant-tree-treenode{padding-bottom:16px}.pageLayout .ant-table-tbody>tr.tablerow-no-border>td{border-bottom:0}.pageLayout .ant-pro-list .ant-pro-card-header{padding:0!important}.pageLayout .ant-pro-list .ant-pro-card.ant-pro-card-border.ant-pro-card-hoverable>.ant-pro-card-body{margin:0!important;padding:20px 0 0!important}.pageLayout .ant-pro-list .ant-pro-card.ant-pro-card-border>ul.ant-pro-card-actions{margin-top:20px}.pageLayout .ant-pro-list .ant-pro-card.ant-pro-card-border>ul.ant-pro-card-actions>li{margin:0}.pageLayout .ant-pro-list .ant-pro-card.ant-pro-card-border.ant-pro-card-hoverable>ul.ant-pro-card-actions>li,.pageLayout .ant-pro-list .ant-pro-card .ant-pro-card-actions .ant-space-item{margin:10px 0 0!important}.main-tab>.ant-tabs-nav{margin-bottom:0!important;padding:2px 15px 0 16px;background-color:#fff}.main-tab>.ant-tabs-nav .ant-tabs-tab{background-color:#fafafa;border-color:#f0f0f0;border-bottom:0}.main-tab>.ant-tabs-nav .ant-tabs-tab button svg{transition:all .3s}.main-tab>.ant-tabs-nav .ant-tabs-tab:hover{background-color:#fafafa;border-color:#f0f0f0}.main-tab.ant-tabs-top>.ant-tabs-nav .ant-tabs-tab-active{background-color:#f6f9fb;border-color:#f0f0f0;box-shadow:2px 0 6px #e7e7e7;border-top-color:#3591fe}.main-tab.ant-tabs-top>.ant-tabs-nav .ant-tabs-tab-active:before{content:"";position:absolute;display:block;background-color:#3591fe;top:0;left:0;width:100%;height:1px}.main-tab>.ant-tabs-content-holder{height:calc(100vh - 86px);padding-top:16px;overflow-y:auto}.main-tab>.ant-tabs-nav .ant-tabs-nav-wrap{bottom:1px}.ant-pro-layout .ant-pro-layout-bg-list,.ant-pro-layout-bg-list{background:#f5f5f5!important}

1
cloudNew/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" version="1.1" viewBox="0 0 200 200"><title>Group 28 Copy 5</title><desc>Created with Sketch.</desc><defs><linearGradient id="linearGradient-1" x1="62.102%" x2="108.197%" y1="0%" y2="37.864%"><stop offset="0%" stop-color="#4285EB"/><stop offset="100%" stop-color="#2EC7FF"/></linearGradient><linearGradient id="linearGradient-2" x1="69.644%" x2="54.043%" y1="0%" y2="108.457%"><stop offset="0%" stop-color="#29CDFF"/><stop offset="37.86%" stop-color="#148EFF"/><stop offset="100%" stop-color="#0A60FF"/></linearGradient><linearGradient id="linearGradient-3" x1="69.691%" x2="16.723%" y1="-12.974%" y2="117.391%"><stop offset="0%" stop-color="#FA816E"/><stop offset="41.473%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient><linearGradient id="linearGradient-4" x1="68.128%" x2="30.44%" y1="-35.691%" y2="114.943%"><stop offset="0%" stop-color="#FA8E7D"/><stop offset="51.264%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient></defs><g id="Page-1" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><g id="logo" transform="translate(-20.000000, -20.000000)"><g id="Group-28-Copy-5" transform="translate(20.000000, 20.000000)"><g id="Group-27-Copy-3"><g id="Group-25" fill-rule="nonzero"><g id="2"><path id="Shape" fill="url(#linearGradient-1)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C99.2571609,26.9692191 101.032305,26.9692191 102.20193,28.1378823 L129.985225,55.8983314 C134.193707,60.1033528 141.017005,60.1033528 145.225487,55.8983314 C149.433969,51.69331 149.433969,44.8756232 145.225487,40.6706018 L108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/><path id="Shape" fill="url(#linearGradient-2)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C100.999864,25.6271836 105.751642,20.541824 112.729652,19.3524487 C117.915585,18.4685261 123.585219,20.4140239 129.738554,25.1889424 C125.624663,21.0784292 118.571995,14.0340304 108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/></g><path id="Shape" fill="url(#linearGradient-3)" d="M153.685633,135.854579 C157.894115,140.0596 164.717412,140.0596 168.925894,135.854579 L195.959977,108.842726 C200.659183,104.147384 200.659183,96.5636133 195.960527,91.8688194 L168.690777,64.7181159 C164.472332,60.5180858 157.646868,60.5241425 153.435895,64.7316526 C149.227413,68.936674 149.227413,75.7543607 153.435895,79.9593821 L171.854035,98.3623765 C173.02366,99.5310396 173.02366,101.304724 171.854035,102.473387 L153.685633,120.626849 C149.47715,124.83187 149.47715,131.649557 153.685633,135.854579 Z"/></g><ellipse id="Combined-Shape" cx="100.519" cy="100.437" fill="url(#linearGradient-4)" rx="23.6" ry="23.581"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
cloudNew/umi.css Normal file
View File

@ -0,0 +1 @@
html,body{padding:0;margin:0}*{box-sizing:border-box}

158
cloudNew/umi.js Normal file

File diff suppressed because one or more lines are too long

37
cloudNew/web.config Normal file
View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="Handle History Mode and custom 404/500" stopProcessing="true">
<match url="(.*)" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="./" />
</rule>
</rules>
</rewrite>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="2147483647" maxQueryString="2147483647"/>
</requestFiltering>
</security>
<httpProtocol>
<customHeaders>
<add name="X-UA-Compatible" value="IE=Edge"/>
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Headers" value="*" />
<add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE" />
</customHeaders>
</httpProtocol>
<modules runAllManagedModulesForAllRequests="true" />
<handlers>
<remove name="ExtensionlessUrlHandler-Integrated-4.0" />
<remove name="OPTIONSVerbHandler" />
<remove name="TRACEVerbHandler" />
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>
</system.webServer>
</configuration>

23
config/config.ts Normal file
View File

@ -0,0 +1,23 @@
// import { defineConfig } from "umi";
import proxy from './proxy';
import router from './router'
//不使用defineConfig则能在其他地方获取这些配置
export default {
plugins: [
'@umijs/plugins/dist/dva'
],
proxy,
dva: {},
base: '/cloudMenu/',
publicPath: '/cloudMenu/',
outputPath: 'dist', // 打包输出目录默认是dist
title: '驿商云平台',
favicons: [
'/favicon.ico'
],
routes: router,
history: {
type: 'browser' // 使用browser history模式配合IIS的URL重写规则
}
};

17
config/proxy.ts Normal file
View File

@ -0,0 +1,17 @@
const proxy = {
'/auth': {
// target: 'http://home.robot-z.cn:7001',
target: 'https://es.robot-z.cn',
changeOrigin: true,
secure: false,
pathRewrite: { '^/auth': '' },
onProxyRes: function (proxyRes) {
proxyRes.headers['Access-Control-Allow-Origin'] = '*';
proxyRes.headers['Access-Control-Allow-Methods'] = 'GET,PUT,POST,DELETE,OPTIONS';
proxyRes.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization';
proxyRes.headers['Access-Control-Allow-Credentials'] = 'true';
}
}
}
export default proxy;

25
config/router.ts Normal file
View File

@ -0,0 +1,25 @@
// 路由配置文件
export default [
{
path: '/',
routes: [
{
path: '/test/index',
name: '测试页面',
component: "@/pages/test/index",
},
{
path: '/Invoicing/index',
name: '开票管理',
component: "@/pages/Invoicing/index",
},
{
path: '/DigitalElectronics/index',
name: '开票管理',
component: "@/pages/DigitalElectronics/index",
}
]
}
]

254
mock/user.ts Normal file
View File

@ -0,0 +1,254 @@
import type { Request, Response } from 'express';
const waitTime = (time: number = 100) => (
new Promise((resolve) => {
setTimeout(
() => {
resolve(true);
},
time
)
})
);
const handleCommonRes = (data: Record<string, unknown> | Record<string, unknown>[], code = 0) => ({
data,
code,
message: !code ? '成功' : '失败'
});
const userApi = {
//登录
'POST /api/user/login': async (req: Request, res: Response) => {
const { password, username, mobile, captcha } = req.body;
await waitTime(2000);
switch (true) {
case username === 'admin' && password === 'ant.design':
case username === 'user' && password === 'ant.design':
case /^1\d{10}$/.test(mobile) && Boolean(captcha):
res.send(
handleCommonRes({
token: 'Bearer xxx'
})
);
return;
}
res.send(
handleCommonRes(
{
token: 'Wrong Bearer'
},
10001
)
);
},
//用户信息
'GET /api/user/info': async (req: Request, res: Response) => {
await waitTime(2000);
if (!req.headers.authorization) {
res.status(401).send();
return;
}
res.send(
handleCommonRes({
name: 'Serati Ma',
avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁金服某某某事业群某某平台部某某技术部UED',
tags: [
{
key: '0',
label: '很有想法的',
},
{
key: '1',
label: '专注设计',
},
{
key: '2',
label: '辣~',
},
{
key: '3',
label: '大长腿',
},
{
key: '4',
label: '川妹子',
},
{
key: '5',
label: '海纳百川',
},
],
notifyCount: 12,
unreadCount: 11,
country: 'China',
geographic: {
province: {
label: '浙江省',
key: '330000',
},
city: {
label: '杭州市',
key: '330100',
},
},
address: '西湖区工专路 77 号',
phone: '0752-268888888',
})
);
},
//用户权限
'GET /api/user/authority': async (req: Request, res: Response) => {
await waitTime(1500);
res.send(
handleCommonRes({
authority: [
'/',
'/about/u/index',
'/about/u/2',
'/about/m',
'/about/um',
'/teacher/u',
'/teacher/m',
'/teacher/um',
'/student',
'/about/m/update'
]
})
);
},
//菜单
'GET /api/user/menu': async (req: Request, res: Response) => {
await waitTime(1000);
res.send(
handleCommonRes([
{
id: 1,
key: '1',
path: '/',
redirect: '',
name: '首页'
},
{
id: 2,
key: '2',
name: '关于',
path: '/about',
redirect: '',
children: [
{
id: 21,
key: '2-1',
name: '关于你',
path: '/about/u',
redirect: '',
pid: 2,
children: [
{
id: 211,
key: '2-1-1',
name: '关于你1',
path: '/about/u/index',
redirect: '',
pid: 21
},
{
id: 212,
key: '2-1-2',
name: '关于你2',
path: '/about/u/2',
redirect: '',
pid: 21
}
]
},
{
id: 22,
key: '2-2',
path: '/about/m',
redirect: '',
name: '(页面元素权限)关于我',
pid: 2
},
{
id: 23,
key: '2-3',
path: '/about/um',
redirect: '',
name: '关于你和我',
pid: 2
}
]
},
{
id: 3,
key: '3',
name: '教师',
path: '/teacher',
redirect: '',
children: [
{
id: 31,
key: '3-1',
path: '/teacher/u',
redirect: '',
name: '(403)关于你教师',
pid: 3
},
{
id: 32,
key: '3-2',
path: '/teacher/m',
redirect: '',
name: '关于我教师',
pid: 3
},
{
id: 33,
key: '3-3',
path: '/teacher/um',
redirect: '',
name: '关于你和我教师',
pid: 3
}
]
},
{
id: 4,
key: '4',
name: '(404)学生',
path: '/student',
redirect: '',
}
])
);
},
//登出
'POST /api/user/logout': (req: Request, res: Response) => {
res.send(
handleCommonRes({})
);
},
//验证码
'GET /api/user/captcha': async (req: Request, res: Response) => {
await waitTime(2000);
return res.send(
handleCommonRes({
captcha: 'captcha-xxx'
})
);
}
};
export default userApi;

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"private": true,
"author": "小镇靓仔 <misunderstandjerry@qq.com>",
"scripts": {
"dev": "umi dev",
"build": "umi build",
"setup": "umi setup",
"postinstall": "umi setup",
"prepare": "husky"
},
"dependencies": {
"@ant-design/icons": "^5.5.1",
"@ant-design/pro-components": "^2.8.6",
"antd": "^5.21.2",
"axios": "^1.7.7",
"crypto-js": "^4.2.0",
"dva": "^3.0.0-alpha.1",
"moment": "^2.30.1",
"qrcode": "^1.5.4",
"umi": "^4.3.24",
"xlsx": "^0.18.5",
"xlsx-style-fixed": "^0.0.4"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.8.1",
"@typescript-eslint/parser": "^8.8.1",
"@umijs/fabric": "^4.0.1",
"@umijs/plugins": "^4.3.24",
"eslint": "^9.21.0",
"eslint-plugin-react": "^7.37.1",
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.6",
"typescript": "^5.6.2"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/favicon.svg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

1
public/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" version="1.1" viewBox="0 0 200 200"><title>Group 28 Copy 5</title><desc>Created with Sketch.</desc><defs><linearGradient id="linearGradient-1" x1="62.102%" x2="108.197%" y1="0%" y2="37.864%"><stop offset="0%" stop-color="#4285EB"/><stop offset="100%" stop-color="#2EC7FF"/></linearGradient><linearGradient id="linearGradient-2" x1="69.644%" x2="54.043%" y1="0%" y2="108.457%"><stop offset="0%" stop-color="#29CDFF"/><stop offset="37.86%" stop-color="#148EFF"/><stop offset="100%" stop-color="#0A60FF"/></linearGradient><linearGradient id="linearGradient-3" x1="69.691%" x2="16.723%" y1="-12.974%" y2="117.391%"><stop offset="0%" stop-color="#FA816E"/><stop offset="41.473%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient><linearGradient id="linearGradient-4" x1="68.128%" x2="30.44%" y1="-35.691%" y2="114.943%"><stop offset="0%" stop-color="#FA8E7D"/><stop offset="51.264%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient></defs><g id="Page-1" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><g id="logo" transform="translate(-20.000000, -20.000000)"><g id="Group-28-Copy-5" transform="translate(20.000000, 20.000000)"><g id="Group-27-Copy-3"><g id="Group-25" fill-rule="nonzero"><g id="2"><path id="Shape" fill="url(#linearGradient-1)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C99.2571609,26.9692191 101.032305,26.9692191 102.20193,28.1378823 L129.985225,55.8983314 C134.193707,60.1033528 141.017005,60.1033528 145.225487,55.8983314 C149.433969,51.69331 149.433969,44.8756232 145.225487,40.6706018 L108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/><path id="Shape" fill="url(#linearGradient-2)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C100.999864,25.6271836 105.751642,20.541824 112.729652,19.3524487 C117.915585,18.4685261 123.585219,20.4140239 129.738554,25.1889424 C125.624663,21.0784292 118.571995,14.0340304 108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/></g><path id="Shape" fill="url(#linearGradient-3)" d="M153.685633,135.854579 C157.894115,140.0596 164.717412,140.0596 168.925894,135.854579 L195.959977,108.842726 C200.659183,104.147384 200.659183,96.5636133 195.960527,91.8688194 L168.690777,64.7181159 C164.472332,60.5180858 157.646868,60.5241425 153.435895,64.7316526 C149.227413,68.936674 149.227413,75.7543607 153.435895,79.9593821 L171.854035,98.3623765 C173.02366,99.5310396 173.02366,101.304724 171.854035,102.473387 L153.685633,120.626849 C149.47715,124.83187 149.47715,131.649557 153.685633,135.854579 Z"/></g><ellipse id="Combined-Shape" cx="100.519" cy="100.437" fill="url(#linearGradient-4)" rx="23.6" ry="23.581"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

37
public/web.config Normal file
View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="Handle History Mode and custom 404/500" stopProcessing="true">
<match url="(.*)" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="./" />
</rule>
</rules>
</rewrite>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="2147483647" maxQueryString="2147483647"/>
</requestFiltering>
</security>
<httpProtocol>
<customHeaders>
<add name="X-UA-Compatible" value="IE=Edge"/>
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Headers" value="*" />
<add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE" />
</customHeaders>
</httpProtocol>
<modules runAllManagedModulesForAllRequests="true" />
<handlers>
<remove name="ExtensionlessUrlHandler-Integrated-4.0" />
<remove name="OPTIONSVerbHandler" />
<remove name="TRACEVerbHandler" />
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>
</system.webServer>
</configuration>

0
src/assets/.gitkeep Normal file
View File

BIN
src/assets/login-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

37
src/assets/logo.svg Normal file
View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="35px" height="30px" viewBox="0 0 35 30" enable-background="new 0 0 35 30" xml:space="preserve"> <image id="image0" width="35" height="30" x="0" y="0"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAeCAYAAACmPacqAAAABGdBTUEAALGPC/xhBQAAACBjSFJN
AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAA
B3RJTUUH5QkBEx07b/a9lAAABN5JREFUWMO1l1tsVUUUhr+16ZVyCbESEi/RB1BAtPVBg0ZAgyjB
qBFFuRoRUe4FVIiJGJUEXoxBQaNAJDTcJFK5aJSbRZ7QBzSIYGgowSZiKBcFbaGc9fuw99lnn/aU
tiDzMvvMnj3zzT//zFrHJNFWSU2dUyrxNDAMqRy4GQiQHKhDHAD2gKryPl1S0+aArRS7HMylKXOK
kN4FpgHFpPvGnyjzHL/TTmBh3soP9/xvMJdemX090g7grjYmTwAq/on0OTAtf9Wy+quCuTS5IgB2
gwZ3YPKW76XfgRH5lR8fuHKYl2aNQ6pMDH4CcRBUK3ESaEIqBW5CuhfoGU2eC7ge8UD+2k8OXxnM
izOqEadAX0l8n//Z0lZN2TR+iiHdB8xGjMxsZVhH4x9GlBdsWN7YYZimF6Z3z/9s6V/tkTbru9GT
H5a0FihNAkVKVRRsXLmkwzBXUy6OmjQAaS/QvZl/jgJzkb4rqFqVc6E5Yfou/7uXxDi5PYK4TW69
5OQjzsutTs5PyHbKqTr6WufTLYBGTnwWaX0IoqiKt+8iYiOwqHDL6oOtwvRbdTYftwUSr8qtCAfJ
kANuSCC3UP2wrVFimdzerp1XfC458IUnn69GDG5xHWS2LwVajHir8Os1qSyY/qvPFiK2yW2oBGkQ
PAQI20KwuD0G5YBkD9bOKz4Vwzw+4VakERHQcERJMw+l1VoHjC/6dl0qiCUyLcMYaibMACOuM8+C
AMwUtUV9AwaYkWXOwi2rawu3Vi4t3Fb5DCm/AfePlHKUcuRCKQcXuI8m5W/EygxYe2aonB0ovfLM
tmRtT1KNqC18NuRIsltqXy8+3pq5G4c9Nx9pUdZJC+uLiN5B5Jx3sldOpk6oQMC/mLabsRpjO8bZ
hIpmpscud9KKtq9fjPubuENKpGu5F8h9ct6dG07fbcZABaFPDCGzuCYAc5Cx3tD036Z2i33R+4Pz
RQpsnLnek1k3oKyto1+0a+PChiEj+yLGZDwEoOEBpqeIFMisspknTF9YwJjDL2dAAI7M7NJYM6tk
BcYgAp02o2dbMACkNAf3C7E67uDqE5gxJGnQlubVBTNmHJrUrdXbsaai5GczRmHq1B6W4r2b/sRV
HUGkgboEGH3MFJ2oJFBaHbb9OrH7H21NUFNRssuMqnYpA+B+Ahfy6HS5GvLMuE6EWyIMA2RhfwuE
3H5o9wQB69rbVa7SZlH+SB4W3Y+W6GkhVgTZ7jSyZlZJQweU6Zt9xNkXgE5aBJMxLkn/dG33BB0p
rrqMZwTuywOMQ7lPUgz06LWB8TlIx3A1Is3tvP+bH62s6uR8iUVhIhQGwfimDW9gSTz0y9ge1dcE
KlECjEozmmI1oHk8MjO23rHmzIRrDWOSKP+y/n1EhUQYZ6JbMROdScef/ZKtwNktcSKKW13l1qkd
6UayLSW3c+l3kjUdX1D4Twizub4EsU+iP82B0gM2C5BxoOx4utGyTdYgMTjOZ8o319+I2CFxe1KV
GCQrkuccMJw8oUYSMBs+rVLmO2R743xm/xOldRj3mLESi+69HPlMjnCR9c6CzGmM+0ZtOW735Nj3
58yBy6vq+0nMRDZKokcL/6RXllAnVCDXtrTMh9LjJBVDHLvsv4OyTfWFyAZKDEKUya03opdkpa37
p1nylcs/nnPbx/4HXgSzauZflRIAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMDktMDFUMTE6Mjk6
NTkrMDg6MDCXs8feAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTA5LTAxVDExOjI5OjU5KzA4OjAw
5u5/YgAAACB0RVh0c29mdHdhcmUAaHR0cHM6Ly9pbWFnZW1hZ2ljay5vcme8zx2dAAAAGHRFWHRU
aHVtYjo6RG9jdW1lbnQ6OlBhZ2VzADGn/7svAAAAF3RFWHRUaHVtYjo6SW1hZ2U6OkhlaWdodAAz
MMb6mdgAAAAWdEVYdFRodW1iOjpJbWFnZTo6V2lkdGgAMzVOP63aAAAAGXRFWHRUaHVtYjo6TWlt
ZXR5cGUAaW1hZ2UvcG5nP7JWTgAAABd0RVh0VGh1bWI6Ok1UaW1lADE2MzA0NjY5OTka9aiIAAAA
EnRFWHRUaHVtYjo6U2l6ZQAxMzg5QkJc76D9AAAARnRFWHRUaHVtYjo6VVJJAGZpbGU6Ly8vYXBw
L3RtcC9pbWFnZWxjL2ltZ3ZpZXcyXzlfMTYzMDQ2NDI3MzkwMjUxNjNfMzVfWzBdmaJpYAAAAABJ
RU5ErkJggg==" ></image>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

1
src/assets/upMenu.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1705300526702" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="23980" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M128 128h768v16H128zM128 320h768v16H128zM128 496h768v16H128z" fill="#707070" p-id="23981"></path><path d="M128.008 508.32l10.323-10.324 387.643 387.642-10.324 10.324z" fill="#707070" p-id="23982"></path><path d="M885.72 498.022l10.324 10.323-387.643 387.643-10.323-10.323z" fill="#707070" p-id="23983"></path></svg>

After

Width:  |  Height:  |  Size: 649 B

32
src/components/Access.tsx Normal file
View File

@ -0,0 +1,32 @@
import type { FC, ReactElement } from 'react';
import { connect } from 'umi';
import type { UserConnectedProps } from '@/models/user';
type Props = {
authority: string;
children: ReactElement;
fallback?: ReactElement;
} & UserConnectedProps;
//处理页面元素权限的组件
const Access: FC<Props> = (props): ReactElement | null => {
const { user, authority, fallback, children } = props;
const { authority: userAuthority } = user;
const accessible = userAuthority.some((item: string) => item === authority);
let res = null;
if (accessible) {
res = children;
} else if (fallback) {
res = fallback;
}
return res;
};
export default connect(
({ user }: { user: UserConnectedProps['user'] }) => ({
user,
}),
)(Access);

View File

@ -0,0 +1,18 @@
@import '@/global.less';
.avatar-container{
height: 100%;
padding: 0 10px;
cursor: pointer;
.avatar{
display: block;
width: 30.9px;
height: 30.9px;
}
.username{
color: #fff;
}
&:hover {
// background: #252a3d;
}
}

View File

@ -0,0 +1,59 @@
import type { FC } from 'react';
import { connect } from 'umi';
import { Dropdown, Space } from 'antd';
import { LogoutOutlined } from '@ant-design/icons';
import type { UserConnectedProps } from '@/models/user';
import './index.less';
const Avatar: FC<UserConnectedProps> = (props) => {
const {
user: { data }, dispatch,
} = props;
const handleLogout = () => {
dispatch?.({
type: 'user/logout',
});
};
const items = [
{
key: 'logout',
label: (
<a
onClick={handleLogout}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '0 8px',
}}
>
<LogoutOutlined />
<span>退</span>
</a>
),
},
];
return (
<Dropdown
menu={{ items }}
>
<Space className="avatar-container">
<img
alt="avatar"
src={data.avatar}
className="avatar"
/>
<span className="username">{data.name}</span>
</Space>
</Dropdown>
);
};
export default connect(
({ user }: { user: UserConnectedProps['user'] }) => ({
user,
}),
)(Avatar);

47
src/components/Footer.tsx Normal file
View File

@ -0,0 +1,47 @@
/*
* @Author: cclu 1106109051@qq.com
* @Date: 2025-02-25 11:39:58
* @LastEditors: cclu 1106109051@qq.com
* @LastEditTime: 2025-02-26 11:25:46
* @FilePath: \umi4-admin-main\src\components\Footer.tsx
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import type { FC } from 'react';
import { DefaultFooter } from '@ant-design/pro-layout';
const Footer: FC = () => {
return (
<DefaultFooter
style={{ background: "#f0f2f5", color: "rgba(0, 0, 0, 0.45)" }}
copyright={`Copyright © 2013-${new Date().getFullYear()} Eshang Cloud. All Rights Reserved. 驿商云 版权所有`}
/>
// <DefaultFooter
// style={{
// backgroundColor: 'inherit',
// }}
// copyright={`${currentYear} ${defaultMessage}`}
// links={[
// {
// key: 'Ant Design Pro',
// title: 'Ant Design Pro',
// href: 'https://pro.ant.design',
// blankTarget: true,
// },
// {
// key: 'github',
// title: <GithubOutlined />,
// href: 'https://github.com/ant-design/ant-design-pro',
// blankTarget: true,
// },
// {
// key: 'Ant Design',
// title: 'Ant Design',
// href: 'https://ant.design',
// blankTarget: true,
// },
// ]}
// />
);
};
export default Footer;

View File

@ -0,0 +1,103 @@
/*
* @Author: cclu 1106109051@qq.com
* @Date: 2025-03-24 14:04:10
* @LastEditors: cclu 1106109051@qq.com
* @LastEditTime: 2025-03-24 14:22:13
* @FilePath: \menuCloud\src\components\LayoutWrapper.tsx
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import { Fragment, useEffect } from 'react';
import type { FC, ReactElement } from 'react';
import { Spin } from 'antd';
import { Helmet, useLocation, connect } from 'umi';
import type { UserConnectedProps } from '@/models/user';
import cfg from '../../config/config';
type Props = {
children: ReactElement;
} & UserConnectedProps;
//登录页不走/src/layouts, 登录完之后的页面才走/src/layouts
const LayoutWrapper: FC<Props> = (props) => {
const { pathname } = useLocation();
const {
dispatch, children,
user: { isLogin, layoutWrapperLoading, indexValidMenuItemByPath },
} = props;
const tabTitle = indexValidMenuItemByPath[pathname]?.label;
const projectTitle = cfg.title;
const title = tabTitle ? `${tabTitle} - ${projectTitle}` : projectTitle;
useEffect(
() => {
//调用户信息相关接口查询登录状态(401表示未登录)
dispatch?.({
type: 'user/getUserInfoAuthorityMenu',
payload: {
type: 'relay',
},
});
},
[dispatch],
);
//进任何页面都需要先loading
let pageContent = (
<Spin
size="large"
style={{
width: '100vw',
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
/>
);
//页面不loading
/**
:
if(!layoutWrapperLoading) {
if(isLogin){
if(pathname === '/user/login') {
, url还是登录页的时候我希望不再进入登录页而是直接跳转离开
handleRedirect();
使history.push则会报如下的错误:
Warning: Cannot update a component (`BrowserRoutes`) while rendering a different component (`LayoutWrapper`).
To locate the bad setState() call inside `LayoutWrapper`, follow the stack trace as described in
https://reactjs.org/link/setstate-in-render
render的时候更新路由, history.push更新BrowserRoutes
使window.location.href = 'xxx';loading,
, : /src/models/user.ts
handleRedirect
}
}
}
*/
if (!layoutWrapperLoading) {
if (isLogin) {
if (pathname !== '/user/login') {
pageContent = children;
}
} else {
//只要没登陆过就渲染登录页而不是跳转到登录页, 因为登录页不走/src/layouts
pageContent = <div></div>;
}
}
return (
<Fragment>
<Helmet>
<title>{title}</title>
</Helmet>
{pageContent}
</Fragment>
);
};
export default connect(
({ user }: { user: UserConnectedProps['user'] }) => ({
user,
}),
)(LayoutWrapper);

View File

@ -0,0 +1,24 @@
.container {
display: flex;
justify-content: space-between;
height: 50px;
padding: 0 15px;
// background-color: #001529;
.former {
display: flex;
.left {
display: flex;
align-items: center;
img {
width: 28px;
height: 28px;
display: block;
}
}
}
.latter {
display: flex;
align-items: center;
gap: 0 15px;
}
}

View File

@ -0,0 +1,43 @@
import type { FC } from 'react';
import { Link } from 'umi';
import Avatar from '../Avatar';
import './index.less';
const Nav: FC = () => {
return (
<div className="container">
<div className="former">
{/* <div className="left">
<Link
to="/"
style={{
display: 'flex',
alignItems: 'center',
gap: '0 15px',
lineHeight: 1,
}}
>
<img
alt="logo"
src="/logo.svg"
className="logo"
/>
<h1
style={{
margin: 0,
color: '#fff',
fontSize: '18px',
}}
>UMI4 Admin</h1>
</Link>
</div> */}
</div>
<div className="latter">
<Avatar />
</div>
</div>
);
};
export default Nav;

View File

@ -0,0 +1,59 @@
/*
* @Author: cclu 1106109051@qq.com
* @Date: 2025-02-25 11:39:58
* @LastEditors: cclu 1106109051@qq.com
* @LastEditTime: 2025-03-10 15:30:28
* @FilePath: \umi4-admin-main\src\components\PageAccess.tsx
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import type { FC, ReactElement } from 'react';
import { connect, history, useLocation } from 'umi';
import { Button, Result } from 'antd';
import authority from '@/pages/authority';
import type { UserConnectedProps } from '@/models/user';
type Props = {
children: ReactElement;
} & UserConnectedProps;
//处理页面权限的组件, 放置在所有需要鉴权的页面的最外层
const PageAccess: FC<Props> = (props): ReactElement | null => {
const { user, children } = props;
const { authority: userAuthority } = user;
const { pathname } = useLocation();
//可选操作符用来处理子路由跳转的情形, 因为这里的权限都是有效路由的权限,
//当遇到输入的是菜单组件中可展开的节点的path的时候就没有权限了, 此时可能会报错,
//加可选操作符可避免这个报错导致的页面渲染问题
const accessible = authority[pathname]?.some((item: string) => userAuthority.includes(item));
console.log('authority', authority);
console.log('userAuthority', userAuthority);
console.log('children', children);
let res = children;
if (!accessible) {
res = (
<Result
title="403"
status="403"
subTitle="对不起, 您没有访问此页面的权限"
extra={(
<Button
type="primary"
onClick={
() => history.push('/')
}
></Button>
)}
/>
);
}
return res;
};
export default connect(
({ user }: { user: UserConnectedProps['user'] }) => ({
user,
}),
)(PageAccess);

View File

@ -0,0 +1,241 @@
import { connect } from "umi";
// import type { ConnectState } from "@/models/connect";
import ProCard from "@ant-design/pro-card";
// import searchIcon from '@/assets/ai/searchIcon.png'
import { useRef, useState } from "react";
import { MenuFoldOutlined } from "@ant-design/icons";
import ProForm, { ProFormText } from "@ant-design/pro-form";
import { Button, Col, FormInstance, Row, Tree } from "antd";
// import { getServerpartTree } from "@/services/options";
import useRequest from "@ahooksjs/use-request";
import { getMerchantShopTree, getServerpartTree } from "./service";
// import './style.less'
// import { getMerchantShopTree } from "@/pages/Setting/Users/service";
type DetailProps = {
setSelectedId: any; // 把选择的服务区 可以带给外层
reload?: boolean; // 选择服务区 是否可以刷新组件之外的内容
actionRef?: any; // 和reload配合使用 确认要刷新的内容
currentUser: any; // 当前用户的信息
width?: number; // 组件的宽度
otherFun?: any; // 点击之后要进行的其他操作 多个操作可以写在一个方法里面传进来
setCollapsible: any; // 是否收缩组件
collapsible: boolean; // 收缩组件的判断依据
haveTest?: boolean;// 是否有测试服务区
handleGetLeftTreeData?: any // 拿到树数据的方法 必须要有输出值的
noWj?: any // 把万佳商贸隐藏
}
const LeftSelectTree = ({ setSelectedId, reload, actionRef, currentUser, width, otherFun, setCollapsible, collapsible, haveTest, handleGetLeftTreeData, noWj }: DetailProps) => {
console.log('currentUser', currentUser);
const searchTreeRef = useRef<FormInstance>();
// 默认的服务区树
const [allTreeViews, setAllTreeViews] = useState<any>()
// 是否要显示全部
const [isShowAllInTree, setIsShowAllInTree] = useState<boolean>(false)
// 加载服务区树
const { loading: treeLoading, data: treeViews } = useRequest(async () => {
let data: any = []
if (currentUser?.UserPattern === 2000) {
data = await getMerchantShopTree({ BusinessManId: currentUser?.BusinessManID, ShowShop: false });
} else {
data = await getServerpartTree(currentUser?.provinceCode, currentUser?.CityAuthority, true, true, true, false, 1000)
}
console.log('datatree', data);
// 判断是否有多个片区的权限
if (data && data.length > 1) {
// 那么就显示全部
setIsShowAllInTree(true)
}
let list: any = []
data.forEach((item: any) => {
// 判断 item这一层已经是片区了 如果item.children 只有一个的话 那就说明 也只有一个服务区 那么就可以判断不显示全部和片区的树形选择层了
if (item.children && item.children.length === 1) {
list.push(item.children[0])
} else {
if (haveTest) {
if (item.value !== 424) {
list.push(item)
}
} else {
if (item.value !== 424 && item.value !== 586) {
list.push(item)
}
}
}
})
if (noWj) {
list = handleFilterList(list, 89)
}
console.log('list2', list);
if (handleGetLeftTreeData) {
let newData: any = await handleGetLeftTreeData()
console.log('newData', newData);
setTreeView(newData)
} else {
setTreeView(list)
}
setAllTreeViews(list)
return data
})
// 显示服务区树搜索框
const [showServiceSearchBox, setShowServiceSearchBox] = useState<boolean>(false)
// 实际显示在左侧的服务区树
const [treeView, setTreeView] = useState<any>()
// 树要展开的行
const [treeShowRow, setTreeShowRow] = useState<any>()
// 筛选左侧的服务区树
const handleFilterServiceTree = async (value?: string) => {
const resList: any = JSON.parse(JSON.stringify(allTreeViews))
setSelectedId('')
if (resList && resList.length > 0 && value) {
setTreeView([])
const list: any = []
resList.forEach((item: any) => {
if (item.label.indexOf(value) !== -1) {
list.push(item)
} else {
if (item.children && item.children.length > 0) {
const childrenList: any = []
item.children.forEach((subItem: any) => {
if (subItem.label.indexOf(value) !== -1) {
childrenList.push(subItem)
}
})
item.children = childrenList
if (childrenList && childrenList.length > 0) {
list.push(item)
}
}
}
})
if (list && list.length > 0) {
const keyList: any = ['0-0']
list.forEach((item: any) => {
keyList.push(item.key)
})
setTreeShowRow(keyList)
}
setTimeout(() => {
setTreeView(list)
}, 100)
} else {
setTreeView([])
setTreeShowRow([])
setTreeView(allTreeViews)
}
}
// 根据传入的服务区id筛选剔除掉这个服务区
const handleFilterList = (list: any, id: any) => {
let res: any = []
list.forEach((item: any) => {
if (item.value === id) {
} else {
res.push(item)
}
})
console.log('res', res);
return res
}
return (
<div>
<ProCard
style={{ width: !collapsible ? width ? `${width}px` : "300px" : "60px" }}
className="pageTable-leftnav"
bodyStyle={{ padding: 0, paddingTop: 20, paddingLeft: 20, width: !collapsible ? width ? `${width}px` : "300px" : "60px" }}
extra={<div className="leftSelectBox">
{/* src={searchIcon} */}
<img className="searchIcon" onClick={() => {
setShowServiceSearchBox(true)
}} />
<MenuFoldOutlined onClick={() => {
setCollapsible(!collapsible);
}} />
<div className="fixedBox" style={{ display: showServiceSearchBox ? 'flex' : 'none', width: width ? `${width}px` : '275px' }}>
<ProForm
formRef={searchTreeRef}
layout={'horizontal'}
submitter={{
render: () => {
return []
}
}}
isKeyPressSubmit
onFinish={(values: any) => {
return handleFilterServiceTree(values?.searchValue || '')
}}
>
<Row>
<Col span={15} className={'noBottom'}>
<ProFormText
name={'searchValue'}
fieldProps={{
placeholder: '请输入服务区名称'
}}
allowClear
/>
</Col>
<Col span={2}></Col>
<Col span={5}>
<Button type={'primary'} onClick={() => {
searchTreeRef.current?.submit()
}}></Button>
</Col>
</Row>
</ProForm>
<img style={{ width: '20px', height: '20px', cursor: 'pointer', marginLeft: '5px' }} src={close} onClick={() => {
setShowServiceSearchBox(false)
}} />
</div>
</div>}
colSpan={!collapsible ? "300px" : "60px"}
title={!collapsible ? "请选择服务区" : ""}
headerBordered
collapsed={collapsible}
>
{treeView && treeView.length > 0 ? <Tree
checkable
treeData={isShowAllInTree ? [{
label: '全部',
value: 0,
key: '0-0',
children: treeView
}] : treeView}
fieldNames={{
title: "label",
key: "key"
}}
blockNode
defaultExpandAll={isShowAllInTree ? false : true}
defaultExpandedKeys={isShowAllInTree ? treeShowRow && treeShowRow.length > 0 ? treeShowRow : ['0-0'] : []}
onCheck={(checkedKeys: React.Key[] | any, info) => {
const selectedIds = info.checkedNodes.filter((n: any) => n?.type === 1)
setSelectedId(selectedIds.map(n => n?.value)?.toString() || '')
if (reload) {
actionRef?.current?.reload()
}
if (otherFun) {
otherFun(info)
}
}}
/> : ''}
</ProCard>
</div>
)
}
export default connect(({ user }: ConnectState) => ({
currentUser: user.data
}))(LeftSelectTree);

View File

@ -0,0 +1,61 @@
import request from "@/utils/requestOld"
export async function getServerpartTree(ProvinceCode?: number | string, ServerpartCodes?: string,
ShowWholePower?: boolean, ShowSPRegion?: boolean, ShowRoyalty?: boolean | false, ShowCompactCount?: boolean | false, StatisticsType?: number): Promise<ServerpartTree[]> {
// ShowRoyalty 无论传入什么都是false
const data = await request(`/BaseInfo/GetServerpartTree?
ProvinceCode=${ProvinceCode || ''}&ServerpartCodes=${ServerpartCodes || ''}&
ShowWholePower=${ShowWholePower || false}&ShowWholePower=${ShowSPRegion || true}&
ShowRoyalty=${false}&ShowCompactCount=${ShowCompactCount || false}&StatisticsType=${StatisticsType || ''}`, {
method: 'GET',
});
if (data.Result_Code !== 100) {
return [];
}
return data.Result_Data.List;
}
export async function getMerchantShopTree(params: { BusinessManId?: any, provinceCode?: number }) {
const data = await request(`/FrameWork/GetMerchantShopTree`, {
method: 'GET',
params
})
if (data.Result_Code !== 100) {
return []
}
const treeTable = wrapTreeNode(data.Result_Data.List);
return [...treeTable];
}
export function wrapTreeNode(data: any[]) {
const wrapData: any = data.map((item: any) => {
const node = { ...item.node };
if (item.children && item.children.length > 0) {
node.children = wrapTreeNode(item.children);
}
return node
});
return wrapData;
}
export async function handleGetServerpartDDL(params: any) {
const data = await request(`/BaseInfo/GetServerpartDDL`, {
method: 'GET',
params
})
if (data.Result_Code !== 100) {
return []
}
return data.Result_Data.List
}

7
src/global.less Normal file
View File

@ -0,0 +1,7 @@
html,body{
padding: 0;
margin: 0;
}
*{
box-sizing: border-box;
}

259
src/layouts/index.less Normal file
View File

@ -0,0 +1,259 @@
.pageLayout{
.ant-layout{
.ant-layout-sider{
background-color: #021529!important;
.ant-layout-sider-children{
padding: 0;
.ant-pro-sider-logo{
display: flex;
align-items: center;
a{
display: block;
h1{
color: #fff;
font-weight: 600;
font-size: 18px;
// line-height: 32px;
vertical-align: none;
margin-left: 12px;
}
}
}
}
.ant-layout-sider-children{
ul{
li{
span{
font-size: 14px;
color: #ffffffA6;
}
i{
font-size: 14px;
color: #ffffffA6;
}
}
}
}
}
}
.pageLayout {
:global {
.ant-pro-menu-item {
&.ant-menu-item-selected {
background-color: #1890ff !important;
.ant-pro-menu-item-title {
color: #fff !important;
}
&:hover {
background-color: #1890ff !important;
}
}
}
.ant-menu-item-selected {
background-color: #1890ff !important;
color: #fff !important;
&:hover {
background-color: #1890ff !important;
color: #fff !important;
}
a {
color: #fff !important;
}
}
.ant-menu-submenu-selected > .ant-menu-submenu-title {
color: #1890ff;
}
.ant-menu-item {
&:hover {
background-color: rgba(24, 144, 255, 0.1);
}
}
}
}
.ant-pro-layout-container{
background-color: #f0f2f5;
main{
padding: 0;
}
}
.tab-extra {
width: 13px;
height: 15px;
padding: 0 26px 16px 0;
// background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAPCAYAAAA/I0V3AAAAAXNSR0IArs4c6QAAANhJREFUOE+9kT1rAlEQRc/MCmLjz7G1tUphKTbpBFs16BpZXkyCuqRLbaFYWqSytc3PSRMC7jxRMMQP5KVxypm5M5dzJXap558ltxN13Vs5skxCHWYa+eDlv0el5yYzRXKhnwy/uSGI2KUZoKH2ANvZawDvgUIDmnt6PTepCzIFrgHZePz9y6A9/0Xef0qr5lkI5E+tevhRoTZ8bC13s6Oc+sm4YqpLgcJB6OFbzarDpLM69M7C7bpxWdEPoAh8GXb3Ouisj8K9RC1ORiU0WmBZ7Tl5+Dzd2QIFbkkh8PpnKgAAAABJRU5ErkJggg==') ;
// background-repeat: no-repeat;
}
.ant-dropdown-open svg {
// color: @primary-color;
// background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAPCAYAAAA/I0V3AAAAAXNSR0IArs4c6QAAAMZJREFUOE+9kb0KwkAQhGdUEBtfxpxtWisLS7GxE4Too3gH1hZKSgsrW1vj09iIkGQkghJ/kLPJlrs7u8M3NDYX/ixWKHIKlaX0dch6Q97L5aM0i2wN1hq+n6A8rRBEYPOMQM3XnoCcHasJoaWPsBAInN7pmYVGoFYAfgFJIY6TOTdP5MZpICkm0Hy3KuBKcphE3Bazl5yMVQ9QMWiVhBeAg2TG/aP3Ea5xCiHtALQBnEH2k4iHl3C/UQucupBikMNTxOP7zg3x4U00HiX/gwAAAABJRU5ErkJggg==');
}
.tab-extra:hover {
// background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAPCAYAAAA/I0V3AAAAAXNSR0IArs4c6QAAAMZJREFUOE+9kb0KwkAQhGdUEBtfxpxtWisLS7GxE4Too3gH1hZKSgsrW1vj09iIkGQkghJ/kLPJlrs7u8M3NDYX/ixWKHIKlaX0dch6Q97L5aM0i2wN1hq+n6A8rRBEYPOMQM3XnoCcHasJoaWPsBAInN7pmYVGoFYAfgFJIY6TOTdP5MZpICkm0Hy3KuBKcphE3Bazl5yMVQ9QMWiVhBeAg2TG/aP3Ea5xCiHtALQBnEH2k4iHl3C/UQucupBikMNTxOP7zg3x4U00HiX/gwAAAABJRU5ErkJggg==');
}
.ant-modal-header {
background-color: #ecf2f7 !important;
border-bottom: none !important;
}
body *::-webkit-scrollbar {
/* 滚动条整体样式 */
width : 8px; /* 高宽分别对应横竖滚动条的尺寸 */
height: 8px;
}
body *::-webkit-scrollbar-thumb {
/* 滚动条里面小方块 */
background-color: #d1cfcf;
// background-image: -webkit-linear-gradient(
// 45deg,
// rgba(255, 255, 255, 0.2) 25%,
// transparent 25%,
// transparent 50%,
// rgba(255, 255, 255, 0.2) 50%,
// rgba(255, 255, 255, 0.562) 75%,
// transparent 75%,
// transparent
// );
border-radius: 10px;
}
body *::-webkit-scrollbar-track {
/* 滚动条里面轨道 */
background: #ededed;
border-radius: 10px;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.01);
}
// 列表页左侧多选树
.pageTable-leftnav .ant-tree-list-holder{
height: calc(100vh - 257px);
padding-right: 0;
padding-bottom: 0;
overflow-y: auto;
}
.leftHeight .ant-tree-list-holder{
height: 100%!important;
}
.leftHeight .ant-tree-list{
height: 560px
}
.pageTable-leftnavs .ant-tree-list-holder{
height: 580px;
padding-right: 0;
padding-bottom: 0;
overflow-y: auto;
}
.pageTable-leftnav .ant-pro-card-header.ant-pro-card-header-border {
padding-top: 29px;
}
.pageTable-leftnav .ant-pro-card-title {
font-size: 14px;
}
.pageTable-leftnav .ant-pro-card-split-vertical{
transition: width 0.3s;
}
.pageTable-leftnav .ant-tree .ant-tree-treenode {
padding-bottom: 16px;
}
.pageTable-leftnavs .ant-tree .ant-tree-treenode {
padding-bottom: 16px;
}
.ant-table-tbody > tr.tablerow-no-border > td{
border-bottom: 0;
}
// prolist 卡片类型
.ant-pro-list .ant-pro-card-header {
padding: 0 !important;
}
.ant-pro-list .ant-pro-card.ant-pro-card-border.ant-pro-card-hoverable>.ant-pro-card-body {
margin: 0 !important;
padding: 20px 0 0 0 !important;
}
.ant-pro-list .ant-pro-card.ant-pro-card-border> ul.ant-pro-card-actions {
margin-top: 20px;
}
.ant-pro-list .ant-pro-card.ant-pro-card-border> ul.ant-pro-card-actions >li{
margin:0;
}
.ant-pro-list .ant-pro-card.ant-pro-card-border.ant-pro-card-hoverable> ul.ant-pro-card-actions > li, .ant-pro-list .ant-pro-card .ant-pro-card-actions .ant-space-item {
margin: 10px 0 0 0 !important;
}
}
.main-tab >.ant-tabs-nav {
margin-bottom: 0 !important;
padding: 2px 15px 0 16px;
background-color: #fff;
}
.main-tab > .ant-tabs-nav .ant-tabs-tab {
background-color: #fafafa;
border-color: #f0f0f0;
border-bottom: 0;
}
.main-tab > .ant-tabs-nav .ant-tabs-tab button svg {
// color: #fff;
transition: all 0.3s;
}
.main-tab > .ant-tabs-nav .ant-tabs-tab:hover {
background-color:#fafafa;
border-color: #f0f0f0;
}
.main-tab > .ant-tabs-nav .ant-tabs-tab:hover button svg {
// color: @link-color;
}
.main-tab.ant-tabs-top > .ant-tabs-nav .ant-tabs-tab-active {
background-color: #f6f9fb;
border-color: #f0f0f0;
box-shadow: 2px 0 6px 0 #e7e7e7;
border-top-color: #3591fe;
}
.main-tab.ant-tabs-top > .ant-tabs-nav .ant-tabs-tab-active::before {
content: '';
position: absolute;
display: block;
background-color: #3591fe;
top: 0;
left: 0;
width: 100%;
height: 1px;
}
.main-tab > .ant-tabs-nav .ant-tabs-tab-active button svg {
// color: @icon-color;
}
.main-tab > .ant-tabs-content-holder {
height: calc(100vh - 86px);
padding-top: 16px;
overflow-y: auto;
}
.main-tab > .ant-tabs-nav .ant-tabs-nav-wrap {
bottom: 1px;
}
.ant-pro-layout .ant-pro-layout-bg-list{
background:#f5f5f5!important
}
.ant-pro-layout-bg-list{
background:#f5f5f5!important
}

201
src/layouts/index.tsx Normal file
View File

@ -0,0 +1,201 @@
import type { FC } from 'react';
import { useState, useEffect } from 'react';
import { Dropdown, Layout, Menu, Tabs, Tooltip } from 'antd';
import type { MenuProps } from 'antd';
import { Outlet, Link, useLocation, connect, history } from 'umi';
import PageAccess from '@/components/PageAccess';
import type { UserConnectedProps, UserModelState } from '@/models/user';
import LayoutWrapper from '@/components/LayoutWrapper';
import Nav from '@/components/Nav';
import Page404 from '@/pages/404';
import handleRecursiveNestedData from '@/utils/handleRecursiveNestedData';
import { MenuDataItem, ProLayout } from '@ant-design/pro-components';
import './index.less'
import logo from '../assets/logo.svg';
import upMenu from '@/assets/upMenu.svg'
import Icon, { DoubleRightOutlined, SmileOutlined } from '@ant-design/icons';
import { ProfileModelState } from '@/models/global';
import React from 'react';
const { Header, Content } = Layout;
const { TabPane } = Tabs;
/**
* openKeys的方法
* @param currentLocation , handleGetCurrentLocation方法返回
* @returns openKeys
*/
const handleGetOpenKeys = (currentLocation: API.MenuItem[] | []): string[] => {
//currentLocation为空
if (!currentLocation.length) return [''];
//currentLocation只有一项
if (currentLocation.length === 1) {
return currentLocation.map((item: API.MenuItem) => `${item.key}`);
}
const res = [];
//currentLocation有多项, 只要前n-1项
for (let i = 0; i < currentLocation.length - 1; i++) {
res.push(`${currentLocation[i].key}`);
}
return res;
};
//自定义的layout页面, 顶部导航通栏+侧边栏(菜单)布局, 可根据需要做调整
const BasicLayout: FC<{ user: UserModelState, global: ProfileModelState, dispatch: any, location: any }> = (props) => {
const [collapsed, setCollapsed] = useState(false);
const { pathname } = useLocation();
console.log('pathname', pathname);
const {
dispatch,
user: {
menu, rootSubmenuKeys, indexAllMenuItemById,
indexValidMenuItemByPath
},
global: {
tabsRoutes
}
} = props;
const validMenuItem = indexValidMenuItemByPath[pathname];
// const selectedKeys = validMenuItem?.key;
const [activeKey, setActiveKey] = useState<string>(validMenuItem?.path || '/')
//Menu中的selectedKeys和openKeys不是一回事:
//openKeys:
//当前展开的SubMenu菜单项key数组, 有子菜单的父菜单, 当selectedKeys为没子菜单的父菜单时该值应该设为[''],
//也就是关闭所有有子菜单的父菜单;
//selectedKeys:
//当前选中的菜单项key数组, 有子菜单则是子菜单(叶子节点), 没有子菜单则是父菜单(一级菜单), 始终是可选中的
//点击有子菜单的父菜单的回调
// const onOpenChange: MenuProps['onOpenChange'] = (keys) => {
// setOpenKeys(keys)
// dispatch({
// namespace: "global/setProfileData",
// payload: {
// keys
// }
// })
// };
//所有MenuItem:
//有children的: 一定都有path, lable不动, children下的label修改为<Link to={path} />
//无children的: 有path的label修改为<Link to={path} />, 没path的label不动
const consumableMenu = handleRecursiveNestedData(
menu,
(item: API.MenuItem) => ({
...item,
name: item.path
? (
<Link
to={item.path}
style={{ color: pathname === item.path ? '#1890ff' : 'inherit' }}
className={pathname === item.path ? 'ant-menu-item-selected' : ''}
>{item.name}</Link>
)
: item.name,
}),
);
// const consumableMenu: MenuDataItem[] = [
// {
// path: '/',
// name: 'Dashboard',
// icon: <SmileOutlined />,
// },
// {
// path: '/about',
// name: 'Users',
// icon: <SmileOutlined />,
// children: [
// {
// path: '/about/u',
// name: 'User List',
// },
// {
// path: '/about/m',
// name: 'User Profile',
// },
// ],
// },
// ];
const location = useLocation();
// 改变panes
const handleTabsPanes = (payload: any): void => {
dispatch({
type: 'global/changeTabsRoutes',
payload: { data: payload, action: 'add' },
});
};
// 关闭当前标签
const handleEdit = (targetKey: string | React.MouseEvent<Element, MouseEvent> | React.KeyboardEvent<Element>, action: "add" | "remove"): void => {
if (action === 'remove' && dispatch) {
const index = tabsRoutes.findIndex((n: { path: string }) => n.path === targetKey)
// 定位关闭当前标签后,需要选中的下一个标签
// eslint-disable-next-line no-nested-ternary
const nextkey = tabsRoutes[index + 1] ? tabsRoutes[index + 1].path : (tabsRoutes[index - 1] ? tabsRoutes[index - 1].path : '')
history.push(nextkey || '/')
setActiveKey(nextkey)
// 缓存路由栈数据
dispatch({
type: 'global/changeTabsRoutes',
payload: { data: [targetKey], action },
})
}
}
// 全部菜单一层的数组
const oneFloorList: any = [
{
SYSTEMMODULE_DESC: "",
guid: "1",
hideInMenu: false,
name: "测试",
path: "/test/index",
},
]
return (
<LayoutWrapper>
<ProLayout
menuRender={false}>
<Layout>
{/* <Header style={{ background: '#fff', height: '48px', padding: '0 16px' }}>
<Nav />
</Header> */}
<Content>
{
//统一对所有有效路由做页面鉴权的处理
validMenuItem
? (
<PageAccess>
<Outlet />
</PageAccess>
)
: <Page404 />
}
</Content>
</Layout>
</ProLayout>
</LayoutWrapper >
);
};
export default connect(
({ user, global }: { user: UserConnectedProps['user'], global: any }) => ({
user,
global
}),
)(BasicLayout);

156
src/layouts/old.tsx Normal file
View File

@ -0,0 +1,156 @@
import type { FC } from 'react';
import { useState, useEffect } from 'react';
import { Layout, Menu } from 'antd';
import type { MenuProps } from 'antd';
import { Outlet, Link, useLocation, connect } from 'umi';
import PageAccess from '@/components/PageAccess';
import type { UserConnectedProps } from '@/models/user';
import LayoutWrapper from '@/components/LayoutWrapper';
import Nav from '@/components/Nav';
import Page404 from '@/pages/404';
import handleRecursiveNestedData from '@/utils/handleRecursiveNestedData';
import handleGetCurrentLocation from '@/utils/handleGetCurrentLocation';
const { Header, Content, Sider } = Layout;
/**
* openKeys的方法
* @param currentLocation , handleGetCurrentLocation方法返回
* @returns openKeys
*/
const handleGetOpenKeys = (currentLocation: API.MenuItem[] | []): string[] => {
//currentLocation为空
if (!currentLocation.length) return [''];
//currentLocation只有一项
if (currentLocation.length === 1) {
return currentLocation.map((item: API.MenuItem) => `${item.key}`);
}
const res = [];
//currentLocation有多项, 只要前n-1项
for (let i = 0; i < currentLocation.length - 1; i++) {
res.push(`${currentLocation[i].key}`);
}
return res;
};
//自定义的layout页面, 顶部导航通栏+侧边栏(菜单)布局, 可根据需要做调整
const BasicLayout: FC<UserConnectedProps> = (props) => {
const [collapsed, setCollapsed] = useState(false);
const [openKeys, setOpenKeys] = useState(['']);
const { pathname } = useLocation();
const {
user: {
menu, rootSubmenuKeys, indexAllMenuItemById,
indexValidMenuItemByPath,
},
} = props;
const validMenuItem = indexValidMenuItemByPath[pathname];
const selectedKeys = validMenuItem?.key;
useEffect(
() => {
//每次页面重新渲染都要设置openKeys
setOpenKeys(
handleGetOpenKeys(
handleGetCurrentLocation(indexValidMenuItemByPath[pathname], indexAllMenuItemById),
),
);
},
[pathname, indexAllMenuItemById, indexValidMenuItemByPath],
);
//Menu中的selectedKeys和openKeys不是一回事:
//openKeys:
//当前展开的SubMenu菜单项key数组, 有子菜单的父菜单, 当selectedKeys为没子菜单的父菜单时该值应该设为[''],
//也就是关闭所有有子菜单的父菜单;
//selectedKeys:
//当前选中的菜单项key数组, 有子菜单则是子菜单(叶子节点), 没有子菜单则是父菜单(一级菜单), 始终是可选中的
//点击有子菜单的父菜单的回调
const onOpenChange: MenuProps['onOpenChange'] = (keys) => {
const latestOpenKey = keys.find((key) => !openKeys.includes(key));
if (!rootSubmenuKeys.includes(`${latestOpenKey}`)) {
setOpenKeys(keys);
} else {
setOpenKeys(latestOpenKey ? [latestOpenKey] : ['']);
}
};
//所有MenuItem:
//有children的: 一定都有path, lable不动, children下的label修改为<Link to={path} />
//无children的: 有path的label修改为<Link to={path} />, 没path的label不动
const consumableMenu = handleRecursiveNestedData(
menu,
(item: API.MenuItem) => ({
...item,
label: item.path
? (
<Link
to={item.path}
style={{ color: 'inherit' }}
>{item.label}</Link>
)
: item.label,
}),
);
return (
<LayoutWrapper>
<Layout style={{ minHeight: '100vh' }}>
<Header
style={{
padding: '0',
height: 'auto',
}}
>
<Nav />
</Header>
<Layout>
<Sider
collapsible
theme="light"
collapsed={collapsed}
onCollapse={(value) => setCollapsed(value)}
>
<Menu
mode="inline"
openKeys={openKeys}
items={consumableMenu}
onOpenChange={onOpenChange}
selectedKeys={[selectedKeys]}
/>
</Sider>
<Layout>
<Content style={{ margin: '0 16px' }}>
{
//统一对所有有效路由做页面鉴权的处理
validMenuItem
? (
<PageAccess>
<Outlet />
</PageAccess>
)
//这里不再使用<Outlet />是因为当一条路由在菜单接口中没有返回, 但实际项目中创建了,
//此时访问应该显示404页面; 菜单中没有, 实际项目中有表示这条路由/页面当前登录用户无法访问,
//而如果继续使用<Outlet />则那条路由/页面依旧会被渲染, 因为对于umi来说这条路由/页面是有效的,
//是存在的, 而对于本项目来说则不是
: <Page404 />
}
</Content>
</Layout>
</Layout>
</Layout>
</LayoutWrapper>
);
};
export default connect(
({ user }: { user: UserConnectedProps['user'] }) => ({
user,
}),
)(BasicLayout);

272
src/models/global.ts Normal file
View File

@ -0,0 +1,272 @@
// models/profile.ts
import session from '@/utils/session';
import { Effect, Reducer } from 'umi';
export type ConnectState = {
global: GlobalModelState;
};
export type NoticeItem = {
id: string;
type: string;
status: string;
} & NoticeIconData;
export type tabsRoute = {
title: string;
path: string;
key: string;
children: any;
}
export type GlobalModelState = {
collapsed: boolean;
notices: NoticeItem[];
tabsRoutes: tabsRoute[];
menuData: MenuDataItem[];
moduleMap: MenuDataItem[];
};
export type GlobalModelType = {
namespace: 'global';
state: GlobalModelState;
effects: {
fetchNotices: Effect;
clearNotices: Effect;
changeNoticeReadState: Effect;
changeTabsRoutes: Effect;
getMenuData: Effect;
};
reducers: {
changeLayoutCollapsed: Reducer<GlobalModelState>;
saveNotices: Reducer<GlobalModelState>;
saveClearedNotices: Reducer<GlobalModelState>;
saveTabsRoutes: Reducer<GlobalModelState>;
saveMenuData: Reducer<GlobalModelState>;
saveMenuModuleMap: Reducer<GlobalModelState>;
};
};
const GlobalModel: GlobalModelType = {
namespace: 'global',
state: {
collapsed: false,
notices: [],
tabsRoutes: [],
menuData: [],
moduleMap: []
},
effects: {
*getMenuData({ payload, callback }, { put, call }) {
const response = yield call(getUserMenu, payload);
const menuData = menuFormatter(response);
// const menuData = wrapTreeNode(response);
globalState.set('menuData', menuData);
yield put({
type: 'saveMenuData',
payload: menuData,
});
yield put({
type: 'saveMenuModuleMap',
payload: menuFormatter(response),
});
if (callback && typeof callback === 'function') {
callback(response)
}
},
*fetchNotices(_, { call, put, select }) {
const user: CurrentUser = yield select(
(state: ConnectState) => state.user.currentUser
);
const data = yield call(queryNotices, { RECSTAFF_ID: user?.ID });
globalState.set('notices', data);
yield put({
type: 'saveNotices',
payload: data,
});
const unreadCount: number = yield select(
(state: ConnectState) => state.global.notices.filter((item) => item.MESSAGE_STATE === 1).length,
);
yield put({
type: 'user/changeNotifyCount',
payload: {
totalCount: data.length,
unreadCount,
},
});
},
*clearNotices({ payload }, { put, select, call }) {
const user: CurrentUser = yield select(
(state: ConnectState) => state.user.currentUser
);
const notices: NoticeItem[] = yield select(
(state: ConnectState) => state.global.notices
);
const clearNoticeIds = notices.reduce((p: any[], c) => {
if (c.MESSAGE_TYPE === (payload as number)) {
return [...p, c.MESSAGE_ID]
}
return p
}, [])
const success = yield call(setMessageState, {
messageIds: clearNoticeIds.toString(),
recStaffId: user?.ID,
messageState: 2
}
);
if (success) {
yield put({
type: 'saveClearedNotices',
payload,
});
const count: number = yield select((state: ConnectState) => state.global.notices.length);
const unreadCount: number = yield select(
(state: ConnectState) => state.global.notices.filter((item) => item.MESSAGE_STATE === 1).length,
);
yield put({
type: 'user/changeNotifyCount',
payload: {
totalCount: count,
unreadCount,
},
});
}
},
* changeNoticeReadState({ payload }, { call, put, select }) {
const notices: NoticeItem[] = yield select((state: ConnectState) =>
state.global.notices.map((item) => {
const notice = { ...item };
if (notice.MESSAGE_ID === payload) {
notice.MESSAGE_STATE = 2;
}
return notice;
}),
);
const success = yield call(changeNoticesState, notices.find(n => n.MESSAGE_ID === payload));
if (success) {
yield put({
type: 'saveNotices',
payload: notices,
});
yield put({
type: 'user/changeNotifyCount',
payload: {
totalCount: notices.length,
unreadCount: notices.filter((item) => item.MESSAGE_STATE === 1).length,
},
});
}
},
* changeTabsRoutes({ payload, callback }, { put, select }) { // 缓存路由栈
const tabsRoutes: tabsRoute[] = yield select((state: ConnectState) => {
const { data } = payload
console.log('data', data);
console.log('payload', payload);
if (payload.action === 'add') {
const index = state.global.tabsRoutes.findIndex(n => n.path === data.path)
if (index === -1) { // 没缓存 则添加
return [...state.global.tabsRoutes, { ...data, index: state.global.tabsRoutes.length }]
}
// 否则不操作
return [...state.global.tabsRoutes]
}
if (payload.action === 'removeAll') {
return []
}
if (callback && typeof callback === 'function') {
callback.call()
}
// 移除单个/多个路由
return state.global.tabsRoutes.filter(n => !data.includes(n.path))
});
yield put({
type: 'saveTabsRoutes',
payload: tabsRoutes,
});
}
},
reducers: {
changeLayoutCollapsed(state = { notices: [], collapsed: true, tabsRoutes: [], menuData: [], moduleMap: [] }, { payload }): GlobalModelState {
return {
...state,
collapsed: payload,
};
},
saveNotices(state, { payload }): GlobalModelState {
return {
collapsed: false,
...state,
notices: payload,
tabsRoutes: state?.tabsRoutes || [],
menuData: state?.menuData || [],
moduleMap: state?.moduleMap || []
};
},
saveClearedNotices(state = { notices: [], collapsed: true, tabsRoutes: [], menuData: [], moduleMap: [] }, { payload }): GlobalModelState {
const type = payload as number
return {
...state,
collapsed: false,
notices: state.notices.filter((item): boolean => item.MESSAGE_TYPE !== type),
};
},
saveTabsRoutes(state = { notices: [], collapsed: true, tabsRoutes: [], menuData: [], moduleMap: [] }, { payload }): GlobalModelState {
return {
...state,
tabsRoutes: payload
}
},
saveMenuData(state = { notices: [], collapsed: true, tabsRoutes: [], menuData: [], moduleMap: [] }, { payload }): GlobalModelState {
return {
...state,
menuData: payload,
}
},
saveMenuModuleMap(state = { notices: [], collapsed: true, tabsRoutes: [], menuData: [], moduleMap: [] }, { payload }): GlobalModelState { // 生成模块
const reduceMenu = (data: any) => {
return data.reduce((p: [], current: Record<string, any>) => {
if (current.children) {
const d = reduceMenu(current.children)
return [...p, ...d]
}
return [...p, current]
}, [])
}
const moduleMap = reduceMenu(payload)
session.set('menu', moduleMap)
return {
...state,
moduleMap,
}
}
},
};
export default GlobalModel;

363
src/models/user.ts Normal file
View File

@ -0,0 +1,363 @@
import { history } from 'umi';
import {
userLogin, retrieveUserInfo, retrieveUserInfoAuthorityMenu, userLogout,
retrieveUserAuthorityMenu,
} from '@/services/user';
import type { Effect, Reducer, ConnectProps } from 'umi';
import handleRedirect from '@/utils/handleRedirect';
import handleGetRootSubmenuKeys from '@/utils/handleGetRootSubmenuKeys';
import handleGetEachDatumFromNestedDataByKey from '@/utils/handleGetEachDatumFromNestedDataByKey';
import handleGetIndexValidMenuItemByPath from '@/utils/handleGetIndexValidMenuItemByPath';
import { notification } from 'antd';
/**
*
* @description isLogin
* @description data
* @description menu
* @description authority
* @description loginBtnLoading loading状态
* @description rootSubmenuKeys key
* @description layoutWrapperLoading loading状态
* @description indexAllMenuItemById id索引索引菜单项的映射
* @description indexAllMenuItemByPath path索引所有菜单项的映射
* @description indexValidMenuItemByPath path索引有效菜单项的映射
*/
export type UserModelState = {
isLogin: boolean;
data: API.UserInfo;
menu: API.MenuData;
authority: string[];
loginBtnLoading: boolean;
rootSubmenuKeys: React.Key[];
layoutWrapperLoading: boolean;
indexAllMenuItemById: IndexAllMenuItemByKey<'id'>;
indexValidMenuItemByPath: IndexValidMenuItemByPath;
indexAllMenuItemByPath: IndexAllMenuItemByKey<'path'>;
}
export type UserConnectedProps = {
user: UserModelState;
} & ConnectProps;
type UserModelType = {
namespace: 'user';
state: UserModelState;
effects: {
login: Effect;
logout: Effect;
resetLoginStatus: Effect;
getUserInfoAuthorityMenu: Effect;
};
reducers: {
save: Reducer<UserModelState>;
};
}
/**
*
* @description |
*/
type ReqOrder = 'concurrent' | 'relay';
const UserModel: UserModelType = {
namespace: 'user',
state: {
data: {},
authority: [],
isLogin: false,
rootSubmenuKeys: [],
loginBtnLoading: false,
layoutWrapperLoading: true,
menu: [
],
indexAllMenuItemById: {
},
indexAllMenuItemByPath: {
},
indexValidMenuItemByPath: {
},
},
effects: {
//登录
*login({ payload }, { call, put, select }) {
// 检查当前是否已经在登录状态
const { isLogin } = yield select((state) => state.user);
if (isLogin) {
return { code: 1, message: '您已经登录' };
}
// 检查是否有payload防止自动触发
if (!payload) {
return { code: 1, message: '登录参数无效' };
}
yield put({
type: 'save',
payload: {
loginBtnLoading: true,
},
});
try {
const res: API.LoginResponse = yield call(userLogin, payload);
console.log('res', res);
if (res.code === 200) {
localStorage.setItem('Authorization', res.data.accessToken);
yield put({
type: 'getUserInfoAuthorityMenu',
payload: {
type: 'concurrent',
},
});
return res;
} else {
yield put({
type: 'save',
payload: {
loginBtnLoading: false,
},
});
return { code: res.code, message: res.message };
}
} catch (error) {
yield put({
type: 'save',
payload: {
loginBtnLoading: false,
},
});
return { code: 1, message: error?.message || '登录失败,请稍后重试' };
}
},
//获取用户信息和权限以及菜单
*getUserInfoAuthorityMenu({ payload }, { call, put }) {
// const { type }: { type: ReqOrder } = payload;
// // 如果当前在登录页面,且不是通过登录操作触发,则不执行后续操作
// if (window.location.pathname === '/user/login' && type !== 'concurrent') {
// yield put({
// type: 'save',
// payload: {
// layoutWrapperLoading: false,
// isLogin: false // 确保在登录页面时重置登录状态
// },
// });
// return false;
// }
let userInfoRes: API.UserInfoResponse = {
data: {},
code: 0,
message: '',
};
// let userAuthorityRes: API.UserAuthorityResponse = {
// data: {
// authority: [],
// },
// code: 0,
// message: '',
// };
let menuRes: API.MenuDataResponse = {
data: [],
code: 0,
message: '',
};
// //用户在登录页登录完成之后执行
// if (type === 'concurrent') {
// const res: API.UserInfoAuthMenuResponse = yield call(retrieveUserInfoAuthorityMenu);
// console.log('res', res);
// userInfoRes = res[0] as API.UserInfoResponse;
// // userAuthorityRes = res[1] as API.UserAuthorityResponse;
// menuRes = res[1] as API.MenuDataResponse;
// } else {
// //其他情形首先查询用户的登录状态, 未登录则不继续操作
// try {
// userInfoRes = yield call(retrieveUserInfo);
// } catch (error) {
// //接口报错了, 比如返回了401
// yield put({
// type: 'save',
// payload: {
// layoutWrapperLoading: false,
// },
// });
// // 只有在非登录页面时才执行重定向
// if (window.location.pathname !== '/user/login') {
// yield put({
// type: 'resetLoginStatus',
// });
// }
// return false;
// }
// const res: API.UserAuthMenuResponse = yield call(retrieveUserAuthorityMenu);
// // userAuthorityRes = res[0] as API.UserAuthorityResponse;
// menuRes = res[0] as API.MenuDataResponse;
// }
// console.log('userInfoRes', userInfoRes);
menuRes.data = [
{
path: '/',
redirect: '',
name: '',
children: [
{
path: '/test/index',
redirect: '',
name: '测试',
component: "@/pages/test/index",
},
{
path: '/Invoicing/index',
redirect: '',
name: '开票管理',
component: "@/pages/Invoicing/index",
},
]
}
]
// menuRes.data = [
// {
// path: '/standard/index',
// redirect: '',
// name: '生成标准页面',
// component: "@/pages/standard/index",
// },
// {
// path: '/examine',
// redirect: '',
// name: '走动式管理',
// children: [
// {
// path: '/examine/index',
// name: '考评分类管理',
// component: "@/pages/examine/index",
// },
// {
// path: '/examine/question',
// name: '考核问题管理',
// component: "@/pages/examine/question",
// },
// {
// path: '/examine/modal',
// name: '考核模版管理',
// component: "@/pages/examine/modal",
// },
// {
// path: '/examine/record',
// name: '考核记录管理',
// component: "@/pages/examine/record",
// }
// ]
// },
// ]
let indexAllMenuItemByPath: any = []
let indexValidMenuItemByPath: any = []
if (menuRes.data && menuRes.data.length > 0) {
indexAllMenuItemByPath = handleGetEachDatumFromNestedDataByKey(menuRes.data, 'path');
indexValidMenuItemByPath = handleGetIndexValidMenuItemByPath(menuRes.data);
}
//在登录完获取菜单数据之后做是否需要重定向的操作
yield call(
handleRedirect,
window.location.pathname === '/cloudMenu/user/login',
indexAllMenuItemByPath,
indexValidMenuItemByPath,
);
yield put({
type: 'save',
payload: {
isLogin: true,
menu: menuRes.data,
data: userInfoRes.data,
loginBtnLoading: false,
indexAllMenuItemByPath,
indexValidMenuItemByPath,
layoutWrapperLoading: false,
// authority: userAuthorityRes.data.authority,
authority: [
'/test/index',
'/cloudMenu/test/index',
"/Invoicing/index"
// '/examine/index',
// '/examine/modal',
// '/examine/question',
// '/examine/record',
],
rootSubmenuKeys: handleGetRootSubmenuKeys(menuRes.data),
indexAllMenuItemById: handleGetEachDatumFromNestedDataByKey(menuRes.data, 'id'),
},
});
//为保证所有语句都return, 因此这里加一句这个
return true;
},
//登出
* logout({ payload }, { call, put }) {
const res: API.LogoutResponse = yield call(userLogout, payload);
console.log('res', res)
if (res.code === 200) {
yield put({
type: 'resetLoginStatus',
});
}
},
//重置登录状态
* resetLoginStatus(_, { put }) {
localStorage.removeItem('Authorization');
yield put({
type: 'save',
payload: {
isLogin: false,
loginBtnLoading: false,
},
});
//当前页面不是登录页时,才进行重定向
if (window.location.pathname !== '/user/login') {
// 只获取路径部分不包含查询参数避免redirect参数累积
let redirectValue = window.location.pathname;
// 检查是否有base路径重复问题
if (redirectValue.startsWith('/') && redirectValue.indexOf('/', 1) !== -1) {
const firstSlashAfterRoot = redirectValue.indexOf('/', 1);
const possiblePrefix = redirectValue.substring(0, firstSlashAfterRoot);
// 检查是否有重复的路径前缀
if (redirectValue.indexOf(possiblePrefix, firstSlashAfterRoot) === firstSlashAfterRoot) {
redirectValue = redirectValue.substring(firstSlashAfterRoot);
}
}
// history.push(`/user/login?redirect=${encodeURIComponent(redirectValue)}`);
}
},
},
reducers: {
save(state, action) {
return {
...state,
...action.payload,
};
},
},
};
export default UserModel;

24
src/pages/404.tsx Normal file
View File

@ -0,0 +1,24 @@
import type { FC } from 'react';
import { history } from 'umi';
import { Button, Result } from 'antd';
const _404: FC = () => {
return (
<Result
title="404"
status="404"
subTitle="对不起, 您访问的页面不存在"
extra={(
<Button
type="primary"
onClick={
() => history.push('/')
}
></Button>
)}
/>
);
};
export default _404;

View File

@ -0,0 +1,52 @@
// 数电的开票
import ProTable, { ActionType } from "@ant-design/pro-table";
import { FormInstance } from "antd";
import { useRef } from "react";
import { connect } from "umi";
const DigitalElectronics: React.FC<{ currentUser: any }> = (props) => {
const actionRef = useRef<ActionType>();
const formRef = useRef<FormInstance>();
const columns: any = []
return (
<div>
<ProTable
actionRef={actionRef}
formRef={formRef}
columns={columns}
bordered
expandable={{
expandRowByClick: true
}}
rowKey={(record) => {
return `${record?.id}`
}}
scroll={{ x: "100%", y: 'calc(100vh - 400px)' }}
headerTitle={<span style={{ color: "#1890ff", fontSize: 14, fontWeight: 600 }}></span>}
search={{ span: 6 }}
request={async (params: any) => {
if (tableData.list && tableData.list.length > 0) {
return {
data: tableData.list,
success: true,
total: tableData.total, // 总数据条数
current: tableData.pageNo, // 当前页码
pageSize: tableData.pageSize, // 每页条数
}
}
return { data: [], success: true }
}}
/>
</div>
)
}
export default connect(({ user }: ConnectState) => ({
currentUser: user.data
}))(DigitalElectronics);

View File

@ -0,0 +1,416 @@
import { useEffect, useRef, useState } from "react";
import { connect } from "umi";
import { handleCancel, handleGetBWTokenFun, handleGetCertifyQrcode, handleGetCertifyQrcodeResult, handleGetDeliver, handleGetInvoiceDetail, handleGetInvoicePageTable, handleGetUserInfoById } from "./service";
import { FormInstance, message, Modal, Space } from "antd";
import { ActionType, ProTable } from "@ant-design/pro-components";
import moment from "moment";
const { confirm } = Modal;
const InvoicingIndex: React.FC<{ currentUser: any }> = (props) => {
// 查询人脸的认证的状态
const timerRef = useRef<NodeJS.Timeout | null>(null); // 保存定时器引用
const actionRef = useRef<ActionType>();
const formRef = useRef<FormInstance>();
// 表格数据
const [tableData, setTableData] = useState<any>()
// 当前的百旺配置详情
const [BWDetailObj, setBWDetailObj] = useState<any>()
// 显示表格
const [showTable, setShowTable] = useState<boolean>(false)
// 当前用户详情
const [userInfo, setUserInfo] = useState<any>()
useEffect(() => {
// 开始刷脸认证的流程
handleGetFacialAuthentication()
}, [])
// 开始刷脸认证的流程
const handleGetFacialAuthentication = async () => {
let url = window.location.search.split('?')[1]
console.log('url', url);
const params = new URLSearchParams(url);
console.log('params', params);
let resultObj: any = {};
for (const [key, value] of params.entries()) {
resultObj[key] = value;
}
console.log('resultObj', resultObj);
let code = "menuCloud"
if (resultObj.UserIdEncrypted) {
handleGetUserInfo(resultObj.UserIdEncrypted)
}
const detail = await handleGetInvoiceDetail(code)
let detailObj = detail.data
setBWDetailObj(detail.data)
// taxNo
// let req: any = {
// code: code
// }
// const data = await handleGetBWTokenFun(req)
// console.log('data', data);
// let token = data.data.data.token
// console.log('token', token);
let codeReq: any = {
qrCodeType: 0,
operatorType: 0
}
// 刷脸二维码的值
const qrCodeData = await handleGetCertifyQrcode('menuCloud', codeReq)
console.log('qrCodeData', qrCodeData);
console.log('321', qrCodeData.data.response.qrCode);
let length = qrCodeData.data.response.qrCode.split(',') && qrCodeData.data.response.qrCode.split(',').length > 0 ? qrCodeData.data.response.qrCode.split(',').length : 0
let img = qrCodeData.data.response.qrCode.split(',')[length - 1]
let src = `data:image/png;base64,${img}`
const instance = confirm({
width: 600,
content: <div>
<div style={{ fontSize: '18px', fontWeight: 600, display: 'flex', justifyContent: 'center' }}></div>
<div style={{ width: '100%', height: '300px', display: 'flex', justifyContent: 'center' }}>
<img style={{ width: '300px', height: '300px' }} src={src} />
</div>
</div>,
footer: false
});
// handleGetCertifyQrcodeResult
timerRef.current = setInterval(async () => {
const resultData = await handleGetCertifyQrcodeResult('menuCloud', {
certifyQueryType: 1
})
let res: any = resultData.data.response
console.log('res', res);
if (res.certificationStatus == '1') {
if (timerRef.current) {
clearInterval(timerRef.current)
timerRef.current = null
}
instance.destroy();
message.success({ content: '认证成功!' })
setShowTable(true)
}
}, 2000);
}
// 列表显示内容
const columns: any = [
{
title: "统计日期",
dataIndex: "staticDate",
hideInTable: true,
valueType: "dateRange",
initialValue: [moment().startOf('M'), moment()],
search: {
transform: (value: any) => {
return {
beginDate: moment(value[0]).format('YYYY-MM-DD'),
endDate: moment(value[1]).format('YYYY-MM-DD')
};
},
},
fieldProps: {
picker: "day",
format: 'YYYY-MM-DD',
}
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "serialNo",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "orderNo",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "orderDateTime",
hideInSearch: true,
width: 150,
ellipsis: true,
render: (_, record) => {
return record?.orderDateTime ? moment(record?.orderDateTime).format('YYYY-MM-DD HH:mm:ss') : ""
}
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "invoiceTime",
hideInSearch: true,
width: 150,
ellipsis: true,
render: (_, record) => {
return record?.invoiceTime ? moment(record?.invoiceTime).format('YYYY-MM-DD HH:mm:ss') : ""
}
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "invoiceTerminalCode",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "taxNo",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "sellerName",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "buyerTaxNo",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "buyerName",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
dataIndex: <div style={{ textAlign: 'center' }}>invoiceType</div>,
title: "开票类型",
hideInSearch: true,
width: 150,
ellipsis: true,
render: (_, record) => {
return record?.invoiceType === 1 ? '蓝票' : record?.invoiceType === 2 ? '红票' : ''
}
},
{
dataIndex: <div style={{ textAlign: 'center' }}>invoiceTypeCode</div>,
title: "发票种类编码",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
dataIndex: <div style={{ textAlign: 'center' }}>remarks</div>,
title: "备注",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "invoiceCode",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "invoiceNo",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "digitInvoiceNo",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "invoiceTotalPrice",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "invoiceTotalTax",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "invoiceTotalPriceTax",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "status",
hideInSearch: true,
width: 150,
ellipsis: true,
render: (_, record) => {
return record?.status === 0 ? '开票中' : record?.status === 1 ? '开票完成' : record?.status === 2 ? '开票失败' : record?.status === 3 ? '发票已作废' : record?.status === 4 ? '发票作废中' : ''
}
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "statusMessage",
hideInSearch: true,
width: 150,
ellipsis: true,
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "pdfUrl",
hideInSearch: true,
width: 150,
ellipsis: true,
render: (_, recprd) => {
return <a></a>
}
},
{
title: '操作',
dataIndex: "options",
width: 100,
hideInSearch: true,
fixed: 'right',
render: (_, record) => {
return <Space>
<a onClick={() => {
console.log('record', record);
handleInvoicing(record)
}}>
</a>
<a onClick={() => {
handleCancelInvoicing(record)
}}>
</a>
</Space>
}
}
]
// 开票
const handleInvoicing = async (obj: any) => {
const req: any = {
// taxNo: BWDetailObj.taxNo,
invoiceCode: obj.invoiceCode, // 发票代码
invoiceNo: obj.invoiceNo, // 发票号码
digitInvoiceNo: obj.digitInvoiceNo, // 数电发票号码
pushPhone: obj.pushPhone, // 交付手机
pushEmail: obj.pushEmail // 交付邮箱
}
console.log('req', req);
// return
const data = await handleGetDeliver('menuCloud', req)
console.log('data', data);
}
// 作废
const handleCancelInvoicing = async (obj: any) => {
console.log('obj', obj);
const req: any = {
taxNo: BWDetailObj.taxNo,
serialNo: obj.serialNo,
orderNo: obj.orderNo,
invoiceCode: obj.invoiceCode,
invoiceNo: obj.invoiceNo,
invalidOperator: "操作人"
}
const data = await handleCancel('menuCloud', req)
}
// 获取用户信息
const handleGetUserInfo = async (UserIdEncrypted: string) => {
const req: any = { UserIdEncrypted: UserIdEncrypted }
const data = await handleGetUserInfoById(req)
console.log('handleGetUserInfo', data);
let userDetail: any = data.Result_Data
setUserInfo(userDetail)
}
return (
<div>
{
showTable ?
<ProTable
actionRef={actionRef}
formRef={formRef}
columns={columns}
bordered
expandable={{
expandRowByClick: true
}}
rowKey={(record) => {
return `${record?.id}`
}}
scroll={{ x: "100%", y: 'calc(100vh - 400px)' }}
headerTitle={<span style={{ color: "#1890ff", fontSize: 14, fontWeight: 600 }}></span>}
search={{ span: 6 }}
request={async (params: any) => {
console.log('params', params);
// startOf('M').
let [state, end] = [moment().format('YYYY-MM-DD 00:00:00'), moment().format('YYYY-MM-DD 23:59:59')]
const req: any = {
// serialNo: "25032610065056001376",
taxNo: BWDetailObj.taxNo || '',
pageNo: params?.current || 1,
pageSize: params?.pageSize || 1,
beginDate: state || '',
endDate: end || ''
}
const data = await handleGetInvoicePageTable('menuCloud', req)
console.log('data3232', data);
let tableData: any = data.data.response
console.log('tableData3232', tableData);
if (tableData.list && tableData.list.length > 0) {
return {
data: tableData.list,
success: true,
total: tableData.total, // 总数据条数
current: tableData.pageNo, // 当前页码
pageSize: tableData.pageSize, // 每页条数
}
}
return { data: [], success: true }
}}
/> : ""
}
</div>
)
}
export default connect(({ user }: ConnectState) => ({
currentUser: user.data
}))(InvoicingIndex);

View File

@ -0,0 +1,52 @@
import request from "@/utils/request"
import requestOld from "@/utils/requestOld"
// 百旺token
export async function handleGetBWTokenFun(params?: any) {
const data = await request.get('/baiwang/auth/token', { params })
return data
}
// 拿到人脸认证的二维码
export async function handleGetCertifyQrcode(code?: any, params?: any) {
const data = await request.post(`/baiwang/auth/certify-qrcode/${code}`, params)
return data
}
// 人脸认证的试试状态
export async function handleGetCertifyQrcodeResult(code?: any, params?: any) {
const data = await request.post(`/baiwang/auth/certify-result/${code}`, params)
return data
}
// 发票列表查询
export async function handleGetInvoicePageTable(code?: any, params?: any) {
const data = await request.post(`/api/baiwang/invoice/query-page/${code}`, params)
return data
}
// 获得百旺指定的配置
export async function handleGetInvoiceDetail(code?: any) {
const data = await request.get(`/baiwang-config/${code}`)
return data
}
// 百旺的发票交付接口
export async function handleGetDeliver(code?: any, params?: any) {
const data = await request.post(`/api/baiwang/invoice/push/${code}`, params)
return data
}
// 百旺的发票作废接口
export async function handleCancel(code?: any, params?: any) {
const data = await request.post(`/api/baiwang/invoice/invalid/${code}`, params)
return data
}
// 用地址栏传入的加密用户id去查询用户详情
export async function handleGetUserInfoById(params: any) {
const data = await requestOld.get(`/Logging/GetPassportInfoById?UserIdEncrypted=${params.UserIdEncrypted}`, params)
return data
}

7
src/pages/authority.ts Normal file
View File

@ -0,0 +1,7 @@
const authority: PageAuthority = {
'/test/index': ['/test/index'],
'/Invoicing/index': ['/Invoicing/index'],
};
export default authority;

885
src/pages/contract/list.tsx Normal file
View File

@ -0,0 +1,885 @@
// import moment from "moment";
// import numeral from 'numeral';
// import { connect } from "umi";
// import React, { useRef, useState } from "react";
// import { PageContainer } from "@ant-design/pro-layout";
// import ProTable from "@ant-design/pro-table";
// import { FormInstance, Tooltip } from "antd";
// import { Button, DatePicker, Drawer, message, Modal, Popconfirm, Select, Typography } from "antd";
// import { ExclamationCircleOutlined, FileSearchOutlined, PlusOutlined } from "@ant-design/icons";
// import ContractEdit from "./components/editor"
// import ContractDetail from "./components/detail"
// import type { ContractListModel } from "./data";
// import type { CurrentUser } from '@/models/user';
// import type { ConnectState } from "@/models/connect";
// import type { ActionType, ProColumns } from "@ant-design/pro-table";
// import { contractType } from "./emun";
// import { exportExcel, getBase64 } from '@/utils/utils';
// import { delCompact, getList, handleContractSupple } from "./service";
// import ProForm, { ProFormDatePicker, ProFormSelect, ProFormTextArea, ProFormUploadButton } from "@ant-design/pro-form";
// import { getProjectList } from "@/pages/BussinessProject/service";
// import useRequest from "@ahooksjs/use-request";
// import { deletePicture, uploadPicture } from "@/services/picture";
// import type { UploadFile } from "antd/es/upload/interface";
// import fileIcon from '@/assets/detail/fileIcon.svg'
// import session from "@/utils/session";
// import PageTitleBox from "@/components/PageTitleBox";
// // 删除数据
// const handelDelete = async (registerCompactId: number) => {
// const hide = message.loading('正在删除...');
// try {
// const result = await delCompact(registerCompactId);
// hide();
// if (result.Result_Code !== 100) {
// message.error(`${result.Result_Desc}`);
// return false;
// }
// message.success('删除成功!');
// return true;
// } catch (error) {
// hide();
// message.error('删除失败');
// return false;
// }
// };
// // 上传附件
// const customUploadRequest = async (fileList: UploadFile[], tableId: string) => {
// if (!fileList.length) {
// message.error("您上传的附件不存在.")
// return false
// }
// const formData = new FormData();
// fileList.forEach(file => {
// formData.append('files[]', file);
// });
// formData.append('TableType', '1115');
// formData.append('TableId', tableId);
// const success = await uploadPicture(formData)
// if (success) {
// return true
// }
// return false
// }
// // const ContractTable: React.FC<{ currentUser?: CurrentUser }> = ({ currentUser }) => {
// const ContractTable: React.FC<{ currentUser: any }> = (props) => {
// const { currentUser } = props
// // props
// const [currentRow, setCurrentRow] = useState<ContractListModel | undefined>(undefined)
// const [showDetail, setShowDetail] = useState<number>(0)
// const actionRef = useRef<ActionType>()
// const [searchParams, setSearchParams] = useState<any>();
// const [showBtn, setShowBtn] = useState<boolean>(false)
// const editorRef = useRef<ActionType>()
// const modalRef = useRef<FormInstance>()
// // 显示附属合同的悬浮框
// const [showModal, setShowModal] = useState<boolean>(false)
// // 悬浮弹出框选中的内容
// const [contract, setContract] = useState<any>()
// // 悬浮框表格数据
// const [modalTableData, setModalTableData] = useState<any>()
// // 结算模式枚举对象
// const [modesObj, setModesObj] = useState<any>()
// // 切換日期的时间
// const [changeDayDate, setChangeDayDate] = useState<any>()
// // modal的loading
// const [modalLoading, setModalLoading] = useState<any>(false)
// // 拿到结算模式列表
// const SETTLEMENT_MODESList = session.get("SETTLEMENT_MODESList")
// const SETTLEMENT_MODESObj = session.get("SETTLEMENT_MODESObj")
// // 合同类型对象
// const compactType = session.get("COMPACT_CHARACTERObj")
// // 合同类型子项
// const COMPACT_DETAILS = session.get("COMPACT_DETAILSObj")
// // 右上角选择的类型
// const [BIGCOMPACT_DETAILS, SETBIGCOMPACT_DETAILS] = useState<any>()
// // 退场时间
// const [exitTime, setExitTime] = useState<any>()
// // 子组件详情数据
// const [childrenData, setChildrenData] = useState<any>()
// // 是否显示新增附属合同
// const [addSubsidiary, setAddSubsidiary] = useState<boolean>(false)
// // 拿到子组件详情
// const handleGetChildrenData = (obj: any) => {
// if (obj.COMPACT_DETAILS === 1000) {
// setAddSubsidiary(true)
// } else {
// setAddSubsidiary(false)
// }
// setChildrenData(obj)
// }
// // 拿到改变的合同类型子项
// const handleGetContractChild = (obj: any) => {
// if (obj === 1000) {
// setAddSubsidiary(true)
// } else {
// setAddSubsidiary(false)
// }
// }
// // 附件文件
// const [fileList, setFileList] = useState<UploadFile[]>([]) // 需要上传的附件图片列表
// const [priviewImage, setPriviewImage] = useState<string>(); // 预览的文件地址
// const [COMPACTLIST, setCOMPACTLIST] = useState<any>()
// const { loading: COMPACTLoading, data: COMPACTLISTS } = useRequest(async () => {
// const options = session.get("COMPACT_DETAILSList")
// if (options && options.length > 0) {
// options.forEach((item: any) => {
// if (item.value !== 2002 && item.value !== 3000) {
// item.disabled = true
// item.label = <Tooltip title={'请跳转到关联合同内添加!'}>
// <span style={{ color: 'rgba(0,0,0,0.25)' }}>{item.label}</span>
// </Tooltip>
// }
// })
// }
// setCOMPACTLIST(options)
// })
// const [columnsStateMap, setColumnsStateMap] = useState<any>({
// THREEPART_LINKMAN: {
// show: false,
// }
// });
// const columns: ProColumns<ContractListModel>[] = [
// // {
// // title: '所属服务区',
// // dataIndex: 'SERVERPART_ID',
// // valueType: 'select',
// // request: async () => {
// // return await getServerpartOption();
// // }
// // },
// {
// title: '查询合同',
// dataIndex: 'searchKey',
// hideInTable: true,
// hideInDescriptions: true,
// fieldProps: {
// placeholder: "请输入合同编号/名称/乙方"
// },
// },
// {
// title: '合同编号',
// dataIndex: 'COMPACT_CODE',
// hideInSearch: true,
// hideInTable: true,
// },
// {
// title: '合同名称',
// dataIndex: 'COMPACT_NAME',
// // ellipsis: true,
// hideInSearch: true,
// hideInDescriptions: true,
// // width:'25%',
// sorter: true,
// ellipsis: true,
// render: (_, record) => {
// return <a onClick={() => {
// setCurrentRow(record)
// setShowDetail(1)
// }}>{record.COMPACT_NAME}</a>
// },
// },
// {
// title: '经营模式',
// dataIndex: 'BUSINESS_TYPE',
// valueType: 'select',
// valueEnum: contractType,
// width: 110,
// align: "center",
// // hideInSearch: true,
// },
// {
// title: '合同金额',
// dataIndex: 'COMPACT_AMOUNT',
// hideInSearch: true,
// align: 'right',
// width: 130,
// sorter: true,
// render: (_) => {
// return <span style={{ paddingRight: 16 }}>{numeral(_).format('0,0.00')}</span>
// },
// },
// {
// title: '乙方',
// dataIndex: 'SECONDPART_NAME',
// hideInDescriptions: true,
// hideInSearch: true,
// sorter: true,
// width: 130,
// ellipsis: true,
// },
// {
// title: '丙方联系人',
// dataIndex: 'THREEPART_LINKMAN',
// hideInDescriptions: true,
// hideInSearch: true,
// align: 'center',
// width: 130,
// sorter: true,
// },
// {
// title: '合同主体',
// dataIndex: 'SERVERPART_NAME',
// ellipsis: true,
// hideInDescriptions: true,
// hideInSearch: true,
// width: 120,
// align: 'center',
// },
// {
// title: '关联合同',
// dataIndex: 'RELATE_COMPACT',
// hideInDescriptions: true,
// hideInSearch: true,
// width: 100,
// align: 'center',
// render: (_, record) => {
// return <span>{record?.RELATE_COMPACT > 0 ? '是' : ''}</span>
// }
// },
// {
// title: '附件',
// dataIndex: 'ATTACHMENT_STATE',
// align: 'center',
// width: 60,
// render: (_, record) => {
// return <div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
// {
// record?.ATTACHMENT_STATE === 1000 ?
// <img style={{ width: '15px', height: '15px' }} src={fileIcon} /> : ''
// }
// </div>
// }
// },
// {
// title: '到期日期',
// dataIndex: 'dateRange',
// valueType: "dateRange",
// hideInTable: true,
// hideInDescriptions: true,
// fieldProps: {
// ranges: {
// "一月内": [moment(), moment().add(1, 'months')],
// "三月内": [moment(), moment().add(3, 'months')],
// "六月内": [moment(), moment().add(6, 'months')],
// }
// }
// },
// {
// title: '合同状态',
// dataIndex: 'COMPACT_STATE',
// valueType: 'select',
// initialValue: '1000',
// // hideInSearch: true,
// hideInTable: true,
// valueEnum: { 0: { text: '无效', status: 'error' }, 1: { text: '待补充', status: 'process' }, 1000: { text: '有效', status: 'success' } },
// },
// {
// title: '合同类型',
// dataIndex: 'COMPACT_TYPE',
// valueType: 'select',
// align: 'center',
// width: 130,
// valueEnum: compactType,
// ellipsis: true,
// },
// {
// title: '类型子项',
// dataIndex: 'COMPACT_DETAILS',
// valueType: 'select',
// align: 'center',
// width: 130,
// valueEnum: COMPACT_DETAILS,
// ellipsis: true,
// },
// {
// title: '开始日期',
// hideInSearch: true,
// dataIndex: 'COMPACT_STARTDATE',
// valueType: 'date',
// sorter: true,
// width: 110
// },
// {
// title: '结束日期',
// hideInSearch: true,
// dataIndex: 'COMPACT_ENDDATE',
// valueType: 'date',
// sorter: true,
// width: 110
// },
// {
// title: '操作人',
// dataIndex: 'STAFF_NAME',
// hideInSearch: true,
// hideInTable: true
// },
// {
// dataIndex: 'OPERATE_DATE',
// title: '更新时间',
// valueType: 'fromNow',
// hideInTable: true,
// hideInSearch: true,
// },
// {
// // 如果是商户账号,则只能查看合同信息,不能做编辑、删除操作
// title: '操作',
// dataIndex: 'option',
// valueType: 'option',
// hideInDescriptions: true,
// hideInSearch: true,
// hideInTable: currentUser?.UserPattern === 2000,
// width: 120,
// render: (_, record) => {
// return [
// // <a onClick={() => { history.push(`/contract/detail/${record.REGISTERCOMPACT_ID}`) }}>查看</a>,
// <a onClick={() => {
// setCurrentRow(record)
// setShowDetail(2)
// // history.push(`contract/edit/${record.REGISTERCOMPACT_ID}`)
// }}>编辑</a>,
// <Popconfirm
// title="确认删除该合同记录吗?"
// onConfirm={async () => {
// const sucesse = await handelDelete(record.REGISTERCOMPACT_ID);
// if (sucesse && actionRef.current) {
// actionRef.current.reload();
// }
// }}
// >
// <a>删除</a>
// </Popconfirm>
// ]
// }
// }
// ];
// // 附属合同的表格字段
// const modalColumns: any = [
// {
// dataIndex: 'BUSINESSPROJECT_NAME',
// title: '项目名称',
// align: 'center',
// ellipsis: true,
// hideInSearch: true,
// },
// {
// dataIndex: 'SERVERPARTSHOP_NAME',
// title: '门店名称',
// align: 'center',
// hideInSearch: true,
// ellipsis: true
// },
// {
// title: '合同金额',
// dataIndex: 'GUARANTEE_PRICE',
// hideInSearch: true,
// width: 130,
// align: "center",
// render: (_) => {
// return <div style={{ width: 100, textAlign: "right" }}>{_}</div>
// }
// },
// {
// title: '开始日期',
// hideInSearch: true,
// dataIndex: 'PROJECT_STARTDATE',
// valueType: 'date',
// align: 'center',
// sorter: true,
// width: 110
// },
// {
// title: '结束日期',
// hideInSearch: true,
// dataIndex: 'PROJECT_ENDDATE',
// valueType: 'date',
// align: 'center',
// sorter: true,
// width: 110
// },
// {
// title: '结算模式',
// width: 110,
// dataIndex: 'SETTLEMENT_MODES',
// align: 'center',
// valueEnum: SETTLEMENT_MODESObj
// },
// {
// title: '切换结算模式',
// width: 140,
// dataIndex: 'SWITCH_MODES',
// align: 'center',
// render: (_, record) => {
// return <Select
// style={{ width: '135px' }}
// options={(SETTLEMENT_MODESList?.filter(d => d.value !== record.SETTLEMENT_MODES) || []).map((item) => ({
// value: item.value,
// label: item.label,
// }))}
// value={record.SWITCH_MODES}
// onChange={(e: any) => {
// const list = JSON.parse(JSON.stringify(modalTableData))
// list.forEach((item: any) => {
// if (item.BUSINESSPROJECT_ID === record.BUSINESSPROJECT_ID) {
// item.SWITCH_MODES = e
// }
// })
// setModalTableData(list)
// }}
// ></Select>
// }
// },
// {
// title: '切换日期',
// width: 130,
// dataIndex: 'SwitchDate',
// align: 'center',
// render: (_, record) => {
// return <DatePicker value={record.SwitchDate} onChange={(e: any) => {
// const list = JSON.parse(JSON.stringify(modalTableData))
// list.forEach((item: any) => {
// if (item.BUSINESSPROJECT_ID === record.BUSINESSPROJECT_ID) {
// item.SwitchDate = moment(e._d)
// }
// })
// setModalTableData(list)
// }} />
// }
// },
// {
// title: '退场日期',
// hideInSearch: true,
// dataIndex: 'exitTime',
// valueType: 'date',
// sorter: true,
// width: 140,
// render: (_, record) => {
// return <DatePicker value={record.exitTime} onChange={(e: any) => {
// const list = JSON.parse(JSON.stringify(modalTableData))
// list.forEach((item: any) => {
// if (item.BUSINESSPROJECT_ID === record.BUSINESSPROJECT_ID) {
// if (e) {
// item.exitTime = moment(e._d)
// } else {
// item.exitTime = undefined
// }
// }
// })
// setModalTableData(list)
// }} />
// }
// },
// ]
// return (
// <PageContainer header={{
// title: '',
// breadcrumb: {},
// }}>
// <ProTable<ContractListModel>
// headerTitle={<PageTitleBox props={props} />}
// rowKey="REGISTERCOMPACT_ID"
// request={async (params, sorter) => {
// const sortstr = Object.keys(sorter).map(n => {
// const value = sorter[n]
// return value ? `${n} ${value.replace('end', '')}` : ''
// })
// const [start, end] = params?.dateRange || ['', '']
// const searchWholeParams = {
// searchParameter: { ...params, COMPACT_STARTDATE: start, COMPACT_ENDDATE: end }, sortstr: sortstr.length ? sortstr.toString() : "",
// keyWord: params.searchKey ? { key: "COMPACT_CODE,SECONDPART_NAME,COMPACT_NAME", value: params.searchKey } : null, // 关键词查询
// pagesize: 999999
// }
// const list = await getList(searchWholeParams)
// setSearchParams(searchWholeParams);
// return list
// }}
// actionRef={actionRef}
// search={{ span: 6 }}
// columns={columns}
// toolbar={{
// // 如果是商户账号,则只能查看合同信息,不能做新增操作
// actions: currentUser?.UserPattern !== 2000 ? [
// <Typography.Text type="secondary">单位:万元</Typography.Text>,
// <Button
// key="new"
// type="primary"
// onClick={async () => {
// const data = await getList(searchParams);
// if (data.data && data.data.length > 0) {
// data.data.forEach((c: any, i) => {
// c.BUSINESS_TYPE = c.BUSINESS_TYPE ? contractType[c.BUSINESS_TYPE] : '-';
// c.COMPACT_STARTDATE = c.COMPACT_STARTDATE ? moment(c.COMPACT_STARTDATE).format('YYYY/MM/DD') : '-';
// c.COMPACT_ENDDATE = c.COMPACT_ENDDATE ? moment(c.COMPACT_ENDDATE).format('YYYY/MM/DD') : '-';
// })
// }
// const success = await exportExcel(
// columns.filter(n => !n.hideInTable && n.dataIndex !== 'option'),
// data.data || [],
// `备案合同_${moment().format('YYYY/MM/DD')}`,
// );
// if (success.message !== 'ok') {
// message.info({ content: success.message });
// }
// }}
// >
// 导出excel
// </Button>,
// <Button
// key="new"
// icon={<PlusOutlined />}
// type="primary"
// onClick={() => {
// setShowDetail(2)
// // history.push('contract/create')
// }}
// >
// 合同
// </Button>,
// ] : [],
// }}
// columnsState={{
// value: columnsStateMap,
// onChange: setColumnsStateMap,
// }}
// pagination={{ defaultPageSize: 10 }}
// />
// <Drawer
// className={showDetail === 1 ? "contract-drawer" : "contract-detail-drawer"}
// width="73%"
// visible={showDetail !== 0}
// onClose={() => {
// setCurrentRow(undefined);
// setShowDetail(0);
// SETBIGCOMPACT_DETAILS(undefined)
// }}
// destroyOnClose
// title={showDetail === 2 ?
// <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
// <div className="card-title" style={{ fontWeight: 700 }}>{!currentRow ? "新增合同" : currentRow?.COMPACT_NAME}</div>
// {
// currentRow && addSubsidiary ?
// <ProForm
// style={{ width: '350px' }}
// className={'subsidiaryBox'}
// layout={'horizontal'}
// submitter={{
// render: (_, record) => {
// }
// }}
// >
// <ProFormSelect
// name="BIGCOMPACT_DETAILS"
// label="新增附属合同"
// placeholder="请选择合同类型"
// // rules={[
// // {
// // required: true,
// // message: '请选择合同类型',
// // },
// // ]}
// // request={async () => {
// // const options = session.get("COMPACT_DETAILSList")
// // return options;
// // }}
// options={COMPACTLIST}
// addonAfter={showBtn ?
// <Button type="primary" icon={<FileSearchOutlined />}
// onClick={() => {
// setShowModal(true)
// }}>选择</Button> :
// false
// }
// fieldProps={{
// onChange: (e: any) => {
// SETBIGCOMPACT_DETAILS(e)
// if (e === 3000 || e === 2002) {
// setShowBtn(true)
// } else {
// setShowBtn(false)
// }
// },
// }}
// />
// </ProForm> : ''
// }
// </div>
// : ""}
// closable={false}
// bodyStyle={{ backgroundColor: "#f9f9f9", padding: 0 }}
// >
// {/* 编辑/查看合同 */}
// {showDetail !== 0 && currentRow !== undefined && (showDetail === 1 ?
// <ContractDetail contractId={currentRow?.REGISTERCOMPACT_ID} currentRow={currentRow} clickedId={[currentRow?.REGISTERCOMPACT_ID]}></ContractDetail> :
// <ContractEdit handleGetChildrenData={handleGetChildrenData} handleGetContractChild={handleGetContractChild} currentUser={currentUser} onRef={editorRef} contractId={currentRow?.REGISTERCOMPACT_ID} actionRef={actionRef} setShowDetail={setShowDetail}></ContractEdit>)
// }
// {/* 新增合同 */}
// {
// !currentRow && showDetail !== 0 && <ContractEdit actionRef={actionRef} setShowDetail={setShowDetail} showType={'add'} />
// }
// </Drawer>
// <Modal
// title="附属合同"
// width={1400}
// style={{ height: '500px' }}
// destroyOnClose
// open={showModal}
// confirmLoading={modalLoading}
// onOk={() => {
// modalRef.current?.validateFields().then(async (res) => {
// if (res) {
// setModalLoading(true)
// if (contract && contract.length > 0) {
// } else {
// message.error('请先选择附属合同')
// setModalLoading(false)
// return
// }
// const list: any = []
// const completeList: any = []
// modalTableData.forEach((item: any) => {
// if (contract.indexOf(item.BUSINESSPROJECT_ID) !== -1) {
// list.push({ label: item.BUSINESSPROJECT_ID, value: item.SWITCH_MODES })
// completeList.push(item)
// }
// })
// const descValue: string = modalRef.current?.getFieldsValue()
// let req
// if (BIGCOMPACT_DETAILS === 2002) {
// if (!completeList[0].exitTime) {
// message.error('请选择退场日期')
// return
// }
// req = {
// CompactDetails: BIGCOMPACT_DETAILS,// 补充协议类型
// ContractId: childrenData?.REGISTERCOMPACT_ID,// 主合同内码
// // ClosedDate:moment(exitTime).format('YYYY-MM-DD'),// 撤场日期
// ClosedDate: moment(completeList[0].exitTime).format('YYYY-MM-DD'),// 撤场日期
// SettlementModes: list[0].value,// 切换后的结算模式
// SwitchDate: completeList[0].SwitchDate ? moment(completeList[0].SwitchDate).format('YYYY-MM-DD') : '',// 切换日期
// ProjectList: list,// 变更经营项目列表
// StaffId: currentUser?.ID,// 操作人内码
// StaffName: currentUser?.Name,// 操作人员
// CompactDPDesc: descValue && descValue.COMPACT_DPDESC ? descValue.COMPACT_DPDESC : '',// 注意事项
// }
// } else {
// if (list[0].value) {
// req = {
// CompactDetails: BIGCOMPACT_DETAILS,// 补充协议类型
// ContractId: childrenData?.REGISTERCOMPACT_ID,// 主合同内码
// SettlementModes: list[0].value,// 切换后的结算模式
// SwitchDate: completeList[0].SwitchDate ? moment(completeList[0].SwitchDate).format('YYYY-MM-DD') : '',// 切换日期
// ProjectList: list,// 变更经营项目列表
// StaffId: currentUser?.ID,// 操作人内码
// StaffName: currentUser?.Name,// 操作人员
// CompactDPDesc: descValue && descValue.COMPACT_DPDESC ? descValue.COMPACT_DPDESC : '',// 注意事项
// }
// } else {
// message.error('请选择结算模式')
// setModalLoading(false)
// return
// }
// }
// const data = await handleContractSupple(req)
// if (data) {
// const waitUpload = fileList.filter(n => n.status !== 'done')
// if (waitUpload.length > 0) {
// await customUploadRequest(waitUpload, data.Result_Data?.REGISTERCOMPACT_ID)
// }
// // actionRef.current.reload();
// }
// if (data.Result_Code === 100) {
// message.success(data.Result_Desc)
// setShowModal(false)
// setFileList([])
// actionRef.current?.reload()
// } else {
// message.error(data.Result_Desc)
// }
// setModalLoading(false)
// }
// })
// }}
// onCancel={() => {
// setModalTableData([])
// setShowModal(false)
// setContract([])
// setFileList([])
// setExitTime(undefined)
// setModalLoading(false)
// }}>
// <ProForm
// layout={'horizontal'}
// formRef={modalRef}
// submitter={{
// render: (_, record) => {
// }
// }}
// >
// <ProFormUploadButton
// name="ImgList"
// title="上传附件"
// label="合同附件"
// wrapperCol={{ span: 20 }}
// labelCol={{ span: 3 }}
// rules={[
// {
// required: true,
// message: '请上传合同文件',
// }
// ]}
// fieldProps={{
// name: 'files',
// listType: 'text',
// className: 'is-dragging',
// // defaultFileList: priviewFileList,
// fileList,
// multiple: true,
// onPreview: async (file) => {
// if (file.type && file.type?.indexOf('image/') > -1) { // 未上传的文件 如果是图片类型的
// if (!file.url && !file.preview) {
// setPriviewImage(await getBase64(file.lastModifiedDate))
// } else {
// setPriviewImage(file?.url)
// }
// } else {
// const filenameSplitPointArr = file.fileName?.split('.') || []
// if (['png', 'jpg', 'jpeg'].indexOf(filenameSplitPointArr[filenameSplitPointArr?.length - 1]) > -1) {
// setPriviewImage(file?.url)
// }
// else if (file?.url) {
// window.open(file?.url)
// }
// }
// },
// beforeUpload: (file, files) => {
// setFileList([...fileList, ...files])
// return false
// },
// onChange: async (info: any) => {
// if (info.file.status === 'removed') {
// // 如果在待上传列表中找到,则说明当前图片没有上传服务器,可直接删除
// const index = fileList.findIndex(n => n.uid === info.file.uid);
// if (!info.file?.deletepath) {
// const newFileList = fileList.slice();
// newFileList.splice(index, 1);
// setFileList(newFileList)
// return
// }
// confirm({
// title: '确认删除该文件吗?',
// icon: <ExclamationCircleOutlined />,
// async onOk() {
// const deleteLoading = message.loading('正在删除...')
// const success = await deletePicture(info.file?.deletepath, info.file?.uid, '', '3000')
// deleteLoading()
// if (success) {
// const files = [...fileList]
// files.splice(index, 1)
// setFileList(files)
// }
// else {
// message.error("删除失败")
// }
// },
// onCancel() {
// },
// });
// }
// }
// }}
// />
// <ProFormTextArea
// label="合同注意事项"
// width={800}
// name="COMPACT_DPDESC"
// fieldProps={{
// autoSize: { minRows: 1, maxRows: 6 },
// }}
// placeholder="请输入合同注意事项"
// wrapperCol={{ span: 20 }}
// labelCol={{ span: 3 }}
// />
// {/* { */}
// {/* BIGCOMPACT_DETAILS===2002? */}
// {/* <ProFormDatePicker */}
// {/* name="exitTime" */}
// {/* label="退场日期" */}
// {/* wrapperCol={{ span: 20 }} */}
// {/* labelCol={{ span: 3 }} */}
// {/* rules={[ */}
// {/* { */}
// {/* required: true, */}
// {/* message: '请选择退场日期', */}
// {/* } */}
// {/* ]} */}
// {/* fieldProps={{ */}
// {/* onChange:(e: any)=>{ */}
// {/* if (e){ */}
// {/* setExitTime(e._d) */}
// {/* } */}
// {/* } */}
// {/* }} */}
// {/* />:'' */}
// {/* } */}
// </ProForm>
// {/* <div style={{display:'flex',alignItems:'center'}}> */}
// {/* <p>退场时间</p> */}
// {/* <DatePicker onChange={(e: any)=>{ */}
// {/* }} /> */}
// {/* </div> */}
// <ProTable
// search={false}
// pagination={false}
// options={false}
// rowKey={'BUSINESSPROJECT_ID'}
// style={{ minHeight: 480 }}
// columns={BIGCOMPACT_DETAILS === 2002 ? [...modalColumns.slice(0, 5), modalColumns[modalColumns.length - 1]] : modalColumns.slice(0, modalColumns.length - 2)}
// request={async () => {
// const req = {
// PROJECT_VALID: 1,
// REGISTERCOMPACT_ID: currentRow?.REGISTERCOMPACT_ID,
// PageIndex: 1,
// pagesize: 9999,
// keyWord: null
// }
// const data = await getProjectList(req)
// setModalTableData(data.data)
// // return data
// }}
// dataSource={modalTableData}
// rowSelection={{ // 可选择的表格配置
// type: "checkbox", // 该表格为单选
// onChange: (selectedRowKeys, selectedRows) => {
// // 选中行发生改变时,存储选中行的数据
// setContract(selectedRowKeys)
// }
// }}
// />
// </Modal>
// </PageContainer>
// );
// }
// export default connect(({ user }: ConnectState) => ({
// currentUser: user.data
// }))(ContractTable);

22
src/pages/index.tsx Normal file
View File

@ -0,0 +1,22 @@
import { PureComponent } from 'react';
import { connect } from 'umi';
import type { UserConnectedProps } from '@/models/user';
class Index extends PureComponent<UserConnectedProps> {
render() {
const { user } = this.props;
const { data } = user;
const { name } = data;
return (
<div>{name}</div>
);
}
}
export default connect(
({ user }: { user: UserConnectedProps['user'] }) => ({
user,
}),
)(Index);

325
src/pages/test/index.tsx Normal file
View File

@ -0,0 +1,325 @@
import { ConnectState } from "@/models/global";
import { ActionType, FormInstance, ProTable } from "@ant-design/pro-components";
import { connect } from "umi";
import moment from "moment";
import { Button, Drawer, Image, message, Popconfirm, Space } from "antd";
import { useRef, useState } from "react";
const examineRecord: React.FC<{ currentUser: any }> = (props) => {
const { currentUser } = props
const actionRef = useRef<ActionType>();
const formRef = useRef<FormInstance>();
const recordDetailRef = useRef<any>()
// 显示的附件数据
const [showImgList, setShowImgList] = useState<string[]>([])
// 预览图片
const [imagePreviewVisible, setImagePreviewVisible] = useState<boolean>(false)
// 预览的索引
const [previewIndex, setPreviewIndex] = useState<number>(0)
// 当行数据
const [currentRow, setCurrentRow] = useState<any>()
// 显示详情抽屉
const [showDetail, setShowDetail] = useState<boolean>(false)
// 判断是否点了出现的是异常处理的抽屉
const [showAbnormal, setShowAbnormal] = useState<boolean>(false)
const [columnsStateMap, setColumnsStateMap] = useState<any>({
score: { show: false }
})
const columns: any = [
{
title: "统计日期",
dataIndex: "staticDate",
hideInTable: true,
valueType: "dateRange",
initialValue: [moment().startOf('M'), moment()],
search: {
transform: (value: any) => {
return {
startTime: moment(value[0]).format('YYYY-MM-DD'),
endTime: moment(value[1]).format('YYYY-MM-DD')
};
},
},
fieldProps: {
picker: "day",
format: 'YYYY-MM-DD',
}
},
{
title: "服务区",
dataIndex: "serverPartId",
hideInTable: true,
valueType: "select",
request: async () => {
const req = {
ProvinceCode: currentUser?.provinceCode,
StatisticsType: 1000
}
const data = await handleGetServerpartDDL(req)
return data
}
},
{
title: "巡查类型",
dataIndex: "inspectionType",
hideInTable: true,
valueType: "select",
valueEnum: {
"1": '异常',
"0": "正常"
}
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "serverPartName",
hideInSearch: true,
width: 150,
ellipsis: true,
render: (_, record) => {
return record?.template.serverPartName ? record?.template.serverPartName : "-"
}
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "placeName",
hideInSearch: true,
width: 150,
ellipsis: true,
render: (_, record) => {
return record?.template.title ? record?.template.title : "-"
}
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "placeName",
hideInSearch: true,
width: 100,
align: 'center',
ellipsis: true,
render: (_, record) => {
let res: any = record.extend ? JSON.parse(record.extend) : "-"
return <span style={{ color: res.situation === 1 ? "red" : "" }}>{res.situation === 1 ? '异常' : res.situation === 0 ? '正常' : ''}</span>
}
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "uploadResult",
hideInSearch: true,
width: 200,
ellipsis: true,
render: (_, record) => {
let extendObj = record?.extend ? JSON.parse(record?.extend) : ""
return extendObj?.uploadResult ? extendObj?.uploadResult : "-"
}
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "uploadResult",
hideInSearch: true,
width: 350,
ellipsis: true,
render: (_, record) => {
let str: string = ''
if (record?.questionResponses && record?.questionResponses.length > 0) {
record?.questionResponses.forEach((item: any, index: number) => {
let anwers: string = ''
if (item.choiceResponse && item.choiceResponse.length > 0) {
item.choiceResponse.forEach((subItem: string, subIndex: number) => {
anwers += `${subIndex > 0 ? '' : ''}${subItem}`
})
}
str += `${index > 0 ? '' : ''}考核内容:${item.question.title},考核结果:${anwers}`
})
}
return str || ''
}
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "score",
hideInSearch: true,
valueType: 'digit',
width: 100,
align: 'center',
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "createdAt",
hideInSearch: true,
width: 150,
ellipsis: true,
align: 'center',
render: (_, record) => {
return record?.createdAt ? moment(record?.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'
}
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "userName",
hideInSearch: true,
width: 100,
ellipsis: true,
align: 'center',
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "errorStatus",
hideInSearch: true,
width: 100,
ellipsis: true,
align: 'center',
render: (_, record) => {
let res: any = record.extend ? JSON.parse(record.extend) : "-"
return <span style={{ color: res.errorStatus === 0 ? "red" : res.errorStatus === 1 ? "#1677ff" : "" }}>{
res.errorStatus === 0
? "待处理"
: res.errorStatus === 1
? "处理中"
: res.errorStatus === 2
? "已处理"
: "-"
}</span>
}
},
{
title: <div style={{ textAlign: 'center' }}></div>,
dataIndex: "placeName",
hideInSearch: true,
width: 150,
ellipsis: true,
align: 'center',
render: (_, record) => {
let extendObj = record?.extend ? JSON.parse(record?.extend) : ""
let imgList = extendObj.imgsList
return imgList && imgList.length > 0 ?
<Button type="primary" onClick={() => {
setShowImgList(imgList)
setImagePreviewVisible(true)
}}></Button> : "-"
}
},
{
title: '操作',
dataIndex: 'option',
align: 'center',
fixed: "right",
hideInSearch: true,
width: 150,
render: (_: any, record: any) => {
let res: any = record.extend ? JSON.parse(record.extend) : "-"
return <Space>
{
res.situation === 1 ?
<a onClick={
() => {
setCurrentRow({
...record,
...res
})
setShowAbnormal(true)
setShowDetail(true)
}
}></a> : ""
}
< a onClick={() => {
setCurrentRow({
...record,
...res
})
setShowDetail(true)
}}>
</a >
<Popconfirm
title={"确认删除?"}
onConfirm={async () => {
deleteRecord(record?.id)
}}
>
<a></a>
</Popconfirm>
</Space >
}
}
]
// 删除记录
const deleteRecord = async (id: any) => {
const data = await handleDeleteRecord({ id: id })
if (data.code === 200) {
message.success(data.message)
actionRef.current?.reload()
}
}
return (
<div style={{ backgroundColor: '#fff', display: 'flex' }}>
<div style={{
// width: !collapsible ? 'calc(100% - 300px)' : 'calc(100% - 60px)',
width: "100%",
paddingTop: 0,
paddingBottom: 0,
paddingRight: 0
}}>
<ProTable
actionRef={actionRef}
formRef={formRef}
columns={columns}
bordered
expandable={{
expandRowByClick: true
}}
scroll={{ x: "100%", y: 'calc(100vh - 400px)' }}
headerTitle={<span style={{ color: "#1890ff", fontSize: 14, fontWeight: 600 }}></span>}
search={{ span: 6 }}
request={async (params) => {
}}
toolbar={{
}}
columnsState={{
value: columnsStateMap,
onChange: setColumnsStateMap,
}}
>
</ProTable>
</div>
{
showImgList && showImgList.length > 0 && <div style={{ display: 'none' }}>
<Image.PreviewGroup
preview={{
visible: imagePreviewVisible,
onVisibleChange: vis => {
setImagePreviewVisible(vis)
},
current: previewIndex
}}>
{
showImgList.map((n) =>
<Image src={n} key={n} />
)
}
</Image.PreviewGroup>
</div>
}
</div>
)
}
export default connect(({ user }: ConnectState) => ({
currentUser: user.data
}))(examineRecord);

105
src/services/user.ts Normal file
View File

@ -0,0 +1,105 @@
import request from '@/utils/request';
//登录
// export const userLogin = (params: Record<string, unknown>): Promise<API.LoginResponse> => (
// request.post('/api/user/login', params)
// );
// 登录
export async function userLogin(params: any) {
const data = await request.post('/auth/admin/login', params)
return data
}
//获取用户信息
export async function retrieveUserInfo(params: any) {
const data = await request.get('/auth/admin/profile', params)
return data
}
// export const retrieveUserInfo = (): Promise<API.UserInfoResponse> => (
// request.get('/api/user/info')
// );
//获取用户权限
export async function retrieveUserAuthority(params: any) {
const data = await request.get('/api/user/authority', params)
return data
}
// export const retrieveUserAuthority = (): Promise<API.UserAuthorityResponse> => (
// request.get('/api/user/authority')
// );
//获取菜单数据
export async function retrieveMenuData(params: any) {
// const data = await request.get('/menus', params)
// return data
return [
{
path: '/standard/index',
redirect: '',
name: '生成标准页面',
component: "@/pages/standard/index",
},
{
path: '/examine',
redirect: '',
name: '走动式管理',
children: [
{
path: '/examine/index',
name: '考评分类管理',
component: "@/pages/examine/index",
},
{
path: '/examine/question',
name: '考核问题管理',
component: "@/pages/examine/question",
},
{
path: '/examine/modal',
name: '考核模版管理',
component: "@/pages/examine/modal",
},
{
path: '/examine/record',
name: '考核记录管理',
component: "@/pages/examine/record",
}
]
},
]
}
// export const retrieveMenuData = (): Promise<API.MenuDataResponse> => (
// request.get('/api/user/menu')
// );
//获取用户信息和权限以及菜单
export const retrieveUserInfoAuthorityMenu = (): Promise<API.UserInfoAuthMenuResponse> => (
Promise.all([
retrieveUserInfo(),
// retrieveUserAuthority(),
retrieveMenuData(),
])
);
//获取用户权限以及菜单
export const retrieveUserAuthorityMenu = (): Promise<API.UserAuthMenuResponse> => (
Promise.all([
// retrieveUserAuthority(),
retrieveMenuData(),
])
);
//登出
export const userLogout = (): Promise<API.LogoutResponse> => (
request.post('/auth/logout')
);
//获取验证码
export const retrieveCaptcha = (params: Record<string, string>): Promise<API.CaptchaResponse> => (
request.get('/api/user/captcha', { params })
);

View File

@ -0,0 +1,4 @@
// 生成 tsx 页面的方法
export const handleCreatePageFile = () => {
}

View File

@ -0,0 +1,34 @@
/**
*
* @description , n级菜单,
* , ,
* @param currentMenuItem , url中的pathname不在菜单中, undefined
* @param indexAllMenuItemById id索引菜单项的映射
* @returns API.MenuItem[] | []
*/
const handleGetCurrentLocation = (
currentMenuItem: API.MenuItem | undefined,
indexAllMenuItemById: IndexAllMenuItemByKey<'id'>,
): API.MenuItem[] | [] => {
let res: API.MenuItem[] = [];
if (!currentMenuItem) return res;
res.push({
id: currentMenuItem.id,
key: currentMenuItem.key,
path: currentMenuItem.path,
label: currentMenuItem.label,
redirect: currentMenuItem.redirect,
});
if (currentMenuItem.pid) {
res = [
...res,
...handleGetCurrentLocation(indexAllMenuItemById[currentMenuItem.pid], indexAllMenuItemById),
];
}
return res.reverse();
};
export default handleGetCurrentLocation;

View File

@ -0,0 +1,32 @@
/**
* key索引每一条数据的映射
* @param data
* @param key key
* @returns key索引每一条数据的映射
*/
const handleGetEachDatumFromNestedDataByKey = <T extends { children?: T[] }>(
data: T[],
key: keyof T, //确定key的类型为T的键名
): Record<string, T> => {
//显式声明byKey的类型
let byKey: Record<string, T> = {};
data.forEach((datum) => {
//添加类型检查, 避免undefined作为对象的键名
if (typeof datum[key] !== 'undefined') {
//强制转换为字符串类型, 避免非字符串类型作为对象的键名
byKey[String(datum[key])] = datum;
}
if (datum.children) {
byKey = {
...byKey,
...handleGetEachDatumFromNestedDataByKey(datum.children, key),
};
}
});
return byKey;
};
export default handleGetEachDatumFromNestedDataByKey;

View File

@ -0,0 +1,25 @@
/**
* path索引菜单项的映射
* @param menu
* @returns path索引菜单项的映射
*/
const handleGetIndexValidMenuItemByPath = (
menu: API.MenuData,
): IndexValidMenuItemByPath => {
let byPath: IndexValidMenuItemByPath = {};
menu.forEach((item: API.MenuItem) => {
if (!item.children) {
byPath[item.path] = item;
} else {
byPath = {
...byPath,
...handleGetIndexValidMenuItemByPath(item.children),
};
}
});
return byPath;
};
export default handleGetIndexValidMenuItemByPath;

View File

@ -0,0 +1,18 @@
/**
* keys的方法
* @param menu
* @returns key数组
*/
const handleGetRootSubmenuKeys = (menu: API.MenuData): React.Key[] => {
const keys: React.Key[] = [];
menu.forEach((item: API.MenuItem) => {
if (item.children) {
keys.push(item.key);
}
});
return keys;
};
export default handleGetRootSubmenuKeys;

View File

@ -0,0 +1,32 @@
/**
*
* @param data
* @param datumCb
* @returns
*/
const handleRecursiveNestedData = (
data: API.MenuItem[],
datumCb: (datum: API.MenuItem) => API.MenuItem,
): API.MenuItem[] => {
const res = [] as API.MenuItem[];
for (const datum of data) {
if (datum.hideInMenu) {
continue;
}
if (datum.children) {
res.push({
...datum,
children: handleRecursiveNestedData(datum.children, datumCb),
});
} else {
res.push(datumCb(datum));
}
}
return res;
};
export default handleRecursiveNestedData;

110
src/utils/handleRedirect.ts Normal file
View File

@ -0,0 +1,110 @@
import { history } from 'umi';
/**
*
* @description , url上有redirect参数则跳转到redirect参数, redirect的有效性,
* ; , , ,
* , , , Menu中可展开的形式,
* , umi处理404的情况
* @param isLoginPage
* @param indexAllMenuItemByPath IndexAllMenuItemByKey<'path'>
* @param indexValidMenuItemByPath IndexValidMenuItemByPath
* @returns Promise<boolean>
*/
const handleRedirect = (
isLoginPage: boolean,
indexAllMenuItemByPath: IndexAllMenuItemByKey<'path'>,
indexValidMenuItemByPath: IndexValidMenuItemByPath,
): Promise<boolean> => (
new Promise((resolve) => {
let routePath = ''
const queryString = window.location.search;
console.log('queryString', queryString);
let {
location: { search, pathname },
} = window;
if (pathname.indexOf('/cloudMenu') !== -1) {
pathname = pathname.split('/cloudMenu')[1]
}
routePath = `${pathname}${search}`;
console.log('routePath', routePath);
// Object.keys(indexValidMenuItemByPath)[0];
// if (isLoginPage) {
// const queryString = window.location.search;
// if (queryString) {
// const matchedRes = queryString.match(/redirect=(.*)/);
// if (matchedRes) {
// //还要考虑redirect参数是否有效
// let decodeRedirect = decodeURIComponent(matchedRes[1]);
// if (decodeRedirect.indexOf('/cloudMenu') !== -1) {
// decodeRedirect = decodeRedirect.split('/cloudMenu')[1]
// }
// // 处理可能存在的路径问题,确保路径格式正确
// // 移除可能的重复前缀
// if (decodeRedirect.startsWith('/') && decodeRedirect.indexOf('/', 1) !== -1) {
// const firstSlashAfterRoot = decodeRedirect.indexOf('/', 1);
// const possiblePrefix = decodeRedirect.substring(0, firstSlashAfterRoot);
// // 检查是否有重复的路径前缀
// if (decodeRedirect.indexOf(possiblePrefix, firstSlashAfterRoot) === firstSlashAfterRoot) {
// decodeRedirect = decodeRedirect.substring(firstSlashAfterRoot);
// }
// }
// // 避免重定向到登录页面本身
// if (decodeRedirect === '/user/login' || decodeRedirect.startsWith('/user/login?')) {
// // 如果重定向目标是登录页面,则使用默认路由
// routePath = Object.keys(indexValidMenuItemByPath)[0];
// }
// //有效: 跳转
// else if (indexValidMenuItemByPath[decodeRedirect]) {
// routePath = decodeRedirect;
// } else if (indexAllMenuItemByPath[decodeRedirect]) {
// //无效
// //有子路由: 跳子路由
// routePath = indexAllMenuItemByPath[decodeRedirect].redirect;
// } else {
// //无子路由: 还是要跳, 此时就是交由umi处理404的情况了
// if (decodeRedirect === '/cloudMenu/') {
// routePath = ''
// } else {
// routePath = decodeRedirect;
// }
// }
// }
// }
// } else {
// let {
// location: { search, pathname },
// } = window;
// if (pathname.indexOf('/cloudMenu') !== -1) {
// pathname = pathname.split('/cloudMenu')[1]
// }
// //考虑url上有查询字符串参数的情况
// //有效
// if (indexValidMenuItemByPath[pathname]) {
// routePath = `${pathname}${search}`;
// } else if (indexAllMenuItemByPath[pathname]) {
// //无效
// //有子路由: 跳子路由
// routePath = `${indexAllMenuItemByPath[pathname].redirect}${search}`;
// } else {
// //无子路由: 也跳, 此时交由umi处理404的情况
// routePath = `${pathname}${search}`;
// }
// }
history.push(routePath);
return resolve(true);
})
);
export default handleRedirect;

View File

@ -0,0 +1,37 @@
// 封装的一些公共方法
// 保留两位小数 不满两位小数的用0补齐 有千分号
export const handleFormatNumber = (num: any) => {
// 确保 num 是数字类型
const parsedNum = Number(num);
// 如果转换后仍然是 NaN返回原始值
if (isNaN(parsedNum)) return "NaN";
// 如果输入为 0直接返回 '0.00'
if (parsedNum === 0) return "0.00";
// 先处理千分号格式,保留两位小数
let [integer, decimal] = parsedNum.toFixed(2).split("."); // 保留两位小数
// 处理千分号
integer = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
// 返回格式化后的数字
return `${integer}.${decimal}`;
};
// base64转为文件格式
export const base64ToFile = (base64Data: any, filename = "qrcode.png") => {
const arr = base64Data.split(",");
const mime = arr[0].match(/:(.*?);/)[1]; // 提取 MIME 类型
const bstr = atob(arr[1]); // 解码 Base64
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
}

158
src/utils/request.ts Normal file
View File

@ -0,0 +1,158 @@
import axios from 'axios';
import { getDvaApp } from 'umi';
import { notification } from 'antd';
import type { AxiosRequestHeaders } from 'axios/index';
import CryptoJS from "crypto-js";
const { UMI_APP_BASEURL } = process.env;
// const instance = axios.create({ baseURL: UMI_APP_BASEURL });
// const instance = axios.create({ baseURL: 'https://api.eshangtech.com/EShangApiMain' });
// const instance = axios.create({ baseURL: 'http://home.robot-z.cn:7001/' });
// 修改baseURL为完整的API地址确保在生产环境中正确访问
const instance = axios.create({ baseURL: 'https://es.eshangtech.com' });
instance.interceptors.request.use(
(config) => {
// 对data数据进行加密
// if (config.data) {
// config.data = preprocessData(JSON.stringify(config.data)); // 调用预处理函数
// }
console.log('config', config);
const isUpload = config.url?.includes("/oss/upload");
config.headers = {
...config.headers,
Authorization: `Bearer ${localStorage.getItem('Authorization') || ''}`,
"Content-Type": isUpload ? "multipart/form-data" : "application/json;charset=utf-8",
} as AxiosRequestHeaders;
return config;
},
(error) => Promise.reject(error),
);
instance.interceptors.response.use(
//状态码为2xx的时候执行
(response) => {
const { data } = response;
if (data.code !== 200) {
notification.error({
message: data.message,
});
}
const timestamp = getFormattedDate()
return data
},
//状态码不是2xx的时候执行
(error) => {
const { response } = error;
if (response && response.status === 401) {
// // 清除本地存储的token
// localStorage.removeItem('Authorization');
// // 重定向到登录页
// window.location.href = '/user/login';
// notification.error({
// message: response?.data?.message || '请求失败',
// description: error.message
// });
} else {
notification.error({
message: response?.data?.message || '请求失败',
description: error.message
});
}
return Promise.reject({
code: response?.status || 500,
message: response?.data?.message || '请求失败'
});
},
);
// 加密
const encryptAESECB = (data: string, key: string) => {
// const cipher = CryptoJS.createCipheriv('aes-128-ecb', key, null); // ECB 模式不需要 IV
const newKey = CryptoJS.enc.Utf8.parse(key); // 密钥必须是 16 字节
const cipher = CryptoJS.AES.encrypt(data, newKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
let encrypted = cipher.ciphertext.toString(CryptoJS.enc.Hex);
// let encrypted = cipher.update(data, 'utf8', 'hex');
// encrypted += cipher.final('hex');
return encrypted;
}
// 解密
const decryptAESECB = (data: string, key: string) => {
// const decipher = CryptoJS.createDecipheriv('aes-128-ecb', key, null);
// let decrypted = decipher.update(data, 'hex', 'utf8');
// decrypted += decipher.final('utf8');
const newKey = CryptoJS.enc.Utf8.parse(key);
const encryptedData = CryptoJS.enc.Hex.parse(data);
// 解密操作
const decrypted = CryptoJS.AES.decrypt({ ciphertext: encryptedData }, newKey, {
mode: CryptoJS.mode.ECB, // ECB 模式
padding: CryptoJS.pad.Pkcs7 // PKCS7 填充方式
});
// 将解密后的结果转为 UTF-8 字符串
const decryptedText = decrypted.toString(CryptoJS.enc.Utf8);
return decryptedText;
}
// md5 签名
const md5 = (key: string, data: string, timestamp: string) => {
const text = "s" + key + data + timestamp;
return CryptoJS.MD5(text).toString(CryptoJS.enc.Hex);
}
// 生成签名戳
const getFormattedDate = () => {
const date = new Date();
const year = date.getFullYear(); // 获取年份 (yyyy)
const month = String(date.getMonth() + 1).padStart(2, '0'); // 获取月份 (MM)
const day = String(date.getDate()).padStart(2, '0'); // 获取日期 (dd)
const hours = String(date.getHours()).padStart(2, '0'); // 获取小时 (HH)
return `es0${year}${month}${day}${hours}0es`; // 拼接成 yyyyMMddHH 格式
}
// 加密方法
const preprocessData = (data: string) => {
console.log('data', data);
// YYYYMMDD
let timestamp = getFormattedDate()
console.log('timestamp', timestamp);
// 秒为单位的时间戳
let timeSecond = parseInt((new Date().getTime() / 1000).toString())
console.log('timeSecond', timeSecond);
// 数据的加密
let encryptionData = encryptAESECB(data, timestamp)
console.log('encryptionData', encryptionData);
// md5签名方法
let md5Data = md5(timestamp, encryptionData, timestamp)
console.log('md5Data', md5Data);
let res = {
data: encryptionData,
timestamp: timeSecond,
sign: md5Data
}
console.log('res', res);
return res
}
export default instance;

158
src/utils/requestMap.ts Normal file
View File

@ -0,0 +1,158 @@
import axios from 'axios';
import { getDvaApp } from 'umi';
import { notification } from 'antd';
import type { AxiosRequestHeaders } from 'axios/index';
import CryptoJS from "crypto-js";
const { UMI_APP_BASEURL } = process.env;
// const instance = axios.create({ baseURL: UMI_APP_BASEURL });
// const instance = axios.create({ baseURL: 'https://api.eshangtech.com/EShangApiMain' });
// const instance = axios.create({ baseURL: 'http://home.robot-z.cn:7001/' });
// 修改baseURL为完整的API地址确保在生产环境中正确访问
const instance = axios.create({ baseURL: 'https://mp.eshangtech.com/Coop.Merchant/Handler/' });
instance.interceptors.request.use(
(config) => {
// 对data数据进行加密
// if (config.data) {
// config.data = preprocessData(JSON.stringify(config.data)); // 调用预处理函数
// }
console.log('config', config);
const isUpload = config.url?.includes("/oss/upload");
config.headers = {
...config.headers,
Authorization: `Bearer ${localStorage.getItem('Authorization') || ''}`,
"Content-Type": "text/plain; charset=utf-8",
} as AxiosRequestHeaders;
return config;
},
(error) => Promise.reject(error),
);
instance.interceptors.response.use(
//状态码为2xx的时候执行
(response) => {
const { data } = response;
if (data.code !== 200) {
// notification.error({
// message: data.message,
// });
}
const timestamp = getFormattedDate()
return data
},
//状态码不是2xx的时候执行
(error) => {
const { response } = error;
if (response && response.status === 401) {
// // 清除本地存储的token
// localStorage.removeItem('Authorization');
// // 重定向到登录页
// window.location.href = '/user/login';
// notification.error({
// message: response?.data?.message || '请求失败',
// description: error.message
// });
} else {
// notification.error({
// message: response?.data?.message || '请求失败',
// description: error.message
// });
}
return Promise.reject({
code: response?.status || 500,
message: response?.data?.message || '请求失败'
});
},
);
// 加密
const encryptAESECB = (data: string, key: string) => {
// const cipher = CryptoJS.createCipheriv('aes-128-ecb', key, null); // ECB 模式不需要 IV
const newKey = CryptoJS.enc.Utf8.parse(key); // 密钥必须是 16 字节
const cipher = CryptoJS.AES.encrypt(data, newKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
let encrypted = cipher.ciphertext.toString(CryptoJS.enc.Hex);
// let encrypted = cipher.update(data, 'utf8', 'hex');
// encrypted += cipher.final('hex');
return encrypted;
}
// 解密
const decryptAESECB = (data: string, key: string) => {
// const decipher = CryptoJS.createDecipheriv('aes-128-ecb', key, null);
// let decrypted = decipher.update(data, 'hex', 'utf8');
// decrypted += decipher.final('utf8');
const newKey = CryptoJS.enc.Utf8.parse(key);
const encryptedData = CryptoJS.enc.Hex.parse(data);
// 解密操作
const decrypted = CryptoJS.AES.decrypt({ ciphertext: encryptedData }, newKey, {
mode: CryptoJS.mode.ECB, // ECB 模式
padding: CryptoJS.pad.Pkcs7 // PKCS7 填充方式
});
// 将解密后的结果转为 UTF-8 字符串
const decryptedText = decrypted.toString(CryptoJS.enc.Utf8);
return decryptedText;
}
// md5 签名
const md5 = (key: string, data: string, timestamp: string) => {
const text = "s" + key + data + timestamp;
return CryptoJS.MD5(text).toString(CryptoJS.enc.Hex);
}
// 生成签名戳
const getFormattedDate = () => {
const date = new Date();
const year = date.getFullYear(); // 获取年份 (yyyy)
const month = String(date.getMonth() + 1).padStart(2, '0'); // 获取月份 (MM)
const day = String(date.getDate()).padStart(2, '0'); // 获取日期 (dd)
const hours = String(date.getHours()).padStart(2, '0'); // 获取小时 (HH)
return `es0${year}${month}${day}${hours}0es`; // 拼接成 yyyyMMddHH 格式
}
// 加密方法
const preprocessData = (data: string) => {
console.log('data', data);
// YYYYMMDD
let timestamp = getFormattedDate()
console.log('timestamp', timestamp);
// 秒为单位的时间戳
let timeSecond = parseInt((new Date().getTime() / 1000).toString())
console.log('timeSecond', timeSecond);
// 数据的加密
let encryptionData = encryptAESECB(data, timestamp)
console.log('encryptionData', encryptionData);
// md5签名方法
let md5Data = md5(timestamp, encryptionData, timestamp)
console.log('md5Data', md5Data);
let res = {
data: encryptionData,
timestamp: timeSecond,
sign: md5Data
}
console.log('res', res);
return res
}
export default instance;

151
src/utils/requestOld.ts Normal file
View File

@ -0,0 +1,151 @@
import axios from 'axios';
import { getDvaApp } from 'umi';
import { notification } from 'antd';
import type { AxiosRequestHeaders } from 'axios/index';
import CryptoJS from "crypto-js";
const { UMI_APP_BASEURL } = process.env;
// const instance = axios.create({ baseURL: UMI_APP_BASEURL });
const instance = axios.create({ baseURL: 'https://api.eshangtech.com/EShangApiMain' });
// const instance = axios.create({ baseURL: '/auth' });
instance.interceptors.request.use(
(config) => {
// 对data数据进行加密
// if (config.data) {
// config.data = preprocessData(JSON.stringify(config.data)); // 调用预处理函数
// }
config.headers = {
...config.headers,
Authorization: `Bearer ${localStorage.getItem('Authorization') || ''}`,
"Content-Type": "application/json;charset=utf-8"
} as AxiosRequestHeaders;
return config;
},
(error) => Promise.reject(error),
);
instance.interceptors.response.use(
//状态码为2xx的时候执行
(response) => {
const { data } = response;
if (data.code !== 200 && data.Result_Code !== 100) {
notification.error({
message: data.message,
});
}
const timestamp = getFormattedDate()
return data
},
//状态码不是2xx的时候执行
(error) => {
const { response } = error;
if (response && response.status === 401) {
// // 清除本地存储的token
// localStorage.removeItem('Authorization');
// // 重定向到登录页
// window.location.href = '/user/login';
// notification.error({
// message: response?.data?.message || '请求失败',
// description: error.message
// });
} else {
notification.error({
message: response?.data?.message || '请求失败',
description: error.message
});
}
return Promise.reject({
code: response?.status || 500,
message: response?.data?.message || '请求失败'
});
},
);
// 加密
const encryptAESECB = (data: string, key: string) => {
// const cipher = CryptoJS.createCipheriv('aes-128-ecb', key, null); // ECB 模式不需要 IV
const newKey = CryptoJS.enc.Utf8.parse(key); // 密钥必须是 16 字节
const cipher = CryptoJS.AES.encrypt(data, newKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
let encrypted = cipher.ciphertext.toString(CryptoJS.enc.Hex);
// let encrypted = cipher.update(data, 'utf8', 'hex');
// encrypted += cipher.final('hex');
return encrypted;
}
// 解密
const decryptAESECB = (data: string, key: string) => {
// const decipher = CryptoJS.createDecipheriv('aes-128-ecb', key, null);
// let decrypted = decipher.update(data, 'hex', 'utf8');
// decrypted += decipher.final('utf8');
const newKey = CryptoJS.enc.Utf8.parse(key);
const encryptedData = CryptoJS.enc.Hex.parse(data);
// 解密操作
const decrypted = CryptoJS.AES.decrypt({ ciphertext: encryptedData }, newKey, {
mode: CryptoJS.mode.ECB, // ECB 模式
padding: CryptoJS.pad.Pkcs7 // PKCS7 填充方式
});
// 将解密后的结果转为 UTF-8 字符串
const decryptedText = decrypted.toString(CryptoJS.enc.Utf8);
return decryptedText;
}
// md5 签名
const md5 = (key: string, data: string, timestamp: string) => {
const text = "s" + key + data + timestamp;
return CryptoJS.MD5(text).toString(CryptoJS.enc.Hex);
}
// 生成签名戳
const getFormattedDate = () => {
const date = new Date();
const year = date.getFullYear(); // 获取年份 (yyyy)
const month = String(date.getMonth() + 1).padStart(2, '0'); // 获取月份 (MM)
const day = String(date.getDate()).padStart(2, '0'); // 获取日期 (dd)
const hours = String(date.getHours()).padStart(2, '0'); // 获取小时 (HH)
return `es0${year}${month}${day}${hours}0es`; // 拼接成 yyyyMMddHH 格式
}
// 加密方法
const preprocessData = (data: string) => {
console.log('data', data);
// YYYYMMDD
let timestamp = getFormattedDate()
console.log('timestamp', timestamp);
// 秒为单位的时间戳
let timeSecond = parseInt((new Date().getTime() / 1000).toString())
console.log('timeSecond', timeSecond);
// 数据的加密
let encryptionData = encryptAESECB(data, timestamp)
console.log('encryptionData', encryptionData);
// md5签名方法
let md5Data = md5(timestamp, encryptionData, timestamp)
console.log('md5Data', md5Data);
let res = {
data: encryptionData,
timestamp: timeSecond,
sign: md5Data
}
console.log('res', res);
return res
}
export default instance;

39
src/utils/session.ts Normal file
View File

@ -0,0 +1,39 @@
const $strorage= window.sessionStorage || sessionStorage
const session = {
get: (key: string) => {
const value = $strorage.getItem(key)
try {
const valueObj = JSON.parse(value);
return valueObj;
} catch (error) {
return value
}
},
set: (key: string, value: any) => {
return $strorage.setItem(key, value ? JSON.stringify(value) : value)
},
remove: (key: string) => {
return $strorage.removeItem(key)
},
clearExcept: (key: string) => {
for (let i = 0; i < $strorage.length; i+=1) {
const itemKey: string | undefined = $strorage.key(i);
if (itemKey && itemKey !== key) {
$strorage.rmoveItem(itemKey);
}
}
},
clearAll: () => {
$strorage.clear()
}
}
export default session;

42
tsconfig.json Normal file
View File

@ -0,0 +1,42 @@
{
"extends": "./src/.umi/tsconfig.json",
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "esnext",
"lib": ["esnext", "dom"],
"sourceMap": true,
"baseUrl": ".",
"jsx": "react-jsx",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"allowJs": true,
"skipLibCheck": true,
"experimentalDecorators": true,
"strict": true,
"paths": {
"@/*": ["src/*"]
}
},
"include": [
"mock/**/*",
"src/**/*",
"playwright.config.ts",
"tests/**/*",
"test/**/*",
"__test__/**/*",
"typings/**/*",
"config/**/*",
".eslintrc.js",
".stylelintrc.js",
".prettierrc.js",
"jest.config.js",
"mock/*",
"typings.d.ts"
],
"exclude": ["node_modules", "build", "dist", "scripts", "src/.umi/*", "webpack", "jest"]
}

162
typings.d.ts vendored Normal file
View File

@ -0,0 +1,162 @@
// import 'umi/typings';
/// reference path="./node_modules/@types/react/index.d.ts" />
declare module '*.less';
/**
*
* @description key是路由path, value是权限数组
*/
type PageAuthority = {
[path: string]: string[];
}
/**
*
* @description key是权限名称, value是具体的权限字符串
*/
type Authority = {
[key: string]: string;
}
/**
* path索引有效菜单项的映射(, url pathname的菜单项)
* @description key是path, value是API.MenuItem
*/
type IndexValidMenuItemByPath = {
[path: string]: API.MenuItem;
}
/**
* API.MenuItem的key索引所有菜单项的映射(, url pathname的菜单项)
* @description key是API.MenuItem的key, value是API.MenuItem
*/
type IndexAllMenuItemByKey<T extends keyof API.MenuItem> = T extends keyof API.MenuItem
? {
[key in API.MenuItem[T]]: API.MenuItem
}
: never;
declare namespace API {
/**
*
* @description data
* @description code 返回码: 0 ,
* @description message 消息: 返回的消息
*/
type ResponstBody<T> = {
data: T;
code: number;
message: string;
};
/**
*
* @description token token票据
*/
type LoginData = {
token: string;
};
/** 登录响应结果 */
type LoginResponse = ResponstBody<LoginData>;
/** 用户信息数据 */
type UserInfo = {
name?: string;
avatar?: string;
userid?: string;
email?: string;
signature?: string;
title?: string;
group?: string;
tags?: { key?: string; label?: string }[];
notifyCount?: number;
unreadCount?: number;
country?: string;
access?: string;
geographic?: {
province?: { label?: string; key?: string };
city?: { label?: string; key?: string };
};
address?: string;
phone?: string;
};
/** 用户信息响应结果 */
type UserInfoResponse = ResponstBody<UserInfo>;
/**
*
* @description authority ()
*/
type UserAuthorityData = {
authority: string[];
}
/** 用户权限响应结果 */
type UserAuthorityResponse = ResponstBody<UserAuthorityData>;
/**
*
* @description id id
* @description pid id(id)
* @description key , 使string类型代替React.Key:
* https://ant.design/components/menu-cn#itemtype, 不然会出现key类型不对导致的菜单项无法被选中的问题
* @description lable
* @description hideInMenu
* @description path ,
* children的菜单都会有这个字段, children的菜单跳转这个值, children的跳redirect,
* children表示这个菜单是可展开的, children的path只是表示它的一个位置,
* @description redirect ,
* children的菜单有, children中有可选中的菜单时, path,
* children中没可以选中的菜单, children时,
* children中的children的第一个可选中的菜单的path,
* , , mock数据中的菜单数据
* , , ,
*
* @description children
*/
type MenuItem = {
id: number;
pid?: number;
key: string;
path: string;
icon?: string;
redirect: string;
hideInMenu?: boolean;
label: React.ReactElement | string;
name: React.ReactElement | string;
children?: MenuItem[];
}
/** 菜单数据 */
type MenuData = MenuItem[];
/** 菜单数据响应结果 */
type MenuDataResponse = ResponstBody<MenuData>;
/** 用户信息权限菜单响应结果 */
type UserInfoAuthMenuResponse = (UserInfoResponse | UserAuthorityResponse | MenuDataResponse)[];
/** 用户权限菜单响应结果 */
type UserAuthMenuResponse = (UserAuthorityResponse | MenuDataResponse)[];
/** 登出响应结果 */
type LogoutResponse = ResponstBody<Record<string, never>>;
/** 登录参数 */
type LoginParams = {
username?: string;
password?: string;
autoLogin?: boolean;
type?: string;
};
/** 验证码数据 */
type CaptchaData = {
captcha: string;
}
/** 验证码响应结果 */
type CaptchaResponse = ResponstBody<CaptchaData>;
}

11599
yarn.lock Normal file

File diff suppressed because it is too large Load Diff