修改注册银行账户 地址以及 融合样式改动

This commit is contained in:
孙景学
2025-07-07 15:01:11 +08:00
parent e2962e09e7
commit 56da66ee21
50 changed files with 1887 additions and 105 deletions

View File

@ -66,7 +66,7 @@
"@umijs/route-utils": "1.0.37",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-react": "^1.0.6",
"antd": "4.15.1",
"antd": "latest-4",
"array-move": "3.0.1",
"axios": "0.21.1",
"classnames": "2.3.1",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="14" height="14" viewBox="0 0 14 14"><defs><clipPath id="master_svg0_51_7400/51_7034/5_5361"><rect x="0" y="0" width="14" height="14" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_51_7400/51_7034/5_5361)"><g><path d="M8.326062556686402,2.0370083067779543C8.326062556686402,2.0370083067779543,3.8005605566864014,6.6990103067779545,3.8005605566864014,6.6990103067779545C3.7186017566864016,6.780970306777954,3.6776225566864014,6.878970306777954,3.6776225566864014,6.993010306777954C3.6776225566864014,7.107050306777954,3.7186017566864016,7.209570306777954,3.8005605566864014,7.300570306777954C3.8005605566864014,7.300570306777954,8.326062556686402,11.962570306777954,8.326062556686402,11.962570306777954C8.4080225566864,12.035470306777954,8.503682556686401,12.071970306777954,8.613062556686401,12.071970306777954C8.722432556686401,12.071970306777954,8.818102556686402,12.030970306777954,8.9000625566864,11.948970306777953C8.982022556686402,11.867050306777955,9.0230025566864,11.771380306777955,9.0230025566864,11.662010306777955C9.0230025566864,11.552630306777955,8.982022556686402,11.456970306777954,8.9000625566864,11.375010306777954C8.9000625566864,11.375010306777954,4.634435556686402,7.000010306777954,4.634435556686402,7.000010306777954C4.634435556686402,7.000010306777954,8.9000625566864,2.6250073067779542,8.9000625566864,2.6250073067779542C8.982022556686402,2.543049306777954,9.0230025566864,2.4473823067779543,9.0230025566864,2.3380073067779543C9.0230025566864,2.2286323067779543,8.982022556686402,2.1329663067779543,8.9000625566864,2.051008306777954C8.818102556686402,1.969049506777954,8.722432556686401,1.928070306777954,8.613062556686401,1.928070306777954C8.503682556686401,1.928070306777954,8.4080225566864,1.9645286067779542,8.326062556686402,2.037445306777954C8.326062556686402,2.037445306777954,8.326062556686402,2.0370083067779543,8.326062556686402,2.0370083067779543C8.326062556686402,2.0370083067779543,8.326062556686402,2.0370083067779543,8.326062556686402,2.0370083067779543Z" fill="#606266" fill-opacity="1"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="14" height="14" viewBox="0 0 14 14"><defs><clipPath id="master_svg0_51_7400/51_7030/5_5361"><rect x="0" y="0" width="14" height="14" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_51_7400/51_7030/5_5361)"><g><path d="M4.6619995,2.0369868065948484C4.5800417,2.1282788065948486,4.5390625,2.2286118065948486,4.5390625,2.3379868065948486C4.5390625,2.4473618065948486,4.5800417,2.5430288065948488,4.6619995,2.6249868065948485C4.6619995,2.6249868065948485,8.9276225,6.999984806594848,8.9276225,6.999984806594848C8.9276225,6.999984806594848,4.6619995,11.37498480659485,4.6619995,11.37498480659485C4.5800417,11.456974806594848,4.5390625,11.552614806594848,4.5390625,11.661984806594848C4.5390625,11.771364806594848,4.5800417,11.86700480659485,4.6619995,11.94897480659485C4.7439585,12.030974806594848,4.8396255,12.071874806594849,4.9490005,12.071874806594849C5.0583755,12.071874806594849,5.1540425,12.035474806594848,5.2360005,11.962574806594848C5.2360005,11.962574806594848,9.761502499999999,7.300544806594849,9.761502499999999,7.300544806594849C9.843462500000001,7.218584806594849,9.884442499999999,7.118264806594849,9.884442499999999,6.999544806594849C9.884442499999999,6.880834806594849,9.843462500000001,6.780514806594849,9.761502499999999,6.698554806594848C9.761502499999999,6.698554806594848,5.2360005,2.0365498065948486,5.2360005,2.0365498065948486C5.1540425,1.9636332065948485,5.0583755,1.9271748065948486,4.9490005,1.9271748065948486C4.8396255,1.9271748065948486,4.7439585,1.9636332065948485,4.6619995,2.0365498065948486C4.6619995,2.0365498065948486,4.6619995,2.0369868065948484,4.6619995,2.0369868065948484C4.6619995,2.0369868065948484,4.6619995,2.0369868065948484,4.6619995,2.0369868065948484Z" fill="#606266" fill-opacity="1"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/assets/divider.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

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

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="12" height="13.843500137329102" viewBox="0 0 12 13.843500137329102"><g><path d="M1,5.3125C1,5.3125,1,12.844,1,12.844C1,12.844,11,12.844,11,12.844C11,12.844,11,5.3125,11,5.3125C11,5.3125,6,1.1405,6,1.1405C6,1.1405,1,5.3125,1,5.3125C1,5.3125,1,5.3125,1,5.3125ZM0.1875,4.6875C0.1875,4.6875,5.6875,0.1095,5.6875,0.1095C5.78117,0.0365001,5.88533,0,6,0C6.11467,0,6.21883,0.0365001,6.3125,0.1095C6.3125,0.1095,11.8125,4.6875,11.8125,4.6875C11.9375,4.79183,12,4.922,12,5.078C12,5.078,12,13.3435,12,13.3435C12,13.4895,11.9532,13.6093,11.8595,13.703C11.7658,13.7967,11.646,13.8435,11.5,13.8435C11.5,13.8435,0.5,13.8435,0.5,13.8435C0.354,13.8435,0.234167,13.7967,0.1405,13.703C0.0468334,13.6093,-2.22045e-16,13.4895,0,13.3435C0,13.3435,0,5.078,0,5.078C0,4.92167,0.0625,4.7915,0.1875,4.6875C0.1875,4.6875,0.1875,4.6875,0.1875,4.6875Z" fill="#08080A" fill-opacity="0.9599999785423279"/></g></svg>

After

Width:  |  Height:  |  Size: 999 B

1
src/assets/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

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

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="12.989924430847168" height="14.000499725341797" viewBox="0 0 12.989924430847168 14.000499725341797"><g><path d="M3.99396,1.5C3.99396,1.5,3.99396,2.594,3.99396,2.594C2.84796,3.20867,2.0173,4.0785,1.50196,5.2035C0.986629,6.3285,0.869462,7.52117,1.15046,8.7815C1.47346,10.0418,2.1193,11.0548,3.08796,11.8205C4.05663,12.5862,5.19213,12.9793,6.49446,13C7.79646,12.979,8.93196,12.5858,9.90096,11.8205C10.87,11.0552,11.5158,10.0422,11.8385,8.7815C12.1198,7.52117,12.0026,6.3285,11.487,5.2035C10.9713,4.0785,10.1406,3.20867,8.99496,2.594C8.99496,2.594,8.99496,1.5,8.99496,1.5C10.443,2.13533,11.5211,3.11717,12.2295,4.4455C12.9378,5.77383,13.1513,7.214,12.87,8.766C12.5263,10.318,11.7736,11.5707,10.612,12.524C9.4503,13.4773,8.07796,13.9695,6.49496,14.0005C4.91163,13.9692,3.53663,13.477,2.36996,12.524C1.2033,11.571,0.453296,10.3237,0.119962,8.782C-0.161371,7.21933,0.0521289,5.774,0.760462,4.446C1.4688,3.118,2.54696,2.13617,3.99496,1.5005C3.99496,1.5005,3.99396,1.5,3.99396,1.5C3.99396,1.5,3.99396,1.5,3.99396,1.5ZM6.49396,0C6.8273,0,6.99396,0.166667,6.99396,0.5C6.99396,0.5,6.99396,5.5,6.99396,5.5C6.99396,5.83333,6.8273,6,6.49396,6C6.16063,6,5.99396,5.83333,5.99396,5.5C5.99396,5.5,5.99396,0.5,5.99396,0.5C5.99396,0.166667,6.16063,0,6.49396,0C6.49396,0,6.49396,0,6.49396,0Z" fill="#000000" fill-opacity="1"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
src/assets/user.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="1750316870480" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2449" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M512 74.666667C270.933333 74.666667 74.666667 270.933333 74.666667 512S270.933333 949.333333 512 949.333333 949.333333 753.066667 949.333333 512 753.066667 74.666667 512 74.666667zM288 810.666667c0-123.733333 100.266667-224 224-224S736 686.933333 736 810.666667c-61.866667 46.933333-140.8 74.666667-224 74.666666s-162.133333-27.733333-224-74.666666z m128-384c0-53.333333 42.666667-96 96-96s96 42.666667 96 96-42.666667 96-96 96-96-42.666667-96-96z m377.6 328.533333c-19.2-96-85.333333-174.933333-174.933333-211.2 32-29.866667 51.2-70.4 51.2-117.333333 0-87.466667-72.533333-160-160-160s-160 72.533333-160 160c0 46.933333 19.2 87.466667 51.2 117.333333-89.6 36.266667-155.733333 115.2-174.933334 211.2-55.466667-66.133333-91.733333-149.333333-91.733333-243.2 0-204.8 168.533333-373.333333 373.333333-373.333333S885.333333 307.2 885.333333 512c0 93.866667-34.133333 177.066667-91.733333 243.2z" fill="#666666" p-id="2450"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -5,6 +5,13 @@
@main-text-color-2: #666;
@main-danger-color: rgb(214, 0, 14);
@layout-background: #666;
@layout-header-background: #666;
@menu-list-bg: #666;
@table-selected-row-bg: #666;
@text-color-secondary: #666;
@menu-item-selected-bg: #666;
//灰色
@gray: rgb(240, 240, 240);
@ -82,3 +89,377 @@
}
}
}
// .ant-layout-header {
// position: fixed;
// top: 0;
// left: 0;
// background-color: @layout-header-background;
// height: 80px;
// color: @primary-color;
// .ant-pro-global-header{
// background-color: #fff;
// height: 56px;
// color: #333;
// }
// }
// .ant-pro-sider-logo {
// height: 80px;
// }
// .ant-pro-sider-light .ant-pro-sider-logo h1 {
// color: #ffffff;
// }
// .ant-menu {
// margin-top: 6px;
// padding-top: 6px !important
// }
// #root .ant-layout-sider {
// z-index: 0;
// }
// // 通用页面布局样式
// .layout-container {
// background-color: @layout-background;
// min-height: 100vh;
// }
// #root {
// .ant-table-tbody > tr.ant-table-row:hover > td,
// .ant-table-tbody > tr > td.ant-table-cell-row-hover {
// background-color: @table-selected-row-bg;
// }
// .ant-table {
// color: @text-color-secondary;
// }
// }
// // 控制左侧菜单背景色
// .ant-menu.ant-menu-sub.ant-menu-inline {
// background-color: @menu-list-bg;
// .ant-menu-item-selected {
// background-color: @menu-item-selected-bg;
// }
// .ant-menu-item-selected a {
// color: @primary-color;
// font-weight: bold;
// }
// }
// // 修改全局分页器样式
// .pagination-container {
// align-items: center;
// .ant-pagination-total-text {
// margin-right: 16px;
// height: 24px;
// line-height: 24px;
// }
// .ant-pagination-prev,
// .ant-pagination-next,
// .ant-pagination-item {
// min-width: 24px;
// height: 24px;
// margin: 0;
// line-height: 24px;
// }
// // .ant-pagination-item {
// // border: none;
// // }
// // .ant-pagination-item-active {
// // color: @primary-color;
// // }
// .ant-pagination-options {
// .ant-pagination-options-quick-jumper {
// margin-left: 16px;
// }
// }
// }
// // .ant-menu-submenu-title {
// // padding-left: 24px !important;
// // }
// // .ant-menu-submenu-arrow {
// // right: 24px;
// // }
// .ant-pro-basicLayout-content {
// margin-top: 97px !important;
// margin-left: 26px !important;
// margin-bottom: 0px !important;
// // padding-left: 224px;
// // display: flex;
// flex: auto;
// flex-direction: row;
// // // 去除滚动条样式
// // margin-right: 0 !important;
// // padding-right: 10px;
// // height: 100%;
// // overflow: auto;
// }
// .bidContent {
// flex: 1;
// }
// .ant-card-head-title {
// padding: 11px 0 10px 0;
// }
// .ant-input {
// border-radius: 3px;
// }
// .ant-form-item {
// margin-bottom: 8px;
// }
// // .ant-tabs-content-holder {
// // margin-top: 24px;
// // }
// .ant-btn-dangerous.ant-btn-link {
// color: #1890ff;
// }
// .ant-table-pagination.ant-pagination {
// margin: 16px 0 24px 0;
// }
// .ant-pagination-item {
// border: 1px solid #999;
// margin: 0 4px;
// }
// .ant-pagination-item-active {
// font-weight: 500;
// background: #014f8f;
// border-color: #fff;
// }
// .ant-page-header .ant-page-header-heading-title {
// font-size: 16px;
// font-weight: normal;
// color: #333;
// }
// .ant-page-header-heading-sub-title {
// color: #333;
// font-size: 16px;
// }
// .first-title {
// font-size: 14px;
// color: @primary-color;
// line-height: 40px;
// font-weight: 500;
// position: relative;
// padding-left: 11px;
// background-color: @primary-color
// }
// .first-title::before{
// content: "";
// display: block;
// position: absolute;
// top: 13px;
// left: 0;
// width: 3px;
// height: 14px;
// background-color: #014f8f;
// }
// .scd-title {
// font-size: 14px;
// color: #333;
// line-height: 42px;
// font-weight: 500;
// position: relative;
// padding-left: 34px;
// }
// .scd-title::before{
// content: "";
// display: block;
// position: absolute;
// top: 20px;
// left: 0;
// width: 24px;
// height: 2px;
// background-color: @primary-color;
// }
// .ant-modal-footer {
// overflow: hidden;
// button {
// float: right;
// margin-left: 8px;
// }
// }
// .MuiTabs-indicator {
// background-color: #ffffff !important;
// }
// .ant-pro-table td.ant-table-cell > a {
// color: #1890ff;
// padding-right: 8px;
// }
// .ant-pro-table-search {
// }
// .ant-pro-table-search {
// padding: 10px 24px 6px 24px;
// margin-bottom: 8px;
// }
// .ant-pagination-item-active:focus a, .ant-pagination-item-active:hover a {
// color: #ffffff;
// }
// .ant-tabs-tab {
// margin: 0px 48px 0 0 !important;
// }
// .ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
// box-shadow: none;
// }
// .ant-pro-sider-light {
// height: auto;
// box-shadow: none;
// }
// .myselfContent .ant-menu-submenu-title {
// padding-right: 34px;
// text-align: center;
// color: #014f8f;
// border-top: 2px solid #014f8f;
// //background: #014f8f;
// }
// .myselfContent .ant-menu-submenu-title i {
// display: none;
// }
// .myselfContent .ant-menu-item {
// text-align: center;
// padding: 0 !important;
// }
// // .myselfContent .ant-menu .ant-menu-item-selected {
// // background-color: rgba(225,225,225,.0);
// // }
// // .myselfContent .ant-menu .ant-menu-submenu-selected {
// // background: #014f8f;
// // color: #fff !important;
// // }
// // .myselfContent .ant-menu .ant-menu-submenu-selected:hover {
// // background: #014f8f;
// // color: #fff !important;
// // }
// .myselfContent .ant-menu-inline .ant-menu-item::after {
// border-right: none;
// }
// .ant-collapse {
// border: none;
// }
// .ant-page-header {
// border-radius: 6px 6px 0 0;
// }
// .ant-table-content .ant-table-cell .ant-btn-text {
// color: @primary-color !important;
// background: rgba(0,0,0,0);
// padding-left: 4px;
// padding-right: 4px;
// }
// .ant-table-content .ant-table-cell .ant-btn-text:hover {
// color: @primary-color;
// background: none;
// }
// .frize-menu {
// width: 224px;
// height: 100%;
// overflow: hidden;
// flex: 0 0 224px;
// max-width: 224px;
// min-width: 224px;
// background: #fff;
// position: fixed;
// min-height: 100%;
// scrollbar-width: none;
// z-index: 100;
// left: 0;
// top: 56px;
// overflow-y: auto;
// border: none;
// // padding-top: 66px;
// padding-bottom: 200px;
// -ms-overflow-style: none; /* IE 10+ */
// }
// .frize-menu::-webkit-scrollbar {
// display: none;
// }
// input::-webkit-outer-spin-button,
// input::-webkit-inner-spin-button {
// -webkit-appearance: none;
// }
// input[type="number"]{
// -moz-appearance: textfield;
// }
// .basicLayout-children{
// margin-top:5px;
// background-color:white;
// border-radius:6px;
// overflow:auto;
// -ms-overflow-style: none;
// }
// .basicLayout-children::-webkit-scrollbar {
// width:0px;
// }
// // 修改全局界面上所有只读表单文字颜色改成黑色
// .ant-input[disabled]{
// color: #333;
// }
// .ant-select-disabled.ant-select:not(.ant-select-customize-input) .ant-select-selector{
// color: #333;
// }
// .ant-radio-disabled + span{
// color: #333;
// }
// .ant-btn-primary[disabled], .ant-btn-primary[disabled]:hover, .ant-btn-primary[disabled]:focus, .ant-btn-primary[disabled]:active{
// color: #333;
// }
// .ant-checkbox-disabled + span{
// color: #333;
// }
// .ant-picker-input > input[disabled]{
// color: #333;
// }
// .ant-btn[disabled], .ant-btn[disabled]:hover, .ant-btn[disabled]:focus, .ant-btn[disabled]:active{
// color: #333;
// }
// //2022.2.18增加PageHeader全局样式 来源ld_style.less
// .ant-page-header{
// background: #fff;
// padding: 6px 24px;
// border-bottom: 1px solid #ddd;
// }

View File

@ -0,0 +1,35 @@
import React from 'react';
import { Result } from 'antd';
import check from './CheckPermissions';
import type { IAuthorityType } from './CheckPermissions';
import type AuthorizedRoute from './AuthorizedRoute';
import type Secured from './Secured';
type AuthorizedProps = {
authority: IAuthorityType;
noMatch?: React.ReactNode;
};
type IAuthorizedType = React.FunctionComponent<AuthorizedProps> & {
Secured: typeof Secured;
check: typeof check;
AuthorizedRoute: typeof AuthorizedRoute;
};
const Authorized: React.FunctionComponent<AuthorizedProps> = ({
children,
authority,
noMatch = (
<Result
status="403"
title="403"
subTitle="Sorry, you are not authorized to access this page."
/>
),
}) => {
const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children;
const dom = check(authority, childrenRender, noMatch);
return <>{dom}</>;
};
export default Authorized as IAuthorizedType;

View File

@ -0,0 +1,33 @@
import { Redirect, Route } from 'umi';
import React from 'react';
import Authorized from './Authorized';
import type { IAuthorityType } from './CheckPermissions';
type AuthorizedRouteProps = {
currentAuthority: string;
component: React.ComponentClass<any, any>;
render: (props: any) => React.ReactNode;
redirectPath: string;
authority: IAuthorityType;
};
const AuthorizedRoute: React.SFC<AuthorizedRouteProps> = ({
component: Component,
render,
authority,
redirectPath,
...rest
}) => (
<Authorized
authority={authority}
noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
>
<Route
{...rest}
render={(props: any) => (Component ? <Component {...props} /> : render(props))}
/>
</Authorized>
);
export default AuthorizedRoute;

View File

@ -0,0 +1,88 @@
import React from 'react';
import { CURRENT } from './renderAuthorize';
// eslint-disable-next-line import/no-cycle
import PromiseRender from './PromiseRender';
export type IAuthorityType =
| undefined
| string
| string[]
| Promise<boolean>
| ((currentAuthority: string | string[]) => IAuthorityType);
/**
* @en-US
* General permission check method
* Common check permissions method
* @param {Permission judgment} authority
* @param {Your permission | Your permission description} currentAuthority
* @param {Passing components} target
* @param {no pass components | no pass components} Exception
* -------------------------------------------------------
* @zh-CN
* 通用权限检查方法 Common check permissions method
*
* @param { 权限判定 | Permission judgment } authority
* @param { 你的权限 | Your permission description } currentAuthority
* @param { 通过的组件 | Passing components } target
* @param { 未通过的组件 | no pass components } Exception
*/
const checkPermissions = <T, K>(
authority: IAuthorityType,
currentAuthority: string | string[],
target: T,
Exception: K,
): T | K | React.ReactNode => {
// No judgment permission. View all by default
// Retirement authority, return target;
if (!authority) {
return target;
}
// Array processing
if (Array.isArray(authority)) {
if (Array.isArray(currentAuthority)) {
if (currentAuthority.some((item) => authority.includes(item))) {
return target;
}
} else if (authority.includes(currentAuthority)) {
return target;
}
return Exception;
}
// Deal with string
if (typeof authority === 'string') {
if (Array.isArray(currentAuthority)) {
if (currentAuthority.some((item) => authority === item)) {
return target;
}
} else if (authority === currentAuthority) {
return target;
}
return Exception;
}
// Deal with promise
if (authority instanceof Promise) {
return <PromiseRender<T, K> ok={target} error={Exception} promise={authority} />;
}
// Deal with function
if (typeof authority === 'function') {
const bool = authority(currentAuthority);
// The return value after the function is executed is Promise
if (bool instanceof Promise) {
return <PromiseRender<T, K> ok={target} error={Exception} promise={bool} />;
}
if (bool) {
return target;
}
return Exception;
}
throw new Error('unsupported parameters');
};
export { checkPermissions };
function check<T, K>(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode {
return checkPermissions<T, K>(authority, CURRENT, target, Exception);
}
export default check;

View File

@ -0,0 +1,96 @@
import React from 'react';
import { Spin } from 'antd';
import isEqual from 'lodash/isEqual';
import { isComponentClass } from './Secured';
// eslint-disable-next-line import/no-cycle
type PromiseRenderProps<T, K> = {
ok: T;
error: K;
promise: Promise<boolean>;
};
type PromiseRenderState = {
component: React.ComponentClass | React.FunctionComponent;
};
export default class PromiseRender<T, K> extends React.Component<
PromiseRenderProps<T, K>,
PromiseRenderState
> {
state: PromiseRenderState = {
component: () => null,
};
componentDidMount(): void {
this.setRenderComponent(this.props);
}
shouldComponentUpdate = (
nextProps: PromiseRenderProps<T, K>,
nextState: PromiseRenderState,
): boolean => {
const { component } = this.state;
if (!isEqual(nextProps, this.props)) {
this.setRenderComponent(nextProps);
}
if (nextState.component !== component) return true;
return false;
};
// set render Component : ok or error
setRenderComponent(props: PromiseRenderProps<T, K>): void {
const ok = this.checkIsInstantiation(props.ok);
const error = this.checkIsInstantiation(props.error);
props.promise
.then(() => {
this.setState({
component: ok,
});
return true;
})
.catch(() => {
this.setState({
component: error,
});
});
}
// Determine whether the incoming component has been instantiated
// AuthorizedRoute is already instantiated
// Authorized render is already instantiated, children is no instantiated
// Secured is not instantiated
checkIsInstantiation = (
target: React.ReactNode | React.ComponentClass,
): React.FunctionComponent => {
if (isComponentClass(target)) {
const Target = target as React.ComponentClass;
return (props: any) => <Target {...props} />;
}
if (React.isValidElement(target)) {
return (props: any) => React.cloneElement(target, props);
}
return () => target as React.ReactNode & null;
};
render() {
const { component: Component } = this.state;
const { ok, error, promise, ...rest } = this.props;
return Component ? (
<Component {...rest} />
) : (
<div
style={{
width: '100%',
height: '100%',
margin: 'auto',
paddingTop: 50,
textAlign: 'center',
}}
>
<Spin size="large" />
</div>
);
}
}

View File

@ -0,0 +1,80 @@
import React from 'react';
import CheckPermissions from './CheckPermissions';
/**
* @en-US No pages can be accessed by default,default is "NULL"
* @zh-CN 默认不能访问任何页面 default is "NULL"
* */
const Exception403 = () => 403;
export const isComponentClass = (component: React.ComponentClass | React.ReactNode): boolean => {
if (!component) return false;
const proto = Object.getPrototypeOf(component);
if (proto === React.Component || proto === Function.prototype) return true;
return isComponentClass(proto);
};
// Determine whether the incoming component has been instantiated
// AuthorizedRoute is already instantiated
// Authorized render is already instantiated, children is no instantiated
// Secured is not instantiated
const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => {
if (isComponentClass(target)) {
const Target = target as React.ComponentClass;
return (props: any) => <Target {...props} />;
}
if (React.isValidElement(target)) {
return (props: any) => React.cloneElement(target, props);
}
return () => target;
};
/**
* @en-US
* Used to determine whether you have permission to access this view permission
* authority supports incoming string, () => boolean | Promise
* e.g.'user' Only user user can access
* e.g.'user,admin' user and admin can access
* e.g. ()=>boolean return true to access, return false to not access
* e.g. Promise then can be accessed, catch can not be accessed
* e.g. authority support incoming string, () => boolean | Promise
* e.g.'user' only user user can access
* e.g.'user, admin' user and admin can access
* e.g. () => boolean true to be able to visit, return false can not be accessed
* e.g. Promise then can not access the visit to catch
*-------------------------------------------------------------
* @zh-CN
* 用于判断是否拥有权限访问此 view 权限 authority 支持传入 string, () => boolean | Promise e.g. 'user' 只有 user 用户能访问
* e.g. 'user,admin' user 和 admin 都能访问 e.g. ()=>boolean 返回true能访问,返回false不能访问 e.g. Promise then 能访问
* catch不能访问 e.g. authority support incoming string, () => boolean | Promise e.g. 'user' only user
* user can access e.g. 'user, admin' user and admin can access e.g. () => boolean true to be able
* to visit, return false can not be accessed e.g. Promise then can not access the visit to catch
*
* @param {string | function | Promise} authority
* @param {ReactNode} error non-required parameter
*/
const authorize = (authority: string, error?: React.ReactNode) => {
/**
* @en-US
* conversion into a class
* Prevent the staticContext from being found to cause an error when the string is passed in
* String parameters can cause staticContext not found error
*-------------------------------------------------------------
* @zh-CN
* Conversion into a class 防止传入字符串时找不到staticContext造成报错 String parameters can cause staticContext
* not found error
*/
let classError: boolean | React.FunctionComponent = false;
if (error) {
classError = (() => error) as React.FunctionComponent;
}
if (!authority) {
throw new Error('authority is required');
}
return function decideAuthority(target: React.ComponentClass | React.ReactNode) {
const component = CheckPermissions(authority, target, classError || Exception403);
return checkIsInstantiation(component);
};
};
export default authorize;

View File

@ -0,0 +1,11 @@
import Authorized from './Authorized';
import Secured from './Secured';
import check from './CheckPermissions';
import renderAuthorize from './renderAuthorize';
Authorized.Secured = Secured;
Authorized.check = check;
const RenderAuthorize = renderAuthorize(Authorized);
export default RenderAuthorize;

View File

@ -0,0 +1,31 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable import/no-mutable-exports */
let CURRENT: string | string[] = 'NULL';
type CurrentAuthorityType = string | string[] | (() => typeof CURRENT);
/**
* Use authority or getAuthority
*
* @param {string|()=>String} currentAuthority
*/
const renderAuthorize = <T>(Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => (
currentAuthority: CurrentAuthorityType,
): T => {
if (currentAuthority) {
if (typeof currentAuthority === 'function') {
CURRENT = currentAuthority();
}
if (
Object.prototype.toString.call(currentAuthority) === '[object String]' ||
Array.isArray(currentAuthority)
) {
CURRENT = currentAuthority as string[];
}
} else {
CURRENT = 'NULL';
}
return Authorized;
};
export { CURRENT };
export default <T>(Authorized: T) => renderAuthorize<T>(Authorized);

View File

@ -16,7 +16,7 @@ interface CompanyInfoProps {
const { TabPane } = Tabs;
const CompanyInfo: React.FC<CompanyInfoProps> = (props) => {
const { viewType = false, record = '' } = props;
const { viewType = false, record = '999698' } = props;
const intl = useIntl();
// 切换tab
const [subTab, setSubTab] = useState<string>('base');

View File

@ -5,7 +5,7 @@ import request from '@/utils/request';
/**
* 供应商基本信息
*/
export const coscoSupplierBase = (id: string) => request.get(`/coscoSupplierBase/${id}`);
export const coscoSupplierBase = (id: string) => request.get(`/coscoSupplierBase/${id? id: '999698'}`);
/**
* 资质分页列表

View File

@ -0,0 +1,88 @@
import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
import { Avatar, Menu, Spin } from 'antd';
import React from 'react';
import type { ConnectProps } from 'umi';
import { history, connect } from 'umi';
import type { ConnectState } from '@/models/connect';
import type { CurrentUser } from '@/models/user';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
export type GlobalHeaderRightProps = {
currentUser?: CurrentUser;
menu?: boolean;
} & Partial<ConnectProps>;
class AvatarDropdown extends React.Component<GlobalHeaderRightProps> {
onMenuClick = (event: { key: React.Key; keyPath: React.Key[]; item: React.ReactInstance }) => {
const { key } = event;
if (key === 'logout') {
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'login/logout',
});
}
return;
}
history.push(`/account/${key}`);
};
render(): React.ReactNode {
const {
currentUser = {
avatar: '',
name: '',
},
menu,
} = this.props;
const menuHeaderDropdown = (
<Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
{menu && (
<Menu.Item key="center">
<UserOutlined />
</Menu.Item>
)}
{menu && (
<Menu.Item key="settings">
<SettingOutlined />
</Menu.Item>
)}
{menu && <Menu.Divider />}
<Menu.Item key="logout">
<LogoutOutlined />
退
</Menu.Item>
</Menu>
);
return currentUser && currentUser.name ? (
<HeaderDropdown overlay={menuHeaderDropdown}>
<span className={`${styles.action} ${styles.account}`}>
<Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
<span className={`${styles.name} anticon`}>{currentUser.name}</span>
</span>
</HeaderDropdown>
) : (
<span className={`${styles.action} ${styles.account}`}>
<Spin
size="small"
style={{
marginLeft: 8,
marginRight: 8,
}}
/>
</span>
);
}
}
export default connect(({ user }: ConnectState) => ({
currentUser: user.currentUser,
}))(AvatarDropdown);

View File

@ -0,0 +1,168 @@
import { Component } from 'react';
import type { ConnectProps } from 'umi';
import { connect } from 'umi';
import { Tag, message } from 'antd';
import groupBy from 'lodash/groupBy';
import moment from 'moment';
import type { NoticeItem } from '@/models/global';
import type { CurrentUser } from '@/models/user';
import type { ConnectState } from '@/models/connect';
import NoticeIcon from '../NoticeIcon';
import styles from './index.less';
export type GlobalHeaderRightProps = {
notices?: NoticeItem[];
currentUser?: CurrentUser;
fetchingNotices?: boolean;
onNoticeVisibleChange?: (visible: boolean) => void;
onNoticeClear?: (tabName?: string) => void;
} & Partial<ConnectProps>;
class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
componentDidMount() {
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'global/fetchNotices',
});
}
}
changeReadState = (clickedItem: NoticeItem): void => {
const { id } = clickedItem;
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'global/changeNoticeReadState',
payload: id,
});
}
};
handleNoticeClear = (title: string, key: string) => {
const { dispatch } = this.props;
message.success(`${'Emptied'} ${title}`);
if (dispatch) {
dispatch({
type: 'global/clearNotices',
payload: key,
});
}
};
getNoticeData = (): Record<string, NoticeItem[]> => {
const { notices = [] } = this.props;
if (!notices || notices.length === 0 || !Array.isArray(notices)) {
return {};
}
const newNotices = notices.map((notice) => {
const newNotice = { ...notice };
if (newNotice.datetime) {
newNotice.datetime = moment(notice.datetime as string).fromNow();
}
if (newNotice.id) {
newNotice.key = newNotice.id;
}
if (newNotice.extra && newNotice.status) {
const color = {
todo: '',
processing: 'blue',
urgent: 'red',
doing: 'gold',
}[newNotice.status];
newNotice.extra = (
<Tag
color={color}
style={{
marginRight: 0,
}}
>
{newNotice.extra}
</Tag>
);
}
return newNotice;
});
return groupBy(newNotices, 'type');
};
getUnreadData = (noticeData: Record<string, NoticeItem[]>) => {
const unreadMsg: Record<string, number> = {};
Object.keys(noticeData).forEach((key) => {
const value = noticeData[key];
if (!unreadMsg[key]) {
unreadMsg[key] = 0;
}
if (Array.isArray(value)) {
unreadMsg[key] = value.filter((item) => !item.read).length;
}
});
return unreadMsg;
};
render() {
const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props;
const noticeData = this.getNoticeData();
const unreadMsg = this.getUnreadData(noticeData);
return (
<NoticeIcon
className={styles.action}
count={currentUser && currentUser.unreadCount}
onItemClick={(item) => {
this.changeReadState(item as NoticeItem);
}}
loading={fetchingNotices}
clearText="Empty"
viewMoreText="See more"
onClear={this.handleNoticeClear}
onPopupVisibleChange={onNoticeVisibleChange}
onViewMore={() => message.info('Click on view more')}
clearClose
>
<NoticeIcon.Tab
tabKey="notification"
count={unreadMsg.notification}
list={noticeData.notification}
title="Notification"
emptyText="You have viewed all notifications"
showViewMore
/>
<NoticeIcon.Tab
tabKey="message"
count={unreadMsg.message}
list={noticeData.message}
title="Message"
emptyText="You have read all messages"
showViewMore
/>
<NoticeIcon.Tab
tabKey="event"
title="To do"
emptyText="You have completed all to-dos"
count={unreadMsg.event}
list={noticeData.event}
showViewMore
/>
</NoticeIcon>
);
}
}
export default connect(({ user, global, loading }: ConnectState) => ({
currentUser: user.currentUser,
collapsed: global.collapsed,
fetchingMoreNotices: loading.effects['global/fetchMoreNotices'],
fetchingNotices: loading.effects['global/fetchNotices'],
notices: global.notices,
}))(GlobalHeaderRight);

View File

@ -0,0 +1,89 @@
import { Tooltip, Tag } from 'antd';
import type { Settings as ProSettings } from '@ant-design/pro-layout';
import { QuestionCircleOutlined } from '@ant-design/icons';
import React from 'react';
import type { ConnectProps } from 'umi';
import { connect, SelectLang } from 'umi';
import type { ConnectState } from '@/models/connect';
import Avatar from './AvatarDropdown';
import HeaderSearch from '../HeaderSearch';
import styles from './index.less';
// import userIcon from '@/assets/user.svg';
// import homeIcon from '@/assets/home.svg';
// import shutdownIcon from '@/assets/shutdown.svg';
// import divider from '@/assets/divider.png';
// const theme = JSON.parse(PROJECT_THEME);
export type GlobalHeaderRightProps = {
theme?: ProSettings['navTheme'] | 'realDark';
} & Partial<ConnectProps> &
Partial<ProSettings>;
const ENVTagColor = {
dev: 'orange',
test: 'green',
pre: '#87d068',
};
const GlobalHeaderRight: React.SFC<GlobalHeaderRightProps> = (props) => {
const { theme, layout } = props;
let className = styles.right;
if (theme === 'dark' && layout === 'top') {
className = `${styles.right} ${styles.dark}`;
}
return (
<div className={className}>
<HeaderSearch
className={`${styles.action} ${styles.search}`}
placeholder="Site Search"
defaultValue="umi ui"
options={[
{ label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>, value: 'umi ui' },
{
label: <a href="next.ant.design">Ant Design</a>,
value: 'Ant Design',
},
{
label: <a href="https://protable.ant.design/">Pro Table</a>,
value: 'Pro Table',
},
{
label: <a href="https://prolayout.ant.design/">Pro Layout</a>,
value: 'Pro Layout',
},
]}
// onSearch={value => {
// //console.log('input', value);
// }}
/>
<Tooltip title="Use documentation">
<a
style={{
color: 'inherit',
}}
target="_blank"
href="https://pro.ant.design/docs/getting-started"
rel="noopener noreferrer"
className={styles.action}
>
<QuestionCircleOutlined />
</a>
</Tooltip>
<Avatar />
{REACT_APP_ENV && (
<span>
<Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
</span>
)}
<SelectLang className={styles.action} />
</div>
);
};
export default connect((state: ConnectState) => ({
theme: state.settings?.navTheme,
layout: state.settings?.layout,
}))(GlobalHeaderRight);

View File

@ -0,0 +1,96 @@
@import '~antd/es/style/themes/default.less';
@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
.menu {
:global(.anticon) {
margin-right: 8px;
}
:global(.ant-dropdown-menu-item) {
min-width: 160px;
}
}
.left-logo {
top: 16px;
img {
margin-left: 32px;
margin-right: 12px;
top: -3px;
}
}
.right-btns {
color: inherit;
padding: 0 10px;
}
.right {
display: flex;
float: right;
height: 48px;
margin-left: auto;
overflow: hidden;
.action {
display: flex;
align-items: center;
height: 100%;
padding: 0 12px;
cursor: pointer;
transition: all 0.3s;
> span {
vertical-align: middle;
}
&:hover {
background: @pro-header-hover-bg;
}
&:global(.opened) {
background: @pro-header-hover-bg;
}
}
.search {
padding: 0 12px;
&:hover {
background: transparent;
}
}
.account {
.avatar {
margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0;
margin-right: 8px;
color: @primary-color;
vertical-align: top;
background: rgba(255, 255, 255, 0.85);
}
}
}
.dark {
.action {
color: rgba(255, 255, 255, 0.85);
> span {
color: rgba(255, 255, 255, 0.85);
}
&:hover,
&:global(.opened) {
background: @primary-color;
}
}
}
:global(.ant-pro-global-header) {
.dark {
.action {
color: @text-color;
> span {
color: @text-color;
}
&:hover {
color: rgba(255, 255, 255, 0.85);
> span {
color: rgba(255, 255, 255, 0.85);
}
}
}
}
}

View File

@ -0,0 +1,16 @@
@import '~antd/es/style/themes/default.less';
.container > * {
background-color: @popover-bg;
border-radius: 4px;
box-shadow: @shadow-1-down;
}
@media screen and (max-width: @screen-xs) {
.container {
width: 100% !important;
}
.container > * {
border-radius: 0 !important;
}
}

View File

@ -0,0 +1,17 @@
import type { DropDownProps } from 'antd/es/dropdown';
import { Dropdown } from 'antd';
import React from 'react';
import classNames from 'classnames';
import styles from './index.less';
export type HeaderDropdownProps = {
overlayClassName?: string;
overlay: React.ReactNode | (() => React.ReactNode) | any;
placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
} & Omit<DropDownProps, 'overlay'>;
const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => (
<Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
);
export default HeaderDropdown;

View File

@ -0,0 +1,30 @@
@import '~antd/es/style/themes/default.less';
.headerSearch {
.input {
width: 0;
min-width: 0;
overflow: hidden;
background: transparent;
border-radius: 0;
transition: width 0.3s, margin-left 0.3s;
:global(.ant-select-selection) {
background: transparent;
}
input {
padding-right: 0;
padding-left: 0;
border: 0;
box-shadow: none !important;
}
&,
&:hover,
&:focus {
border-bottom: 1px solid @border-color-base;
}
&.show {
width: 210px;
margin-left: 8px;
}
}
}

View File

@ -0,0 +1,105 @@
import { SearchOutlined } from '@ant-design/icons';
import { AutoComplete, Input } from 'antd';
import useMergedState from 'rc-util/es/hooks/useMergedState';
import type { AutoCompleteProps } from 'antd/es/auto-complete';
import React, { useRef } from 'react';
import classNames from 'classnames';
import styles from './index.less';
export type HeaderSearchProps = {
onSearch?: (value?: string) => void;
onChange?: (value?: string) => void;
onVisibleChange?: (b: boolean) => void;
className?: string;
placeholder?: string;
options: AutoCompleteProps['options'];
defaultOpen?: boolean;
open?: boolean;
defaultValue?: string;
value?: string;
};
const HeaderSearch: React.FC<HeaderSearchProps> = (props) => {
const {
className,
defaultValue,
onVisibleChange,
placeholder,
open,
defaultOpen,
...restProps
} = props;
const inputRef = useRef<Input | null>(null);
const [value, setValue] = useMergedState<string | undefined>(defaultValue, {
value: props.value,
onChange: props.onChange,
});
const [searchMode, setSearchMode] = useMergedState(defaultOpen ?? false, {
value: props.open,
onChange: onVisibleChange,
});
const inputClass = classNames(styles.input, {
[styles.show]: searchMode,
});
return (
<div
className={classNames(className, styles.headerSearch)}
onClick={() => {
setSearchMode(true);
if (searchMode && inputRef.current) {
inputRef.current.focus();
}
}}
onTransitionEnd={({ propertyName }) => {
if (propertyName === 'width' && !searchMode) {
if (onVisibleChange) {
onVisibleChange(searchMode);
}
}
}}
>
<SearchOutlined
key="Icon"
style={{
cursor: 'pointer',
}}
/>
<AutoComplete
key="AutoComplete"
className={inputClass}
value={value}
style={{
height: 28,
marginTop: -6,
}}
options={restProps.options}
onChange={setValue}
>
<Input
ref={inputRef}
defaultValue={defaultValue}
aria-label={placeholder}
placeholder={placeholder}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (restProps.onSearch) {
restProps.onSearch(value);
}
}
}}
onBlur={() => {
setSearchMode(false);
}}
/>
</AutoComplete>
</div>
);
};
export default HeaderSearch;

View File

@ -1,7 +1,15 @@
@import '~antd/es/style/themes/default.less';
@import './baseStyle.less';
// 页面公用样式
@import './utils/componentStyle.less';
@font-face {
font-family: 'HarmonyOS Sans SC';
src: url('./assets/HarmonyOS_Sans_SC_Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
html,
body,
@ -16,6 +24,7 @@ body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: 'HarmonyOS Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.ant-layout-header {
@ -28,3 +37,41 @@ body {
padding: 4px 8px;
}
}
.ant-layout {
// min-height: 100vh;
}
canvas {
display: block;
}
ul,
ol {
list-style: none;
}
@media (max-width: @screen-xs) {
.ant-table {
width: 100%;
overflow-x: auto;
&-thead > tr,
&-tbody > tr {
> th,
> td {
white-space: pre;
> span {
display: block;
}
}
}
}
}
// Compatible with IE11
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
body .ant-design-pro > .ant-layout {
// min-height: 100vh;
}
}

View File

@ -0,0 +1,10 @@
import React from 'react';
import { Inspector } from 'react-dev-inspector';
const InspectorWrapper = process.env.NODE_ENV === 'development' ? Inspector : React.Fragment;
const Layout: React.FC = ({ children }) => {
return <InspectorWrapper>{children}</InspectorWrapper>;
};
export default Layout;

View File

@ -0,0 +1,58 @@
import React from 'react';
import { PageLoading } from '@ant-design/pro-layout';
import type { ConnectProps } from 'umi';
import { Redirect, connect } from 'umi';
import { stringify } from 'querystring';
import type { ConnectState } from '@/models/connect';
import type { CurrentUser } from '@/models/user';
type SecurityLayoutProps = {
loading?: boolean;
currentUser?: CurrentUser;
} & ConnectProps;
type SecurityLayoutState = {
isReady: boolean;
};
class SecurityLayout extends React.Component<SecurityLayoutProps, SecurityLayoutState> {
state: SecurityLayoutState = {
isReady: false,
};
componentDidMount() {
this.setState({
isReady: true,
});
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'user/fetchCurrent',
});
}
}
render() {
const { isReady } = this.state;
const { children, loading, currentUser } = this.props;
// You can replace it to your authentication rule (such as check token exists)
// You can replace it with your own login authentication rules (such as judging whether the token exists)
const isLogin = currentUser && currentUser.userid;
const queryString = stringify({
redirect: window.location.href,
});
if ((!isLogin && loading) || !isReady) {
return <PageLoading />;
}
if (!isLogin && window.location.pathname !== '/user/login') {
return <Redirect to={`/user/login?${queryString}`} />;
}
return children;
}
}
export default connect(({ user, loading }: ConnectState) => ({
currentUser: user.currentUser,
loading: loading.models.user,
}))(SecurityLayout);

View File

@ -0,0 +1,71 @@
@import '~antd/es/style/themes/default.less';
.container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
background: @layout-body-background;
}
.lang {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
:global(.ant-dropdown-trigger) {
margin-right: 24px;
}
}
.content {
flex: 1;
padding: 32px 0;
}
@media (min-width: @screen-md-min) {
.container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
.content {
padding: 32px 0 24px;
}
}
.top {
text-align: center;
}
.header {
height: 44px;
line-height: 44px;
a {
text-decoration: none;
}
}
.logo {
height: 44px;
margin-right: 16px;
vertical-align: top;
}
.title {
position: relative;
top: 2px;
color: @heading-color;
font-weight: 600;
font-size: 33px;
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
}
.desc {
margin-top: 12px;
margin-bottom: 40px;
color: @text-color-secondary;
font-size: @font-size-base;
}

View File

@ -0,0 +1,70 @@
import type { MenuDataItem } from '@ant-design/pro-layout';
import { DefaultFooter, getMenuData, getPageTitle } from '@ant-design/pro-layout';
import { Helmet, HelmetProvider } from 'react-helmet-async';
import type { ConnectProps } from 'umi';
import { Link, SelectLang, useIntl, connect, FormattedMessage } from 'umi';
import React from 'react';
import type { ConnectState } from '@/models/connect';
import logo from '../assets/logo.svg';
import styles from './UserLayout.less';
export type UserLayoutProps = {
breadcrumbNameMap: Record<string, MenuDataItem>;
} & Partial<ConnectProps>;
const UserLayout: React.FC<UserLayoutProps> = (props) => {
const {
route = {
routes: [],
},
} = props;
const { routes = [] } = route;
const {
children,
location = {
pathname: '',
},
} = props;
const { formatMessage } = useIntl();
const { breadcrumb } = getMenuData(routes);
const title = getPageTitle({
pathname: location.pathname,
formatMessage,
breadcrumb,
...props,
});
return (
<HelmetProvider>
<Helmet>
<title>{title}</title>
<meta name="description" content={title} />
</Helmet>
<div className={styles.container}>
<div className={styles.lang}>
<SelectLang />
</div>
<div className={styles.content}>
<div className={styles.top}>
<div className={styles.header}>
<Link to="/">
<img alt="logo" className={styles.logo} src={logo} />
<span className={styles.title}>Ant Design</span>
</Link>
</div>
<div className={styles.desc}>
<FormattedMessage
id="pages.layouts.userLayout.title"
defaultMessage="Ant Design. The most influential Web design specification in Xihu District."
/>
</div>
</div>
{children}
</div>
<DefaultFooter />
</div>
</HelmetProvider>
);
};
export default connect(({ settings }: ConnectState) => ({ ...settings }))(UserLayout);

View File

@ -72,9 +72,9 @@ export default {
'menu.categoryLibraryReview': '品类库建库审核管理',
'menu.supplierEntryManage': '供应商入库管理',
'menu.supplierEntryReview': '供应商入库审核管理',
'menu.supplierBlacklist': '供应商黑名单管理',
'menu.blacklistManage': '供应商黑名单管理',
'menu.blacklistAudit': '供应商黑名单审核管理',
'menu.supplierBlacklist': '供应商黑名单管理',
'menu.blacklistManage': '供应商黑名单管理',
'menu.blacklistAudit': '供应商黑名单审核管理',
'menu.supplierExit': '供应商退出管理',
'menu.supplierExitManage': '供应商退出管理',
'menu.supplierExitAudit': '供应商退出审核管理',

30
src/models/connect.d.ts vendored Normal file
View File

@ -0,0 +1,30 @@
import type { MenuDataItem, Settings as ProSettings } from '@ant-design/pro-layout';
import { GlobalModelState } from './global';
import { UserModelState } from './user';
import type { StateType } from './login';
export { GlobalModelState, UserModelState };
export type Loading = {
global: boolean;
effects: Record<string, boolean | undefined>;
models: {
global?: boolean;
menu?: boolean;
setting?: boolean;
user?: boolean;
login?: boolean;
};
};
export type ConnectState = {
global: GlobalModelState;
loading: Loading;
settings: ProSettings;
user: UserModelState;
login: StateType;
};
export type Route = {
routes?: Route[];
} & MenuDataItem;

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

@ -0,0 +1,85 @@
import type { Effect, Reducer } from 'umi';
import { queryCurrent, query as queryUsers } from '@/servers/user';
export type CurrentUser = {
avatar?: string;
name?: string;
title?: string;
group?: string;
signature?: string;
tags?: {
key: string;
label: string;
}[];
userid?: string;
unreadCount?: number;
};
export type UserModelState = {
currentUser?: CurrentUser;
};
export type UserModelType = {
namespace: 'user';
state: UserModelState;
effects: {
fetch: Effect;
fetchCurrent: Effect;
};
reducers: {
saveCurrentUser: Reducer<UserModelState>;
changeNotifyCount: Reducer<UserModelState>;
};
};
const UserModel: UserModelType = {
namespace: 'user',
state: {
currentUser: {},
},
effects: {
*fetch(_, { call, put }) {
const response = yield call(queryUsers);
yield put({
type: 'save',
payload: response,
});
},
*fetchCurrent(_, { call, put }) {
const response = yield call(queryCurrent);
yield put({
type: 'saveCurrentUser',
payload: response,
});
},
},
reducers: {
saveCurrentUser(state, action) {
return {
...state,
currentUser: action.payload || {},
};
},
changeNotifyCount(
state = {
currentUser: {},
},
action,
) {
return {
...state,
currentUser: {
...state.currentUser,
notifyCount: action.payload.totalCount,
unreadCount: action.payload.unreadCount,
},
};
},
},
};
export default UserModel;

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />

View File

@ -2,7 +2,7 @@
* 供应商注册表单通用部分
* 封装了国内企业和境外企业注册表单相同的部分
*/
import React from 'react';
import React, { useState, useEffect } from 'react';
import {
Form,
Input,
@ -20,55 +20,26 @@ import {
import { UploadOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import { message } from 'antd';
import { validateFileSize } from '@/utils/utils';
import { getRegionTree, getregionInternational } from '@/servers/api/register';
const { Option } = Select;
// 中国省市区级联数据
export const addressOptions = [
{
value: '330000',
label: '浙江省',
children: [
{
value: '330100',
label: '杭州市',
children: [
{ value: '330102', label: '上城区' },
{ value: '330103', label: '下城区' },
{ value: '330104', label: '江干区' },
{ value: '330105', label: '拱墅区' },
{ value: '330106', label: '西湖区' },
{ value: '330108', label: '滨江区' },
],
},
],
},
{
value: '310000',
label: '上海市',
children: [
{
value: '310100',
label: '上海市',
children: [
{ value: '310101', label: '黄浦区' },
{ value: '310104', label: '徐汇区' },
{ value: '310105', label: '长宁区' },
],
},
],
},
];
interface CommonFormSectionsProps {
form: any;
supplierType?: string;
}
// 扩展问卷部分的属性接口
interface SurveySectionProps extends CommonFormSectionsProps {
surveyQuestions?: API.SurveyQuestionResponse;
}
function convertToCascaderOptions(data: any[]): any[] {
return data.map(item => ({
label: item.name,
value: item.id,
children: item.children && item.children.length > 0 ? convertToCascaderOptions(item.children) : undefined,
}));
}
/**
* 资质信息表单部分
* 包含资质证书类型、名称、编号、等级、发证机构、发证日期、有效期等
@ -326,7 +297,21 @@ export const InvoiceSection: React.FC<CommonFormSectionsProps> = ({ form }) => {
* 银行账户表单部分
* 包含开户银行、账户名称、账号、所在地区等
*/
export const BankAccountSection: React.FC<CommonFormSectionsProps> = ({ form }) => {
export const BankAccountSection: React.FC<CommonFormSectionsProps> = ({ form, supplierType }) => {
// 地区
const [addressOptions, setAddressOptions] = useState<API.RegionOption[]>([]);
useEffect(() => {
if(supplierType) {
const submitInterface = supplierType === 'dvs'? getRegionTree : getregionInternational;
submitInterface().then(res => {
if (res.code === 200) {
setAddressOptions(convertToCascaderOptions(res.data));
}
});
}
}, [supplierType]);
return (
<>
<div className="form-section-title"></div>
@ -391,7 +376,7 @@ export const BankAccountSection: React.FC<CommonFormSectionsProps> = ({ form })
),
},
{
title: '国家、省、市',
title: '地址',
dataIndex: 'location',
render: (_, record) => (
<Form.Item
@ -401,7 +386,7 @@ export const BankAccountSection: React.FC<CommonFormSectionsProps> = ({ form })
>
<Cascader
options={addressOptions}
placeholder="请选择省市区"
placeholder="请选择地址"
showSearch={{
filter: (inputValue, path) => {
return path.some((option) => {

View File

@ -256,12 +256,12 @@ const DomesticForm: React.FC<DomesticFormProps> = ({
{ required: true, message: '请输入电子邮箱' },
]}
>
<Input prefix={<MailOutlined />} placeholder="请输入企业联系电话" />
<Input prefix={<MailOutlined />} placeholder="XXX@XXX.com " />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="telephone" label="固定电话">
<Input placeholder="XXX@XXX.com" />
<Input placeholder="请输入企业联系电话" />
</Form.Item>
</Col>
</Row>
@ -269,7 +269,7 @@ const DomesticForm: React.FC<DomesticFormProps> = ({
{/* 使用通用表单组件 */}
<QualificationSection form={form} />
<InvoiceSection form={form} />
<BankAccountSection form={form} />
<BankAccountSection form={form} supplierType={'dvs'} />
<SurveySection form={form} surveyQuestions={surveyQuestions} />
<AttachmentSection form={form} />
</>

View File

@ -1,6 +1,6 @@
/* 境外企业 表单项 */
import React from 'react';
import { Form, Input, Button, Select, Row, Col } from 'antd';
import React, { useState, useEffect } from 'react';
import { Form, Input, Button, Select, Row, Col, Cascader } from 'antd';
import {
MobileOutlined,
MailOutlined,
@ -17,6 +17,8 @@ import {
AttachmentSection,
} from './CommonFormSections';
import { getregionInternational } from '@/servers/api/register';
const { Option } = Select;
const { TextArea } = Input;
@ -33,6 +35,16 @@ interface ForeignFormProps {
* 其他部分使用通用表单组件
*/
const ForeignForm: React.FC<ForeignFormProps> = ({ form, countdown, handleGetCaptcha, surveyQuestions }) => {
// 全球
const [regionOptions, setRegionOptions] = useState<API.RegionOption[]>([]);
useEffect(() => {
getregionInternational().then(res => {
if (res.code === 200) {
setRegionOptions(res.data);
}
});
}, []);
return (
<>
{/* 境外企业特有的基本信息部分 */}
@ -73,16 +85,11 @@ const ForeignForm: React.FC<ForeignFormProps> = ({ form, countdown, handleGetCap
rules={[{ required: true, message: '请选择国家/地区' }]}
>
<Select placeholder="请选择国家/地区">
<Option value="US"></Option>
<Option value="UK"></Option>
<Option value="JP"></Option>
<Option value="DE"></Option>
<Option value="FR"></Option>
<Option value="AU"></Option>
<Option value="CA"></Option>
<Option value="SG"></Option>
<Option value="HK"></Option>
<Option value="OTHER"></Option>
{
regionOptions.map((item) => {
return <Option value={item.id}>{item.name}</Option>
})
}
</Select>
</Form.Item>
</Col>
@ -307,7 +314,7 @@ const ForeignForm: React.FC<ForeignFormProps> = ({ form, countdown, handleGetCap
{/* 使用通用表单组件 */}
<QualificationSection form={form} />
<InvoiceSection form={form} />
<BankAccountSection form={form} />
<BankAccountSection form={form} supplierType={'ovs'} />
<SurveySection form={form} surveyQuestions={surveyQuestions} />
<AttachmentSection form={form} />
</>

View File

@ -80,8 +80,8 @@ const supplierNews: React.FC = () => {
},
{
title: '业务类型',
dataIndex: 'type',
key: 'type',
dataIndex: 'typeCn',
key: 'typeCn',
},
{
title: '发送时间',

View File

@ -120,12 +120,7 @@ const SupplierAccessDetailModal: React.FC<SupplierAccessDetailModalProps> = ({ v
key: "exitTime",
align: "center",
},
{
title: "进入黑名单时间",
dataIndex: "blackTime",
key: "blackTime",
align: "center",
},
];
return (
<Modal

View File

@ -115,12 +115,7 @@ const SupplierAccessDetailModal: React.FC<SupplierAccessDetailModalProps> = ({ v
key: "exitTime",
align: "center",
},
{
title: "进入黑名单时间",
dataIndex: "blackTime",
key: "blackTime",
align: "center",
},
];
return (
<Modal

View File

@ -11,7 +11,7 @@ const messageTypeOptions = [
{ label: '供应商评价', value: '供应商评价' },
{ label: '供应商评审', value: '供应商评审' },
{ label: '供应商退出', value: '供应商退出' },
{ label: '供应商黑名单', value: '供应商黑名单' },
{ label: '供应商黑名单', value: '供应商黑名单' },
];
const SupplierMessage: React.FC = () => {

View File

@ -21,3 +21,17 @@ export async function fetchSurveyQuestions(): Promise<API.APIResponse<API.Survey
method: 'GET',
})
}
// 获取省市县
export async function getRegionTree(){
return request('/api/cosco/dictRegion/region', {
method: 'GET',
})
}
// 获取全球
export async function getregionInternational(){
return request('/api/cosco/dictRegion/regionInternational', {
method: 'GET',
})
}

View File

@ -5,6 +5,14 @@ declare namespace API {
message: string;
data: T;
}
// 省市区
interface RegionOption {
label: string;
value: string;
id: string;
name: string;
children?: RegionOption[];
}
// 用户相关接口类型定义
export interface UserListRequest {

13
src/servers/user.ts Normal file
View File

@ -0,0 +1,13 @@
import request from '@/utils/request';
export async function query(): Promise<any> {
return request('/api/users');
}
export async function queryCurrent(): Promise<any> {
return request('/api/currentUser');
}
export async function queryNotices(): Promise<any> {
return request('/api/notices');
}

16
src/utils/Authorized.ts Normal file
View File

@ -0,0 +1,16 @@
import RenderAuthorize from '@/components/Authorized';
import { getAuthority } from './authority';
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable import/no-mutable-exports */
let Authorized = RenderAuthorize(getAuthority());
// Reload the rights component
const reloadAuthorized = (): void => {
Authorized = RenderAuthorize(getAuthority());
};
/** Hard code block need it。 */
window.reloadAuthorized = reloadAuthorized;
export { reloadAuthorized };
export default Authorized;

32
src/utils/authority.ts Normal file
View File

@ -0,0 +1,32 @@
import { reloadAuthorized } from './Authorized';
// use localStorage to store the authority info, which might be sent from server in actual project.
export function getAuthority(str?: string): string | string[] {
const authorityString =
typeof str === 'undefined' && localStorage ? localStorage.getItem('antd-pro-authority') : str;
// authorityString could be admin, "admin", ["admin"]
let authority;
try {
if (authorityString) {
authority = JSON.parse(authorityString);
}
} catch (e) {
authority = authorityString;
}
if (typeof authority === 'string') {
return [authority];
}
// preview.pro.ant.design only do not use in your production.
// preview.pro.ant.design Dedicated environment variable, please do not use it in your project.
if (!authority && ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
return ['admin'];
}
return authority;
}
export function setAuthority(authority: string | string[]): void {
const proAuthority = typeof authority === 'string' ? [authority] : authority;
localStorage.setItem('antd-pro-authority', JSON.stringify(proAuthority));
// auto reload
reloadAuthorized();
}