diff --git a/config/config.ts b/config/config.ts index a89f9421cb512b376a20c8479db8b1daed250c35..a5ca999de6bf811a0ef3c629c84731b937063cd7 100644 --- a/config/config.ts +++ b/config/config.ts @@ -8,7 +8,7 @@ import routes from './routes' const { REACT_APP_ENV, UMI_ENV } = process.env; const isDev = UMI_ENV === 'dev'; -// const buildPublicPath = '{OPENANOLIS_TEST_LIB_FRONT_PATH}/'; +// const buildPublicPath = '{OPENANOLIS_TEST_LIB_TOOLS_SERVER}/'; const buildPublicPath = '/' const publicPath = isDev ? '/' : buildPublicPath; diff --git a/config/routes.ts b/config/routes.ts index 21c08d6bc10112acf3c3a47bad42b0e6c1ca606c..58aed84775c29015d47976072b489effa20f80e4 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -62,6 +62,22 @@ name: "sys", component: "./Sys", routes: [ + { + path: "approval", + name: "approval", + hideInMenu: true, + component: "./Sys/ApprovalAndRecord", + routes: [{ + path: "record", + name: "record", + hideInMenu: true, + component: "./Sys/ApprovalAndRecord/Record", + }, { + name: "approval", + hideInMenu: true, + component: "./Sys/ApprovalAndRecord/Approval", + }] + }, { path: "user", name: "user", @@ -85,6 +101,20 @@ } ] }, + { + path: "login", + name: "login", + hideInMenu: true, + layout: false, + component: "./Auth/Login", + }, + { + path: "regist", + name: "regist", + layout: false, + hideInMenu: true, + component: "./Auth/Regist" + }, { path: "*", redirect: "/outline", diff --git a/src/access.ts b/src/access.ts index cbfba9a3725c655a98ef483180c36cd0f06eacff..ac75b675d3417364f1961d4a87165006a3c95f6b 100644 --- a/src/access.ts +++ b/src/access.ts @@ -1,9 +1,21 @@ /** * @see https://umijs.org/zh-CN/plugins/plugin-access * */ + export default function access(initialState: { currentUser?: API.CurrentUser | undefined }) { const { currentUser } = initialState || {}; + + const isAdmin = () => currentUser && currentUser.role === "admin" + const isTester = () => currentUser && currentUser.role === "senior" + const isMember = () => currentUser && currentUser.role === "common" + return { - canAdmin: currentUser && currentUser.access === 'admin', + isAdmin, + isTester, + isMember, + + canMemberRole: () => ["admin", "senior", "common"].includes(currentUser?.role || ""), + canTester: () => ["admin", "senior",].includes(currentUser?.role || ""), + canSysAdmin: () => isAdmin() }; } diff --git a/src/app.tsx b/src/app.tsx index 2b6fd15addd2e7e767e86068bd055dfa01a7898a..5f2a539e7cd5394e2f5e4652d51a667fb2a9c16f 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -4,11 +4,12 @@ import type { RunTimeLayoutConfig } from 'umi'; import { history, Link } from 'umi'; import RightContent from '@/components/RightContent'; import Footer from '@/components/Footer'; -import { currentUser as queryCurrentUser } from './services/ant-design-pro/api'; import { BookOutlined, LinkOutlined } from '@ant-design/icons'; import menuItemRender from "@/components/MenuItemRender" import logo from "@/assets/logo.png" import { HeaderCls } from './components/Header/headerStyled'; +import { getUserInfo } from './pages/Auth/Login/services'; +import { request as requestInject } from "./request" const isDev = process.env.NODE_ENV === 'development'; const loginPath = '/user/login'; @@ -28,18 +29,18 @@ export async function getInitialState(): Promise<{ location: any }> { const fetchUserInfo = async () => { - try { - const msg = await queryCurrentUser(); - return msg.data; - } catch (error) { - history.push(loginPath); + const { code, msg, data } = await getUserInfo(); + if (code !== 200) { + /* 未登录跳转 */ + return null } - return undefined; + return data; }; + const data = await fetchUserInfo() return { fetchUserInfo, - currentUser: {}, + currentUser: data, settings: {}, location: null }; @@ -60,9 +61,9 @@ export const layout: RunTimeLayoutConfig = (props) => { const { location } = history; setInitialState({ ...initialState, location }) // 如果没有登录,重定向到 login - if (!initialState?.currentUser && location.pathname !== loginPath) { + /* if (!initialState?.currentUser && location.pathname !== loginPath) { history.push(loginPath); - } + } */ }, headerTheme: "dark", headerRender: (props, dom) => , @@ -88,3 +89,6 @@ export const layout: RunTimeLayoutConfig = (props) => { ...initialState?.settings, }; }; + + +export const request = requestInject \ No newline at end of file diff --git a/src/assets/auth/anolis_logo.svg b/src/assets/auth/anolis_logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..6e41f2183a7112185ac8186cf88cd345e3e06b6c --- /dev/null +++ b/src/assets/auth/anolis_logo.svg @@ -0,0 +1,13 @@ + + + logo1 + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/auth/bg.svg b/src/assets/auth/bg.svg new file mode 100644 index 0000000000000000000000000000000000000000..344093030f2ec1e55b443ca5a69cf7a2ebddcb8e --- /dev/null +++ b/src/assets/auth/bg.svg @@ -0,0 +1,18 @@ + + + bg + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/auth/bg0.png b/src/assets/auth/bg0.png new file mode 100644 index 0000000000000000000000000000000000000000..1929e1114ead040fe7d57ad6a01000faa518b128 Binary files /dev/null and b/src/assets/auth/bg0.png differ diff --git a/src/assets/rightContent/copy-fill-line.svg b/src/assets/rightContent/copy-fill-line.svg new file mode 100644 index 0000000000000000000000000000000000000000..a3134616bf3480b809e170f23d407e96371f0439 --- /dev/null +++ b/src/assets/rightContent/copy-fill-line.svg @@ -0,0 +1,16 @@ + + + copy-fill-line + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/rightContent/export-outlined.svg b/src/assets/rightContent/export-outlined.svg new file mode 100644 index 0000000000000000000000000000000000000000..38888816d089f4468c055d821d2f62c113476c6e --- /dev/null +++ b/src/assets/rightContent/export-outlined.svg @@ -0,0 +1,16 @@ + + + export-outlined + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/rightContent/refresh.svg b/src/assets/rightContent/refresh.svg new file mode 100644 index 0000000000000000000000000000000000000000..3a9b35b3c48be200bae821b7cef221a13a973c50 --- /dev/null +++ b/src/assets/rightContent/refresh.svg @@ -0,0 +1,14 @@ + + + refresh + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/rightContent/token_pass.svg b/src/assets/rightContent/token_pass.svg new file mode 100644 index 0000000000000000000000000000000000000000..b65505ac6355d438d5af04014f46371c3c4da724 --- /dev/null +++ b/src/assets/rightContent/token_pass.svg @@ -0,0 +1,15 @@ + + + 密码安全 + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/rightContent/user-avatar.svg b/src/assets/rightContent/user-avatar.svg new file mode 100644 index 0000000000000000000000000000000000000000..ac4cdb619d05903d136fc6f8e9280d5848e68f05 --- /dev/null +++ b/src/assets/rightContent/user-avatar.svg @@ -0,0 +1,17 @@ + + + user-avatar + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/rightContent/user_admin.svg b/src/assets/rightContent/user_admin.svg new file mode 100644 index 0000000000000000000000000000000000000000..8333e25a10a3a88493ea0bd14138319123ae12db --- /dev/null +++ b/src/assets/rightContent/user_admin.svg @@ -0,0 +1,16 @@ + + + 管理员 + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/rightContent/view-off.svg b/src/assets/rightContent/view-off.svg new file mode 100644 index 0000000000000000000000000000000000000000..f728df5df91c548b467fcfd28034c8fdfa3ba11b --- /dev/null +++ b/src/assets/rightContent/view-off.svg @@ -0,0 +1,15 @@ + + + view-off + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/rightContent/view.svg b/src/assets/rightContent/view.svg new file mode 100644 index 0000000000000000000000000000000000000000..8de165a658cb217b84caa5f519f8bda6a66e811d --- /dev/null +++ b/src/assets/rightContent/view.svg @@ -0,0 +1,15 @@ + + + view + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/Public/UserAvatarColumn.tsx b/src/components/Public/UserAvatarColumn.tsx new file mode 100644 index 0000000000000000000000000000000000000000..acb5c7de844400cc1c18e01a76a640bf00b17dd1 --- /dev/null +++ b/src/components/Public/UserAvatarColumn.tsx @@ -0,0 +1,20 @@ +import { Avatar, Typography, Space } from "antd" +import React from "react" + +type IProps = { + avatar?: string; + username?: string; +} + +const UserAvatarColumn: React.FC = (props) => { + const { avatar, username } = props + + return ( + + + {username} + + ) +} + +export default UserAvatarColumn \ No newline at end of file diff --git a/src/components/RightContent/AvatarDropdown.tsx b/src/components/RightContent/AvatarDropdown.tsx index 7b7cff7e69c523d573acaaa5e4ba5bb105e8f203..a22c74636625b972fe151cd086922c0cb4eb907d 100644 --- a/src/components/RightContent/AvatarDropdown.tsx +++ b/src/components/RightContent/AvatarDropdown.tsx @@ -1,105 +1,153 @@ import React, { useCallback } from 'react'; -import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons'; -import { Avatar, Menu, Spin } from 'antd'; -import { history, useModel } from 'umi'; +import { Avatar, Menu, Spin, Space, Typography, Row } from 'antd'; +import { history, useModel, useAccess } from 'umi'; import { stringify } from 'querystring'; import HeaderDropdown from '../HeaderDropdown'; import styles from './index.less'; -import { outLogin } from '@/services/ant-design-pro/api'; import type { MenuInfo } from 'rc-menu/lib/interface'; +import { userRoleMap } from '@/utils'; +import Token from "./Token" + +import { ReactComponent as TokenPass } from "@/assets/rightContent/token_pass.svg" +import { ReactComponent as UserAvatar } from "@/assets/rightContent/token_pass.svg" +import { ReactComponent as UserAdmin } from "@/assets/rightContent/user_admin.svg" +import { ReactComponent as LogoutOutlined } from "@/assets/rightContent/export-outlined.svg" +import { userApplyManage } from '@/services'; + +import ConfirmApply from './ConfirmApply'; export type GlobalHeaderRightProps = { - menu?: boolean; + menu?: boolean; }; /** * 退出登录,并且将当前的 url 保存 */ const loginOut = async () => { - await outLogin(); - const { query = {}, pathname } = history.location; - const { redirect } = query; - // Note: There may be security issues, please note - if (window.location.pathname !== '/user/login' && !redirect) { - history.replace({ - pathname: '/user/login', - search: stringify({ - redirect: pathname, - }), - }); - } + const { query = {}, pathname } = history.location; + const { redirect } = query; + localStorage.setItem("auth_token", "") + // Note: There may be security issues, please note + if (window.location.pathname !== '/login' && !redirect) { + history.replace({ + pathname: '/login', + search: stringify({ + redirect: pathname, + }), + }); + } }; const AvatarDropdown: React.FC = ({ menu }) => { - return <> - const { initialState, setInitialState } = useModel('@@initialState'); - - const onMenuClick = useCallback( - (event: MenuInfo) => { - const { key } = event; - if (key === 'logout') { - setInitialState((s) => ({ ...s, currentUser: undefined })); - loginOut(); - return; - } - history.push(`/account/${key}`); - }, - [setInitialState], - ); - - const loading = ( - - - - ); - - - if (!initialState) { - return loading; - } - - const { currentUser } = initialState; - - if (!currentUser || !currentUser.name) { - return loading; - } - - const menuHeaderDropdown = ( - - {menu && ( - - - 个人中心 - - )} - {menu && ( - - - 个人设置 - - )} - {menu && } - - - - 退出登录 - - - ); - return ( - - - - {currentUser.name} - - - ); + const { initialState, setInitialState } = useModel('@@initialState'); + + const access = useAccess() + const confirmApplyRef = React.useRef(null) as any + + const onMenuClick = useCallback( + (event: MenuInfo) => { + const { key } = event; + if (key === 'logout') { + setInitialState((s: any) => ({ ...s, currentUser: undefined })); + loginOut(); + return; + } + if (key === "apply") { + confirmApplyRef.current?.show() + } + }, + [setInitialState], + ); + + const loading = ( + + + + ); + + + if (!initialState) { + return loading; + } + + const { currentUser } = initialState; + + if (!currentUser || !currentUser.nick_name) { + return loading; + } + + const menuHeaderDropdown = ( + + + + +
+ + {currentUser?.nick_name} + + + {userRoleMap.get(currentUser?.role || "")} + +
+
+ + + + + 邮箱 + {currentUser?.email} + + + + + + + Token + + + +
+ + { + access.isMember() && + <> + {menu && } + + + + 申请测试管理员 + + + + } + + {menu && } + + + + + 退出登录 + + +
+ ); + return ( + <> + + + + {currentUser.nick_name} + + + + + ); }; export default AvatarDropdown; diff --git a/src/components/RightContent/ConfirmApply.tsx b/src/components/RightContent/ConfirmApply.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5482014cfc5deee898aa131f8a4d830ddda8b63a --- /dev/null +++ b/src/components/RightContent/ConfirmApply.tsx @@ -0,0 +1,64 @@ +import React from "react" +import { Modal, Space, Button, Typography } from "antd" +import { ExclamationCircleOutlined } from "@ant-design/icons" + +type IProps = { + onOk: () => void; + onCancel?: () => void; +} + +type IRefs = { + [k: string]: any +} + +const ReactComponent: React.ForwardRefRenderFunction = (props, ref) => { + const { onOk } = props + + const [visible, setVisible] = React.useState(false) + const [loading, setLoading] = React.useState(false) + const [source, setSource] = React.useState(undefined) + + React.useImperativeHandle(ref, () => ({ + show(_: any) { + setSource(_) + setVisible(true) + } + })) + + const handleCancel = () => { + setVisible(false) + setLoading(false) + setSource(undefined) + } + + const handleOk = async () => { + if (loading) return + setLoading(true) + onOk() + handleCancel() + } + + return ( + + + + + } + onCancel={handleCancel} + > + + + + + 确认申请测试管理员权限吗? + + + ) +} + +export default React.forwardRef(ReactComponent) \ No newline at end of file diff --git a/src/components/RightContent/Token.tsx b/src/components/RightContent/Token.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3ceb728da1b46894f0b16b5c68ef88b0d19c1109 --- /dev/null +++ b/src/components/RightContent/Token.tsx @@ -0,0 +1,79 @@ +import { Space, Typography } from "antd" +import React from "react" +import { useModel } from "umi" +import { resetToken } from "@/services" +import styled from "styled-components" + +import { ReactComponent as View } from "@/assets/rightContent/view.svg" +import { ReactComponent as Refresh } from "@/assets/rightContent/refresh.svg" +import { ReactComponent as Off } from "@/assets/rightContent/view-off.svg" +import { ReactComponent as Copy } from "@/assets/rightContent/copy-fill-line.svg" + + +const PointerSpan = styled.span` + cursor: pointer; + display: flex; + flex-direction: row; + align-items: center; + gap: 2px; +` + +const Token: React.FC = () => { + const { initialState, setInitialState } = useModel("@@initialState") + + const [show, setShow] = React.useState(false) + const [loading, setLoading] = React.useState(false) + + const handleReset = async () => { + if (loading) return + setLoading(true) + const { code } = await resetToken() + setLoading(false) + if (code !== 200) { + return + } + if (initialState && initialState.fetchUserInfo) { + const data = await initialState?.fetchUserInfo() + setInitialState((p: any) => ({ ...initialState, currentUser: data })) + } + } + + const handleCopy = () => { + + } + + return ( + <> + { + show ? + + {initialState?.currentUser?.token} + : + ************************************* + } + + + + 复制 + + { + show ? + setShow(false)}> + + 隐藏 + : + setShow(true)}> + + 查看 + + } + + + 重置 + + + + ) +} + +export default Token \ No newline at end of file diff --git a/src/components/RightContent/index.less b/src/components/RightContent/index.less index 486e80c9be2042e0c5344c28b909c83468ab58f6..6833f6dce1f1951c09eec1813564b254a1bcbfb9 100644 --- a/src/components/RightContent/index.less +++ b/src/components/RightContent/index.less @@ -3,82 +3,85 @@ @pro-header-hover-bg: rgba(0, 0, 0, 0.025); .menu { - :global(.anticon) { - margin-right: 8px; - } - :global(.ant-dropdown-menu-item) { - min-width: 160px; - } + :global(.anticon) { + margin-right: 8px; + } + :global(.ant-dropdown-menu-item) { + min-width: 160px; + } } .right { - display: flex; - float: right; - height: 48px; - margin-left: auto; - overflow: hidden; - .action { display: flex; - align-items: center; + float: right; height: 48px; - padding: 0 12px; - cursor: pointer; - transition: all 0.3s; - > span { - vertical-align: middle; - } - &:hover { - background: @pro-header-hover-bg; + margin-left: auto; + overflow: hidden; + .action { + display: flex; + align-items: center; + height: 48px; + 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; + } } - &:global(.opened) { - background: @pro-header-hover-bg; - } - } - .search { - padding: 0 12px; - &:hover { - background: transparent; + .search { + padding: 0 12px; + &:hover { + background: transparent; + } } - } - .account { - .avatar { - margin-right: 8px; - color: @primary-color; - vertical-align: top; - background: rgba(255, 255, 255, 0.85); + .account { + .avatar { + margin-right: 8px; + color: @primary-color; + vertical-align: top; + background: rgba(255, 255, 255, 0.85); + } + .name { + color: #fff!important; + } } - } } .dark { - .action { - &:hover { - background: #252a3d; - } - &:global(.opened) { - background: #252a3d; + .action { + &:hover { + background: #252a3d; + } + &:global(.opened) { + background: #252a3d; + } } - } } @media only screen and (max-width: @screen-md) { - :global(.ant-divider-vertical) { - vertical-align: unset; - } - .name { - display: none; - } - .right { - position: absolute; - top: 0; - right: 12px; - .account { - .avatar { - margin-right: 0; - } + :global(.ant-divider-vertical) { + vertical-align: unset; } - .search { - display: none; + .name { + display: none; + } + .right { + position: absolute; + top: 0; + right: 12px; + .account { + .avatar { + margin-right: 0; + } + } + .search { + display: none; + } } - } } diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index d9528a8e4f220daf56c87d342bac84b89f222caf..f985b7625634de00419e9d82901c700ee80540a7 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -12,14 +12,18 @@ export default { "menu.sys.user": "用户管理", "menu.sys.tag": "标签管理", "menu.sys.kit": "测试套管理", + "menu.sys.approval": "审批管理", + "menu.sys.approval.approval": "待审批", + "menu.sys.approval.record": "审批记录", + + "menu.login": "用户登录", + "menu.regist": "注册账号", 'menu.welcome': '欢迎', 'menu.more-blocks': '更多区块', 'menu.home': '首页', 'menu.admin': '管理页', 'menu.admin.sub-page': '二级管理页', - 'menu.login': '登录', - 'menu.register': '注册', 'menu.register-result': '注册结果', 'menu.dashboard': 'Dashboard', 'menu.dashboard.analysis': '分析页', diff --git a/src/pages/Auth/Login/index.tsx b/src/pages/Auth/Login/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e4dd0fac6e728bdd1c2da3ccc022338f1e1db5e5 --- /dev/null +++ b/src/pages/Auth/Login/index.tsx @@ -0,0 +1,111 @@ +import React from "react" +import AuthLayout from "../components/Layout" +import { Form, Row, Typography, Input, Space, Button, message } from "antd" +import { Link, history, useModel } from "umi" +import { ReactComponent as AnolisLogo } from "@/assets/auth/anolis_logo.svg" +import { login } from "@/pages/Auth/services" +import { BasicForm } from "@/pages/Auth/styled" + +const LoginPage: React.FC = () => { + const [form] = Form.useForm() + + const { setInitialState, initialState } = useModel("@@initialState") + const [loading, setLoading] = React.useState(false) + + const handleLogin = () => { + if (loading) return + form.validateFields() + .then(async (values) => { + setLoading(true) + const { data, msg, code } = await login(values) + setLoading(false) + if (code !== 200) { + return message.error(msg) + } + localStorage.setItem("auth_token", data) + if (initialState && initialState.fetchUserInfo) { + const userInfo = await initialState.fetchUserInfo() + setInitialState((p: any) => ({ ...p, currentUser: userInfo })) + history.push(`/`) + } + }) + .catch(error => { + console.log(error) + setLoading(false) + }) + } + + return ( + + + + 登录 + + + + + 用户名 + } + /> + + + 密码 + } + /> + + + + + 忘记密码? + + + + + + + + 还没有账号,去 + + 注册 + + + + + + + ) +} + +export default LoginPage \ No newline at end of file diff --git a/src/pages/Auth/Login/services.ts b/src/pages/Auth/Login/services.ts new file mode 100644 index 0000000000000000000000000000000000000000..16245feb53a67fe0b2835be47383366e0611332d --- /dev/null +++ b/src/pages/Auth/Login/services.ts @@ -0,0 +1,42 @@ +import { request } from "umi" + +declare namespace User { + /* + ● name: 注册的昵称 + ● password:密码,前端md5加密后传输 + */ + type loginData = { + name: string; + password: string; + } + /* + ● nick_name: 昵称,唯一字段,重复会报错 + ● email:邮箱,唯一字段,重复会报错 + ● password:密码,前端md5加密后传输 + */ + type registData = { + nickname: string; + email: string; + password: string; + } +} +/* 用户登录 */ +export const login = async (data: User.loginData) => { + return request(`/api/user/login`, { data, method: "post" }) +} +/* 退出登录 */ +export const logout = async () => { + return request(``) +} + +/* 用户注册 */ +export const register = async (data: User.registData) => { + return request(`/api/user/register`, { data, method: "post" }) +} + +/* 获取当前用户信息 */ +export const getUserInfo = async () => { + return request(`/api/user/info`, { method: "get" }) +} + + diff --git a/src/pages/Auth/Regist/index.tsx b/src/pages/Auth/Regist/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..497d0c5314794848749d12c07e230170d680652f --- /dev/null +++ b/src/pages/Auth/Regist/index.tsx @@ -0,0 +1,163 @@ +import React from "react" +import AuthLayout from "../components/Layout" +import { Form, Row, Typography, Input, Space, Button, Checkbox, message } from "antd" +import { Link, history } from "umi" +import { ReactComponent as AnolisLogo } from "@/assets/auth/anolis_logo.svg" +import { register } from "@/pages/Auth/services" +import { BasicForm } from "@/pages/Auth/styled" + +const RigistPage: React.FC = () => { + const [form] = Form.useForm() + const [loading, setLoading] = React.useState(false) + + const handleRegist = () => { + if (loading) return + form.validateFields() + .then(async (values) => { + setLoading(true) + const { msg, code } = await register(values) + setLoading(false) + if (code !== 200) { + return message.error(msg) + } + history.push(`/login`) + }) + .catch(error => { + console.log(error) + setLoading(false) + }) + } + + return ( + + + + 注册TestLib账号 + + + + + 用户名 + } + /> + + + 邮箱 + } + /> + + + 密码 + } + /> + + ({ + validator(_, value) { + if (!value || getFieldValue('password') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error('两次密码输入不一致,请检查后重新输入!')); + }, + }), + ]} + > + 确认密码 + } + /> + + + value ? Promise.resolve() : Promise.reject(new Error('请先阅读协议!')), + }, + ]} + > + + 我已阅读并接受 + + 《Testlib隐私协议》 + + + + + + + + + + + 已有账号,去 + + 登录 + + + + + + + ) +} + +export default RigistPage \ No newline at end of file diff --git a/src/pages/Auth/components/Layout.tsx b/src/pages/Auth/components/Layout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c762cc5d67ec11cff5fd3daf2b392c32be2ad63f --- /dev/null +++ b/src/pages/Auth/components/Layout.tsx @@ -0,0 +1,72 @@ +import React from "react" +import styled from "styled-components" + +import auth_bg from "@/assets/auth/bg.svg" +import { Avatar, Row, Space, Typography } from "antd" +import bg from "@/assets/auth/bg0.png" + +const Container = styled.div` + width: 100%; + height: 100%; + padding: 3%; + position: relative; + background: url(${bg}) no-repeat left center / 100% 100%; +` + +const Wrapper = styled.div` + width: ${472 + 708}px; + height: 600px; + margin: 0 auto; + display: flex; + border-radius: 10px 0 0 10px; + overflow: hidden; +` + +const Left = styled.div` + width: 472px; + height: 100%; + background: url(${auth_bg}) no-repeat left center /100% 100%; + padding: 40px; +` + +const Right = styled.div` + width: 708px; + height: 100%; + background-color: #fff; +` + +const LogoAvatar = styled(Avatar)` + img { + width: 40px !important; + height: 40px!important; + margin: 13px auto 0; + } +` + +const AuthLayout: React.FC = ({ children }) => { + + return ( + + + + + + + + TestLib + + + 测试管理系统 + + + {children} + + + ) +} + +export default AuthLayout \ No newline at end of file diff --git a/src/pages/Auth/services.ts b/src/pages/Auth/services.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e07058991a4c48251213d7abba678ca0770dee2 --- /dev/null +++ b/src/pages/Auth/services.ts @@ -0,0 +1,30 @@ +import { request } from "umi" + +type LoginOrRigistData = { + nick_name?: string; + email?: string; + password: string; +} + +/* + API POST /api/user/login + 请求参数: + ● name: 注册的昵称 + ● password:密码 +*/ + +export const login = async (data?: LoginOrRigistData) => { + return request(`/api/user/login`, { method: "post", data }) +} + +/* + API POST /api/user/register + 请求参数: + ● nick_name: 昵称,唯一字段,重复会报错 + ● email:邮箱,唯一字段,重复会报错 + ● password:密码 +*/ + +export const register = async (data?: LoginOrRigistData) => { + return request(`/api/user/register`, { method: "post", data }) +} \ No newline at end of file diff --git a/src/pages/Auth/styled.tsx b/src/pages/Auth/styled.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a527dcd443eef3056f9886eaf756dfad557c6072 --- /dev/null +++ b/src/pages/Auth/styled.tsx @@ -0,0 +1,12 @@ +import { CustomForm } from "@/components/CustomStyled" +import styled from "styled-components" + +export const BasicForm = styled(CustomForm)` + .ant-input-prefix { + margin-right: 20px + } + + .ant-typography { + width: 56px; + } +` \ No newline at end of file diff --git a/src/pages/Suite/components/FilterForm/index.tsx b/src/pages/Suite/components/FilterForm/index.tsx index 317f2a86f7fb96607f60dc838e7d69aeab70359a..c02574715a179158978599b41dbe35472e654705 100644 --- a/src/pages/Suite/components/FilterForm/index.tsx +++ b/src/pages/Suite/components/FilterForm/index.tsx @@ -67,8 +67,19 @@ const columns = [ } ] -const FilterForm: React.FC = () => { - const [filter, setFilter] = React.useState([{ name: "test_type", value: "" }]) +type IProps = { + onChange?: (val: any) => void; + columns?: any[]; + defaultValue?: any[]; + onClearn?: () => void; + rowCol?: number; + itemCol?: number; +} + +const FilterForm: React.FC = (props) => { + const { onChange } = props + + const [filter, setFilter] = React.useState([{ name: "test_type", value: null }]) const hasFields = React.useMemo(() => { return filter.map((field: any) => field.name); @@ -105,14 +116,12 @@ const FilterForm: React.FC = () => { } const renderRightElement = (col: any, index: number) => { - console.log(col, index) const idx = columns.findIndex((c: any) => c.name === col.name) const { list, children } = columns[idx] const rowProps: any = { value: col.value, style: { width: "100%" }, - size: "small", onChange: (evt: any) => handleFieldChange(evt, index) } @@ -127,13 +136,22 @@ const FilterForm: React.FC = () => { return React.cloneElement(children, rowProps) } - console.log(leftSelectOption, filter) + React.useEffect(() => { + onChange && + onChange( + filter.reduce((p: any, c: any) => { + const [field, val] = Object.values(c) + p[field] = val + return p + }, {}) + ) + }, [filter]) const handleAddField = () => { const hasFields = leftSelectOption.filter(({ disabled }) => !disabled) const field = hasFields[0] const { value }: any = field - setFilter(p => p.concat({ name: value, value: "" })) + setFilter(p => p.concat({ name: value, value: null })) } return ( @@ -156,7 +174,6 @@ const FilterForm: React.FC = () => { handleUserRoleChange(val, row)} + dropdownStyle={{ width: 200 }} + dropdownMatchSelectWidth={false} > - + + + 测试人员 + 拥有测试权限 + + + + + 普通用户 + 仅有管理自己的数据权限 + + ) } @@ -74,7 +105,6 @@ const TableList: React.FC = (props) => { intl.formatMessage({ id: `menu.sys.${props?.route?.name}` }) } - = (props) => { } }} /> - ) } diff --git a/src/pages/Sys/Users/services.ts b/src/pages/Sys/Users/services.ts index dc9b6e334cb43508e36ef68c9bd890b597989445..5e0644bb6349ad92dc40ff35878bc8e4fe5c64c8 100644 --- a/src/pages/Sys/Users/services.ts +++ b/src/pages/Sys/Users/services.ts @@ -1,5 +1,52 @@ import { request } from "umi"; -export const queryList = async (params: any) => { - return request(`/api/`, { params }) +/* + ○ page_num: 分页页数、默认为1 + ○ page_size:分页大小,默认为10 + ○ name:昵称,模糊搜索 +*/ + +type ListProps = { + page_num?: number; + page_size?: number; + name?: string; +} + +export const queryList = async (params: ListProps) => { + return request(`/api/user/user-list`, { params, method: "get" }) +} +/* + ○ avatar_url: 用户头像的地址 + ○ name:用户名称,和昵称是两个字段,可空、可重复 +*/ +type Update = { + avatar_url?: string; + name?: string; +} +export const updateUser = async (data: Update) => { + return request(`/api/user/update`, { data, method: "post" }) +} + +type UpdateUserRole = { + user_id: number; + role: string; + method: string; +} +export const updateUserRole = async (data: UpdateUserRole) => { + return request(`/api/user/update-role`, { data, method: "post" }) +} + +/* 主动添加普通用户为管理员 */ +/* user_id: 用户id */ +type ModifyUserToManage = { + user_id: number +} + +export const modifyUserToManage = async (data: ModifyUserToManage) => { + return request(`/api/user/promote-role`, { data, method: "post" }) +} + +/* 删除用户 */ +export const deleteUser = async (params: ModifyUserToManage) => { + return request(`/api/user`, { params, method: "delete" }) } \ No newline at end of file diff --git a/src/pages/Sys/index.tsx b/src/pages/Sys/index.tsx index 7dccede4a5d9d563961d27c377413d3f58aba0e0..4b1652c51c68b25048f50acb5caac7b304aa3b31 100644 --- a/src/pages/Sys/index.tsx +++ b/src/pages/Sys/index.tsx @@ -11,12 +11,19 @@ const SysContainer: React.FC = (props) => { const intl = useIntl() const { pathname } = useLocation() + const currentPath = React.useMemo(() => { + for (const child of route.children) + if (~pathname.indexOf(child.path)) + return child.path + return pathname + }, [pathname, route]) + return ( - + { diff --git a/src/request.tsx b/src/request.tsx new file mode 100644 index 0000000000000000000000000000000000000000..911cf7a1b2a9007f693bdb97053e00144254b51d --- /dev/null +++ b/src/request.tsx @@ -0,0 +1,68 @@ +const codeMessage = { + 200: '服务器成功返回请求的数据。', + 201: '新建或修改数据成功。', + 202: '一个请求已经进入后台排队(异步任务)。', + 204: '删除数据成功。', + 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。', + 401: '用户没有权限(令牌、用户名、密码错误)。', + 403: '用户得到授权,但是访问是被禁止的。', + 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。', + 405: '请求方法不被允许。', + 406: '请求的格式不可得。', + 410: '请求的资源被永久删除,且不会再得到的。', + 422: '当创建一个对象时,发生一个验证错误。', + 500: '服务器发生错误,请检查服务器。', + 502: '网关错误。', + 503: '服务不可用,服务器暂时过载或维护。', + 504: '网关超时。', +}; + +// 全局请求 +const requestInterceptor = (url: string, options: any) => { + return { + url, // 此处可以添加域名前缀 + options: { + ...options, + headers: { + ...options.headers, + "X-Sanic-Token": localStorage.getItem("auth_token"), + // "testLib": localStorage.getItem("auth_token"), + }, + }, + }; +}; + +// 全局相应拦截 +const responseInterceptor = (response: any, options: any) => { + return response; +}; + +const errorHandler = (error: any) => { + const { response } = error; + if (response && response.status) { + const errorText = codeMessage[response.status] || response.statusText; + const { status, url } = response; + console.log(`请求错误 ${status}: ${url}`); + // notification.error({ + // message: `请求错误 ${status}: ${url}`, + // description: errorText, + // }); + } + + if (!response) { + // notification.error({ + // description: '您的网络发生异常,无法连接服务器', + // message: '网络异常', + // }); + } + throw error; +}; + +export const request = { + errorConfig: {}, + middlewares: [], + // 异常处理 + errorHandler, + requestInterceptors: [requestInterceptor], + responseInterceptors: [responseInterceptor], +}; diff --git a/src/services/ant-design-pro/typings.d.ts b/src/services/ant-design-pro/typings.d.ts index 13e5a680c4d403edfcbe14c0c7647b21a17a7e41..7eb7b654b9a498fc7900ef9e18f9a3429c0ab75f 100644 --- a/src/services/ant-design-pro/typings.d.ts +++ b/src/services/ant-design-pro/typings.d.ts @@ -4,6 +4,9 @@ declare namespace API { type CurrentUser = { name?: string; + nick_name?: string; + avatar_url?: string; + token?: string; avatar?: string; userid?: string; email?: string; @@ -15,6 +18,7 @@ declare namespace API { unreadCount?: number; country?: string; access?: string; + role?: string; geographic?: { province?: { label?: string; key?: string }; city?: { label?: string; key?: string }; diff --git a/src/services/index.ts b/src/services/index.ts index 4c6cae37ed5937cc55b0b2c50b05ef3b2a90f414..9ccf1fea85a522f22ae6d1c8d4750700fa9957b9 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -2,4 +2,13 @@ import { request } from "umi"; export const uploadOss = (data: any) => { return request(`/api/oss/image`, { data, method: "post" }) +} + +/* 重置/更新用户的通用Token */ +export const resetToken = () => { + return request(`/api/user/token`) +} + +export const userApplyManage = () => { + return request(`/api/user/apply`, { method: "post" }) } \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index 0cf35e5d2e86611cd7f3757a4ba382c8c837595c..ca3a26fff0054fff38eda3f2f58cad74e05f2787 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -67,6 +67,13 @@ const setFormFieldsValue = (form: any, newObj: object) => { form.setFieldsValue({ ...valuesClone, ...newObj }) } +// admin super common +export const userRoleMap = new Map([ + ["admin", "系统管理员"], + ["senior", "测试人员"], + ["common", "普通用户"], +]) + export { requestFn, requestCodeMessage,