diff --git a/package.json b/package.json index 702f1a8..9fb89b3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/favicon.ico b/public/favicon.ico index 29f0ff4..56bab80 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/logo.svg b/public/logo.svg index 113f6d9..2641598 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -1,25 +1 @@ - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/assets/HarmonyOS_Sans_SC_Regular.ttf b/src/assets/HarmonyOS_Sans_SC_Regular.ttf new file mode 100644 index 0000000..aff150a Binary files /dev/null and b/src/assets/HarmonyOS_Sans_SC_Regular.ttf differ diff --git a/src/assets/arrow-left.svg b/src/assets/arrow-left.svg new file mode 100644 index 0000000..2effad8 --- /dev/null +++ b/src/assets/arrow-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/arrow-right.svg b/src/assets/arrow-right.svg new file mode 100644 index 0000000..9a8522e --- /dev/null +++ b/src/assets/arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/divider.png b/src/assets/divider.png new file mode 100644 index 0000000..f6d8ee1 Binary files /dev/null and b/src/assets/divider.png differ diff --git a/src/assets/home.svg b/src/assets/home.svg new file mode 100644 index 0000000..ced8afb --- /dev/null +++ b/src/assets/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..2641598 --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/shutdown.svg b/src/assets/shutdown.svg new file mode 100644 index 0000000..5ab9f1c --- /dev/null +++ b/src/assets/shutdown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/user.svg b/src/assets/user.svg new file mode 100644 index 0000000..44c668d --- /dev/null +++ b/src/assets/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/baseStyle.less b/src/baseStyle.less index df231c6..e6bf7f9 100644 --- a/src/baseStyle.less +++ b/src/baseStyle.less @@ -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; +// } + diff --git a/src/components/Authorized/Authorized.tsx b/src/components/Authorized/Authorized.tsx new file mode 100644 index 0000000..4f6d2b4 --- /dev/null +++ b/src/components/Authorized/Authorized.tsx @@ -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 & { + Secured: typeof Secured; + check: typeof check; + AuthorizedRoute: typeof AuthorizedRoute; +}; + +const Authorized: React.FunctionComponent = ({ + children, + authority, + noMatch = ( + + ), +}) => { + const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children; + const dom = check(authority, childrenRender, noMatch); + return <>{dom}; +}; + +export default Authorized as IAuthorizedType; diff --git a/src/components/Authorized/AuthorizedRoute.tsx b/src/components/Authorized/AuthorizedRoute.tsx new file mode 100644 index 0000000..751a344 --- /dev/null +++ b/src/components/Authorized/AuthorizedRoute.tsx @@ -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; + render: (props: any) => React.ReactNode; + redirectPath: string; + authority: IAuthorityType; +}; + +const AuthorizedRoute: React.SFC = ({ + component: Component, + render, + authority, + redirectPath, + ...rest +}) => ( + } />} + > + (Component ? : render(props))} + /> + +); + +export default AuthorizedRoute; diff --git a/src/components/Authorized/CheckPermissions.tsx b/src/components/Authorized/CheckPermissions.tsx new file mode 100644 index 0000000..021b7cb --- /dev/null +++ b/src/components/Authorized/CheckPermissions.tsx @@ -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 + | ((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 = ( + 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 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 ok={target} error={Exception} promise={bool} />; + } + if (bool) { + return target; + } + return Exception; + } + throw new Error('unsupported parameters'); +}; + +export { checkPermissions }; + +function check(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode { + return checkPermissions(authority, CURRENT, target, Exception); +} + +export default check; diff --git a/src/components/Authorized/PromiseRender.tsx b/src/components/Authorized/PromiseRender.tsx new file mode 100644 index 0000000..f8f15c8 --- /dev/null +++ b/src/components/Authorized/PromiseRender.tsx @@ -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 = { + ok: T; + error: K; + promise: Promise; +}; + +type PromiseRenderState = { + component: React.ComponentClass | React.FunctionComponent; +}; + +export default class PromiseRender extends React.Component< + PromiseRenderProps, + PromiseRenderState +> { + state: PromiseRenderState = { + component: () => null, + }; + + componentDidMount(): void { + this.setRenderComponent(this.props); + } + + shouldComponentUpdate = ( + nextProps: PromiseRenderProps, + 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): 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) => ; + } + 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 ? ( + + ) : ( +
+ +
+ ); + } +} diff --git a/src/components/Authorized/Secured.tsx b/src/components/Authorized/Secured.tsx new file mode 100644 index 0000000..1a6d1c6 --- /dev/null +++ b/src/components/Authorized/Secured.tsx @@ -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) => ; + } + 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; diff --git a/src/components/Authorized/index.tsx b/src/components/Authorized/index.tsx new file mode 100644 index 0000000..6703a46 --- /dev/null +++ b/src/components/Authorized/index.tsx @@ -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; diff --git a/src/components/Authorized/renderAuthorize.ts b/src/components/Authorized/renderAuthorize.ts new file mode 100644 index 0000000..3c34cc6 --- /dev/null +++ b/src/components/Authorized/renderAuthorize.ts @@ -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 = (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 (Authorized: T) => renderAuthorize(Authorized); diff --git a/src/components/CompanyInfo/index.tsx b/src/components/CompanyInfo/index.tsx index 3784aba..b29c38e 100644 --- a/src/components/CompanyInfo/index.tsx +++ b/src/components/CompanyInfo/index.tsx @@ -16,7 +16,7 @@ interface CompanyInfoProps { const { TabPane } = Tabs; const CompanyInfo: React.FC = (props) => { - const { viewType = false, record = '' } = props; + const { viewType = false, record = '999698' } = props; const intl = useIntl(); // 切换tab const [subTab, setSubTab] = useState('base'); diff --git a/src/components/CompanyInfo/services.ts b/src/components/CompanyInfo/services.ts index 2b1a5d3..fa5873a 100644 --- a/src/components/CompanyInfo/services.ts +++ b/src/components/CompanyInfo/services.ts @@ -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'}`); /** * 资质分页列表 diff --git a/src/components/GlobalHeader/AvatarDropdown.tsx b/src/components/GlobalHeader/AvatarDropdown.tsx new file mode 100644 index 0000000..f86b2c9 --- /dev/null +++ b/src/components/GlobalHeader/AvatarDropdown.tsx @@ -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; + +class AvatarDropdown extends React.Component { + 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 && ( + + + 个人中心 + + )} + {menu && ( + + + 个人设置 + + )} + {menu && } + + + + 退出登录 + + + ); + return currentUser && currentUser.name ? ( + + + + {currentUser.name} + + + ) : ( + + + + ); + } +} + +export default connect(({ user }: ConnectState) => ({ + currentUser: user.currentUser, +}))(AvatarDropdown); diff --git a/src/components/GlobalHeader/NoticeIconView.tsx b/src/components/GlobalHeader/NoticeIconView.tsx new file mode 100644 index 0000000..d908e9f --- /dev/null +++ b/src/components/GlobalHeader/NoticeIconView.tsx @@ -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; + +class GlobalHeaderRight extends Component { + 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 => { + 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 = ( + + {newNotice.extra} + + ); + } + + return newNotice; + }); + return groupBy(newNotices, 'type'); + }; + + getUnreadData = (noticeData: Record) => { + const unreadMsg: Record = {}; + 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 ( + { + 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 + > + + + + + ); + } +} + +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); diff --git a/src/components/GlobalHeader/RightContent.tsx b/src/components/GlobalHeader/RightContent.tsx new file mode 100644 index 0000000..97819e5 --- /dev/null +++ b/src/components/GlobalHeader/RightContent.tsx @@ -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 & + Partial; + +const ENVTagColor = { + dev: 'orange', + test: 'green', + pre: '#87d068', +}; + +const GlobalHeaderRight: React.SFC = (props) => { + const { theme, layout } = props; + let className = styles.right; + + if (theme === 'dark' && layout === 'top') { + className = `${styles.right} ${styles.dark}`; + } + + return ( +
+ umi ui, value: 'umi ui' }, + { + label: Ant Design, + value: 'Ant Design', + }, + { + label: Pro Table, + value: 'Pro Table', + }, + { + label: Pro Layout, + value: 'Pro Layout', + }, + ]} + // onSearch={value => { + // //console.log('input', value); + // }} + /> + + + + + + + {REACT_APP_ENV && ( + + {REACT_APP_ENV} + + )} + +
+ ); +}; + +export default connect((state: ConnectState) => ({ + theme: state.settings?.navTheme, + layout: state.settings?.layout, +}))(GlobalHeaderRight); diff --git a/src/components/GlobalHeader/index.less b/src/components/GlobalHeader/index.less new file mode 100644 index 0000000..feea2df --- /dev/null +++ b/src/components/GlobalHeader/index.less @@ -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); + } + } + } + } +} diff --git a/src/components/HeaderDropdown/index.less b/src/components/HeaderDropdown/index.less new file mode 100644 index 0000000..004b53e --- /dev/null +++ b/src/components/HeaderDropdown/index.less @@ -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; + } +} diff --git a/src/components/HeaderDropdown/index.tsx b/src/components/HeaderDropdown/index.tsx new file mode 100644 index 0000000..45af90a --- /dev/null +++ b/src/components/HeaderDropdown/index.tsx @@ -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; + +const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => ( + +); + +export default HeaderDropdown; diff --git a/src/components/HeaderSearch/index.less b/src/components/HeaderSearch/index.less new file mode 100644 index 0000000..9af69d5 --- /dev/null +++ b/src/components/HeaderSearch/index.less @@ -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; + } + } +} diff --git a/src/components/HeaderSearch/index.tsx b/src/components/HeaderSearch/index.tsx new file mode 100644 index 0000000..d3b1127 --- /dev/null +++ b/src/components/HeaderSearch/index.tsx @@ -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 = (props) => { + const { + className, + defaultValue, + onVisibleChange, + placeholder, + open, + defaultOpen, + ...restProps + } = props; + + const inputRef = useRef(null); + + const [value, setValue] = useMergedState(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 ( +
{ + setSearchMode(true); + if (searchMode && inputRef.current) { + inputRef.current.focus(); + } + }} + onTransitionEnd={({ propertyName }) => { + if (propertyName === 'width' && !searchMode) { + if (onVisibleChange) { + onVisibleChange(searchMode); + } + } + }} + > + + + { + if (e.key === 'Enter') { + if (restProps.onSearch) { + restProps.onSearch(value); + } + } + }} + onBlur={() => { + setSearchMode(false); + }} + /> + +
+ ); +}; + +export default HeaderSearch; diff --git a/src/global.less b/src/global.less index a30358b..7c21bde 100644 --- a/src/global.less +++ b/src/global.less @@ -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; + } +} + diff --git a/src/layouts/BlankLayout.tsx b/src/layouts/BlankLayout.tsx new file mode 100644 index 0000000..28a0a8d --- /dev/null +++ b/src/layouts/BlankLayout.tsx @@ -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 {children}; +}; + +export default Layout; diff --git a/src/layouts/SecurityLayout.tsx b/src/layouts/SecurityLayout.tsx new file mode 100644 index 0000000..6597213 --- /dev/null +++ b/src/layouts/SecurityLayout.tsx @@ -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 { + 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 ; + } + if (!isLogin && window.location.pathname !== '/user/login') { + return ; + } + return children; + } +} + +export default connect(({ user, loading }: ConnectState) => ({ + currentUser: user.currentUser, + loading: loading.models.user, +}))(SecurityLayout); diff --git a/src/layouts/UserLayout.less b/src/layouts/UserLayout.less new file mode 100644 index 0000000..cdc207e --- /dev/null +++ b/src/layouts/UserLayout.less @@ -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; +} diff --git a/src/layouts/UserLayout.tsx b/src/layouts/UserLayout.tsx new file mode 100644 index 0000000..f5ad681 --- /dev/null +++ b/src/layouts/UserLayout.tsx @@ -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; +} & Partial; + +const UserLayout: React.FC = (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 ( + + + {title} + + + +
+
+ +
+
+
+
+ + logo + Ant Design + +
+
+ +
+
+ {children} +
+ +
+
+ ); +}; + +export default connect(({ settings }: ConnectState) => ({ ...settings }))(UserLayout); diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index 9cbb643..cbe33a5 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -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': '供应商退出审核管理', diff --git a/src/models/connect.d.ts b/src/models/connect.d.ts new file mode 100644 index 0000000..60fe8e1 --- /dev/null +++ b/src/models/connect.d.ts @@ -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; + 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; diff --git a/src/models/user.ts b/src/models/user.ts new file mode 100644 index 0000000..08322ad --- /dev/null +++ b/src/models/user.ts @@ -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; + changeNotifyCount: Reducer; + }; +}; + +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; diff --git a/src/pages/document.ejs b/src/pages/document.ejs index e2623cb..b6bdfb4 100644 --- a/src/pages/document.ejs +++ b/src/pages/document.ejs @@ -1,5 +1,5 @@ - + diff --git a/src/pages/register/supplier/CommonFormSections.tsx b/src/pages/register/supplier/CommonFormSections.tsx index 66f8b98..a94fe36 100644 --- a/src/pages/register/supplier/CommonFormSections.tsx +++ b/src/pages/register/supplier/CommonFormSections.tsx @@ -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 = ({ form }) => { * 银行账户表单部分 * 包含开户银行、账户名称、账号、所在地区等 */ -export const BankAccountSection: React.FC = ({ form }) => { +export const BankAccountSection: React.FC = ({ form, supplierType }) => { + // 地区 + const [addressOptions, setAddressOptions] = useState([]); + + useEffect(() => { + if(supplierType) { + const submitInterface = supplierType === 'dvs'? getRegionTree : getregionInternational; + submitInterface().then(res => { + if (res.code === 200) { + setAddressOptions(convertToCascaderOptions(res.data)); + } + }); + } + + }, [supplierType]); return ( <>
银行账户
@@ -391,7 +376,7 @@ export const BankAccountSection: React.FC = ({ form }) ), }, { - title: '国家、省、市', + title: '地址', dataIndex: 'location', render: (_, record) => ( = ({ form }) > { return path.some((option) => { diff --git a/src/pages/register/supplier/DomesticForm.tsx b/src/pages/register/supplier/DomesticForm.tsx index d2218c1..b052813 100644 --- a/src/pages/register/supplier/DomesticForm.tsx +++ b/src/pages/register/supplier/DomesticForm.tsx @@ -256,12 +256,12 @@ const DomesticForm: React.FC = ({ { required: true, message: '请输入电子邮箱' }, ]} > - } placeholder="请输入企业联系电话" /> + } placeholder="XXX@XXX.com " /> - + @@ -269,7 +269,7 @@ const DomesticForm: React.FC = ({ {/* 使用通用表单组件 */} - + diff --git a/src/pages/register/supplier/ForeignForm.tsx b/src/pages/register/supplier/ForeignForm.tsx index aea3b4c..4f8fbce 100644 --- a/src/pages/register/supplier/ForeignForm.tsx +++ b/src/pages/register/supplier/ForeignForm.tsx @@ -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 = ({ form, countdown, handleGetCaptcha, surveyQuestions }) => { + // 全球 + const [regionOptions, setRegionOptions] = useState([]); + useEffect(() => { + getregionInternational().then(res => { + if (res.code === 200) { + setRegionOptions(res.data); + } + }); + }, []); + return ( <> {/* 境外企业特有的基本信息部分 */} @@ -73,16 +85,11 @@ const ForeignForm: React.FC = ({ form, countdown, handleGetCap rules={[{ required: true, message: '请选择国家/地区' }]} > @@ -307,7 +314,7 @@ const ForeignForm: React.FC = ({ form, countdown, handleGetCap {/* 使用通用表单组件 */} - + diff --git a/src/pages/supplier/backend/supplierNews/index.tsx b/src/pages/supplier/backend/supplierNews/index.tsx index 53bbb24..88546e7 100644 --- a/src/pages/supplier/backend/supplierNews/index.tsx +++ b/src/pages/supplier/backend/supplierNews/index.tsx @@ -80,8 +80,8 @@ const supplierNews: React.FC = () => { }, { title: '业务类型', - dataIndex: 'type', - key: 'type', + dataIndex: 'typeCn', + key: 'typeCn', }, { title: '发送时间', diff --git a/src/pages/supplier/informationRetrieval/mySupplierInquiry/components/SupplierDetailModal.tsx b/src/pages/supplier/informationRetrieval/mySupplierInquiry/components/SupplierDetailModal.tsx index 570bf41..7bbd4d9 100644 --- a/src/pages/supplier/informationRetrieval/mySupplierInquiry/components/SupplierDetailModal.tsx +++ b/src/pages/supplier/informationRetrieval/mySupplierInquiry/components/SupplierDetailModal.tsx @@ -120,12 +120,7 @@ const SupplierAccessDetailModal: React.FC = ({ v key: "exitTime", align: "center", }, - { - title: "进入黑名单时间", - dataIndex: "blackTime", - key: "blackTime", - align: "center", - }, + ]; return ( = ({ v key: "exitTime", align: "center", }, - { - title: "进入黑名单时间", - dataIndex: "blackTime", - key: "blackTime", - align: "center", - }, + ]; return ( { diff --git a/src/servers/api/register.ts b/src/servers/api/register.ts index ad22ab2..82ff37d 100644 --- a/src/servers/api/register.ts +++ b/src/servers/api/register.ts @@ -20,4 +20,18 @@ export async function fetchSurveyQuestions(): Promise { + return request('/api/users'); +} + +export async function queryCurrent(): Promise { + return request('/api/currentUser'); +} + +export async function queryNotices(): Promise { + return request('/api/notices'); +} diff --git a/src/utils/Authorized.ts b/src/utils/Authorized.ts new file mode 100644 index 0000000..cd99bc5 --- /dev/null +++ b/src/utils/Authorized.ts @@ -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; diff --git a/src/utils/authority.ts b/src/utils/authority.ts new file mode 100644 index 0000000..ddf15ec --- /dev/null +++ b/src/utils/authority.ts @@ -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(); +}