初始化
This commit is contained in:
38
src/App.css
Normal file
38
src/App.css
Normal file
@ -0,0 +1,38 @@
|
||||
/*.App {*/
|
||||
/* text-align: center;*/
|
||||
/*}*/
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
39
src/App.jsx
Normal file
39
src/App.jsx
Normal file
@ -0,0 +1,39 @@
|
||||
import './App.css';
|
||||
import {ConfigProvider} from "antd";
|
||||
import locale from 'antd/locale/zh_CN';
|
||||
import {useEffect, useState} from "react";
|
||||
import {BrowserRouter as Router} from "react-router-dom";
|
||||
import Routers from "./routes";
|
||||
|
||||
|
||||
function App() {
|
||||
|
||||
const [height, setHeight] = useState(window.innerHeight)
|
||||
|
||||
useEffect(() => {
|
||||
setHeight(window.innerHeight)
|
||||
window.addEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const handleResize = () => {
|
||||
setHeight(window.innerHeight)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: '#980000'
|
||||
}
|
||||
}}
|
||||
locale={locale}>
|
||||
<div className="App" style={{height: height}}>
|
||||
<Router>
|
||||
<Routers/>
|
||||
</Router>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
8
src/App.test.js
Normal file
8
src/App.test.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
26
src/component/CardDiv/CardDiv.jsx
Normal file
26
src/component/CardDiv/CardDiv.jsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import {theme} from "antd";
|
||||
|
||||
const CardDiv = props => {
|
||||
|
||||
const {
|
||||
token: {colorBgContainer, borderRadiusLG, colorBgBase},
|
||||
} = theme.useToken();
|
||||
|
||||
return (
|
||||
<div className={'card-div'}
|
||||
style={{
|
||||
padding: 28,
|
||||
background: colorBgContainer,
|
||||
borderRadius: borderRadiusLG,
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
}}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CardDiv.propTypes = {};
|
||||
|
||||
export default CardDiv;
|
||||
3
src/component/CardDiv/card-div.css
Normal file
3
src/component/CardDiv/card-div.css
Normal file
@ -0,0 +1,3 @@
|
||||
div {
|
||||
|
||||
}
|
||||
90
src/component/Header/ChangePasswordModal.jsx
Normal file
90
src/component/Header/ChangePasswordModal.jsx
Normal file
@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, Flex, Form, Input, Modal, Typography} from "antd";
|
||||
|
||||
const ChangePasswordModal = props => {
|
||||
const formLayout = {
|
||||
labelCol: {span: 4},
|
||||
wrapperCol: {span: 20},
|
||||
};
|
||||
const {commonAxios, messageApi, open, setOpen, closable} = props;
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const onSubmit = (values) => {
|
||||
const passwordConfirmCheck = values.newPassword === values.confirmNewPassword;
|
||||
if (!passwordConfirmCheck) {
|
||||
messageApi.error('两次输入的密码不一致,请重新输入');
|
||||
return;
|
||||
}
|
||||
const changeRequest = {
|
||||
originalPassword: values.originalPassword,
|
||||
newPassword: values.newPassword,
|
||||
}
|
||||
commonAxios.put('/api/auth/change/password', changeRequest)
|
||||
.then(response => {
|
||||
let result = response.data.success;
|
||||
if (result) {
|
||||
messageApi.success('修改密码成功');
|
||||
setOpen(false);
|
||||
form.resetFields();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false);
|
||||
form.resetFields();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
open={open}
|
||||
title="修改密码"
|
||||
onCancel={onCancel}
|
||||
closable={closable}
|
||||
maskClosable={closable}
|
||||
footer={
|
||||
<Flex justify={"end"} align={"center"} gap={"middle"}>
|
||||
<Button disabled={!closable} onClick={onCancel}>取消</Button>
|
||||
<Button type={"primary"} onClick={() => form.submit()}>确认</Button>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
{!closable ? <Typography.Text level={5}
|
||||
style={{color: "red"}}>您的密码已过期,请修改密码!</Typography.Text> : <></>}
|
||||
<Form
|
||||
form={form}
|
||||
{...formLayout}
|
||||
style={{marginTop: 20}}
|
||||
onFinish={onSubmit}
|
||||
>
|
||||
<Form.Item label={'旧密码'} name='originalPassword'
|
||||
rules={[{required: true, message: '请输入旧密码'}]}>
|
||||
<Input type={'password'}/>
|
||||
</Form.Item>
|
||||
<Form.Item label={'新密码'} name='newPassword'
|
||||
rules={[{required: true, message: '请输入新密码'}]}>
|
||||
<Input type={'password'}/>
|
||||
</Form.Item>
|
||||
<Form.Item label={'确认密码'} name='confirmNewPassword'
|
||||
rules={[{required: true, message: '请再次输入新密码'}]}>
|
||||
<Input type={'password'}/>
|
||||
</Form.Item>
|
||||
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
;
|
||||
|
||||
ChangePasswordModal.propTypes = {
|
||||
commonAxios: PropTypes.func.isRequired,
|
||||
messageApi: PropTypes.object.isRequired,
|
||||
open: PropTypes.bool.isRequired,
|
||||
setOpen: PropTypes.func.isRequired,
|
||||
closable: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default ChangePasswordModal;
|
||||
104
src/component/Header/LayoutHeader.jsx
Normal file
104
src/component/Header/LayoutHeader.jsx
Normal file
@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
import {Badge, Button, Dropdown, Flex, Layout, Modal, Space, theme} from "antd";
|
||||
import {DownOutlined, KeyOutlined, LogoutOutlined, MailOutlined, UserOutlined} from "@ant-design/icons";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import ChangePasswordModal from "./ChangePasswordModal";
|
||||
|
||||
function LayoutHeader(props) {
|
||||
const {
|
||||
token: {colorBgContainer, borderRadiusLG, colorBgBase},
|
||||
} = theme.useToken();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {profile, commonAxios, messageApi} = props;
|
||||
const [changePasswordModalOpen, setChangePasswordModalOpen] = React.useState(false);
|
||||
|
||||
const logout = () => {
|
||||
Modal.confirm({
|
||||
title: '确认退出登录吗?',
|
||||
onOk: () => {
|
||||
commonAxios.post('/api/auth/logout').then(res => {
|
||||
localStorage.removeItem('token');
|
||||
navigate('/auth/login');
|
||||
})
|
||||
},
|
||||
onCancel: () => {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const accountItems = [
|
||||
{
|
||||
label: (
|
||||
<div onClick={() => setChangePasswordModalOpen(true)}>修改密码</div>
|
||||
),
|
||||
icon: <KeyOutlined/>,
|
||||
key: 'changePassword',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
label: <div onClick={logout}>退出登录</div>,
|
||||
key: 'logout',
|
||||
icon: <LogoutOutlined/>,
|
||||
danger: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ChangePasswordModal commonAxios={commonAxios} messageApi={messageApi} open={changePasswordModalOpen}
|
||||
setOpen={setChangePasswordModalOpen} closable={true}/>
|
||||
<Layout.Header
|
||||
style={{
|
||||
height: '64px',
|
||||
background: colorBgBase
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<a href={'/overview'} style={{height: '100%', color: 'black'}}>
|
||||
<Flex justify={'flex-start'} style={{height: '100%'}} align={"center"}>
|
||||
|
||||
<img style={{width: 'auto', height: '80%'}} src={'/hrbnu_logo.png'}
|
||||
alt={'哈师大logo'}/>
|
||||
<h2 style={{marginLeft: '20px'}}>工作量精算管家</h2>
|
||||
|
||||
</Flex>
|
||||
</a>
|
||||
<Flex justify={"flex-start"} align={"center"} gap={"large"}>
|
||||
<a hidden>
|
||||
<Badge count={0}>
|
||||
<MailOutlined style={{fontSize: '15px'}}/>
|
||||
</Badge>
|
||||
</a>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: accountItems,
|
||||
}}
|
||||
>
|
||||
<Button type={'text'}>
|
||||
<Space>
|
||||
<UserOutlined/>
|
||||
{profile.staffNumber}{profile.name}
|
||||
<DownOutlined/>
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Flex>
|
||||
</div>
|
||||
</Layout.Header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutHeader;
|
||||
111
src/component/Header/MobileHeader.jsx
Normal file
111
src/component/Header/MobileHeader.jsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import {Button, Col, Dropdown, Flex, Layout, Modal, Row, Space, theme} from "antd";
|
||||
import {KeyOutlined, LogoutOutlined, MenuOutlined} from "@ant-design/icons";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import ChangePasswordModal from "./ChangePasswordModal";
|
||||
|
||||
const MobileHeader = (props) => {
|
||||
|
||||
const {
|
||||
token: {colorBgContainer, borderRadiusLG, colorBgBase},
|
||||
} = theme.useToken();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const logout = () => {
|
||||
Modal.confirm({
|
||||
title: '确认退出登录吗?',
|
||||
onOk: () => {
|
||||
commonAxios.post('/api/auth/logout').then(res => {
|
||||
localStorage.removeItem('token');
|
||||
navigate('/auth/login');
|
||||
})
|
||||
},
|
||||
onCancel: () => {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const {profile, commonAxios, messageApi} = props;
|
||||
const [changePasswordModalOpen, setChangePasswordModalOpen] = React.useState(false);
|
||||
|
||||
|
||||
const mobileAccountItems = [
|
||||
{
|
||||
label: (
|
||||
<div>{profile.name} ({profile.staffNumber})</div>
|
||||
),
|
||||
key: 'user',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
// {
|
||||
// label: (
|
||||
// <Badge count={5}>
|
||||
// 消息中心
|
||||
// </Badge>
|
||||
// ),
|
||||
// icon: <MailOutlined/>,
|
||||
// key: 'message',
|
||||
// },
|
||||
{
|
||||
label: (
|
||||
<div onClick={() => setChangePasswordModalOpen(true)}>修改密码</div>
|
||||
),
|
||||
icon: <KeyOutlined/>,
|
||||
key: 'changePassword',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
label: <div onClick={logout}>退出登录</div>,
|
||||
key: 'logout',
|
||||
icon: <LogoutOutlined/>,
|
||||
danger: true,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div>
|
||||
<ChangePasswordModal commonAxios={commonAxios} messageApi={messageApi} open={changePasswordModalOpen}
|
||||
setOpen={setChangePasswordModalOpen} closable={true}/>
|
||||
<Layout.Header style={{background: colorBgBase}}>
|
||||
<Row>
|
||||
<Col span={2}></Col>
|
||||
<Col span={20}>
|
||||
<a
|
||||
href={'/overview'}
|
||||
style={{
|
||||
height: '100%',
|
||||
color: 'black'
|
||||
}}>
|
||||
<Flex justify={"center"} style={{height: '100%'}}>
|
||||
<img style={{width: 'auto', height: '60px'}} src={'/hrbnu_logo.png'}
|
||||
alt={'哈师大logo'}/>
|
||||
</Flex>
|
||||
</a>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<div>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: mobileAccountItems,
|
||||
}}
|
||||
>
|
||||
<Button type={'text'}>
|
||||
<Space>
|
||||
<MenuOutlined/>
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Layout.Header>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileHeader;
|
||||
63
src/component/Menu/DashboardMenu.jsx
Normal file
63
src/component/Menu/DashboardMenu.jsx
Normal file
@ -0,0 +1,63 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import {Menu, message} from "antd";
|
||||
import DashboardMenuItems from "../../menu/DashboardMenuItems";
|
||||
import {useLocation} from "react-router-dom";
|
||||
import creatMessageCommonAxios from "../../http/CreatMessageCommonAxios";
|
||||
|
||||
const DashboardMenu = props => {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
let commonAxios = creatMessageCommonAxios(messageApi);
|
||||
|
||||
const [permissions, setPermissions] = React.useState([]);
|
||||
const [isAdmin, setIsAdmin] = React.useState(false);
|
||||
const [menuItems, setMenuItems] = React.useState(DashboardMenuItems);
|
||||
const [path, setPath] = React.useState(useLocation().pathname);
|
||||
|
||||
const removeTrailingSlash = (path) => {
|
||||
if (path.endsWith('/')) {
|
||||
return path.slice(0, -1);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
const fetchPermission = () => {
|
||||
commonAxios.get('/api/auth/permissions').then((response) => {
|
||||
if (response.data.data != null) {
|
||||
setPermissions(response.data.data)
|
||||
if (response.data.data.includes('ROLE_ADMIN')) {
|
||||
setIsAdmin(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchPermission();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const filteredMenuItems = DashboardMenuItems.filter(item => {
|
||||
console.log(item.key);
|
||||
return item.key !== 'system-management' || isAdmin;
|
||||
});
|
||||
setMenuItems(filteredMenuItems);
|
||||
}, [isAdmin]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Menu
|
||||
mode="inline"
|
||||
defaultSelectedKeys={[removeTrailingSlash(path)]}
|
||||
defaultOpenKeys={['workload-retrieval', 'system-management']}
|
||||
style={{
|
||||
height: '100%',
|
||||
borderRight: 0,
|
||||
}}
|
||||
items={menuItems}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DashboardMenu.propTypes = {};
|
||||
|
||||
export default DashboardMenu;
|
||||
55
src/component/TableTransfer/index.jsx
Normal file
55
src/component/TableTransfer/index.jsx
Normal file
@ -0,0 +1,55 @@
|
||||
import {Table, Transfer} from "antd";
|
||||
|
||||
const TableTransfer = (props) => {
|
||||
const { leftColumns, rightColumns, ...restProps } = props;
|
||||
return (
|
||||
<Transfer
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
{({
|
||||
direction,
|
||||
filteredItems,
|
||||
onItemSelect,
|
||||
onItemSelectAll,
|
||||
selectedKeys: listSelectedKeys,
|
||||
disabled: listDisabled,
|
||||
}) => {
|
||||
const columns = direction === 'left' ? leftColumns : rightColumns;
|
||||
const rowSelection = {
|
||||
getCheckboxProps: () => ({
|
||||
disabled: listDisabled,
|
||||
}),
|
||||
onChange(selectedRowKeys) {
|
||||
onItemSelectAll(selectedRowKeys, 'replace');
|
||||
},
|
||||
selectedRowKeys: listSelectedKeys,
|
||||
selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT, Table.SELECTION_NONE],
|
||||
};
|
||||
return (
|
||||
<Table
|
||||
rowSelection={rowSelection}
|
||||
columns={columns}
|
||||
dataSource={filteredItems}
|
||||
size="small"
|
||||
style={{
|
||||
pointerEvents: listDisabled ? 'none' : undefined,
|
||||
}}
|
||||
onRow={({ key, disabled: itemDisabled }) => ({
|
||||
onClick: () => {
|
||||
if (itemDisabled || listDisabled) {
|
||||
return;
|
||||
}
|
||||
onItemSelect(key, !listSelectedKeys.includes(key));
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Transfer>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableTransfer;
|
||||
32
src/component/Workload/CourseTypeTag.jsx
Normal file
32
src/component/Workload/CourseTypeTag.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
import {Tag} from "antd";
|
||||
import React, {useEffect} from "react";
|
||||
|
||||
const CourseTypeTag = (props) => {
|
||||
const {courseNature} = props;
|
||||
|
||||
const [color, setColor] = React.useState('');
|
||||
const [text, setText] = React.useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (courseNature === '01') {
|
||||
setColor('green');
|
||||
setText('公共必修');
|
||||
}
|
||||
if (courseNature === '02') {
|
||||
setColor('blue');
|
||||
setText('院选修');
|
||||
}
|
||||
if (courseNature === '03') {
|
||||
setColor('purple');
|
||||
setText('专业必修');
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
return (
|
||||
<Tag color={color} key={courseNature}>
|
||||
{text}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
export default CourseTypeTag;
|
||||
6
src/config/BaseWebConfig.js
Normal file
6
src/config/BaseWebConfig.js
Normal file
@ -0,0 +1,6 @@
|
||||
const baseWebConfig ={
|
||||
baseUrl: 'http://localhost:8080',
|
||||
timeout: 10000,
|
||||
}
|
||||
|
||||
export default baseWebConfig;
|
||||
32
src/http/AuthorizeAxios.js
Normal file
32
src/http/AuthorizeAxios.js
Normal file
@ -0,0 +1,32 @@
|
||||
import axios from 'axios';
|
||||
import baseWebConfig from "../config/BaseWebConfig";
|
||||
|
||||
const authorizeAxios = axios.create({
|
||||
baseURL: window.BACKEND_ADDRESS || baseWebConfig.baseUrl,
|
||||
timeout: window.BACKEND_TIMEOUT || baseWebConfig.timeout,
|
||||
});
|
||||
|
||||
// 传入messageApi
|
||||
|
||||
|
||||
// 响应拦截器
|
||||
authorizeAxios.interceptors.response.use(
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
const errorList = JSON.parse(localStorage.getItem('errorList') || '[]');
|
||||
|
||||
errorList.push({
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
localStorage.setItem('errorList', JSON.stringify(errorList));
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default authorizeAxios;
|
||||
77
src/http/CreatMessageCommonAxios.js
Normal file
77
src/http/CreatMessageCommonAxios.js
Normal file
@ -0,0 +1,77 @@
|
||||
import axios from 'axios';
|
||||
import baseWebConfig from "../config/BaseWebConfig";
|
||||
|
||||
const creatMessageCommonAxios = (messageApi) => {
|
||||
const instance = axios.create({
|
||||
baseURL: window.BACKEND_ADDRESS || baseWebConfig.baseUrl,
|
||||
timeout: window.BACKEND_TIMEOUT || baseWebConfig.timeout,
|
||||
});
|
||||
|
||||
const defaultResponse = {
|
||||
data: {},
|
||||
success: false,
|
||||
failed: true,
|
||||
};
|
||||
|
||||
|
||||
// 请求拦截器
|
||||
instance.interceptors.request.use(
|
||||
(config) => {
|
||||
// 可以在这里添加请求头或者其他配置
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
instance.interceptors.response.use(
|
||||
(response) => {
|
||||
let errorDetails = response.data.errorDetails;
|
||||
if (errorDetails) {
|
||||
messageApi.error(errorDetails.message + '(' + errorDetails.code + ')');
|
||||
// return Promise.reject(response);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
const errorList = JSON.parse(localStorage.getItem('errorList') || '[]');
|
||||
|
||||
errorList.push({
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
localStorage.setItem('errorList', JSON.stringify(errorList));
|
||||
|
||||
// 401跳转登录页面
|
||||
if (error.response?.status === 401) {
|
||||
messageApi.error('登录过期,请重新登录!');
|
||||
// 等待1秒后跳转到登录页面
|
||||
setTimeout(() => {
|
||||
window.location.href = '/auth/login';
|
||||
}, 1000);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// 403 弹出 ant-design 消息
|
||||
else if (error.response?.status === 403) {
|
||||
messageApi.error('权限不足,请重试!');
|
||||
} else {
|
||||
messageApi.error(error.response
|
||||
? error.response.data.errorDetails.message + '(' + error.response.data.errorDetails.code + ')'
|
||||
: '网络中断,请调试网络后重试!');
|
||||
}
|
||||
return Promise.resolve(defaultResponse);
|
||||
}
|
||||
);
|
||||
return instance;
|
||||
};
|
||||
export default creatMessageCommonAxios;
|
||||
13
src/index.css
Normal file
13
src/index.css
Normal file
@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
17
src/index.js
Normal file
17
src/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
1
src/logo.svg
Normal file
1
src/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
71
src/menu/DashboardMenuItems.js
Normal file
71
src/menu/DashboardMenuItems.js
Normal file
@ -0,0 +1,71 @@
|
||||
import {
|
||||
ControlOutlined,
|
||||
DashboardOutlined,
|
||||
DatabaseOutlined,
|
||||
DownloadOutlined,
|
||||
MonitorOutlined,
|
||||
ReconciliationOutlined,
|
||||
UsergroupAddOutlined,
|
||||
UserSwitchOutlined
|
||||
} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import {NavLink} from "react-router-dom";
|
||||
|
||||
const DashboardMenuItems = [
|
||||
{
|
||||
key: '/overview',
|
||||
icon: <DashboardOutlined/>,
|
||||
label: <NavLink to={'/overview'}>概览</NavLink>
|
||||
},
|
||||
{
|
||||
key: `workload-retrieval`,
|
||||
icon: <ReconciliationOutlined/>,
|
||||
label: `个人工作量`,
|
||||
children: [
|
||||
{
|
||||
key: `/data-check`,
|
||||
icon: <MonitorOutlined/>,
|
||||
label: <NavLink to={'/data-check'}>数据核对</NavLink>
|
||||
},
|
||||
{
|
||||
key: `/data-print`,
|
||||
icon: <DownloadOutlined/>,
|
||||
label: <NavLink to={'/data-print'}>数据打印</NavLink>
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: `system-management`,
|
||||
icon: <ControlOutlined/>,
|
||||
label: `系统管理`,
|
||||
children: [
|
||||
{
|
||||
key: `/user-management`,
|
||||
icon: <UserSwitchOutlined/>,
|
||||
label: <NavLink to={'/user-management'}>用户管理</NavLink>
|
||||
},
|
||||
// {
|
||||
// key: `announcement-management`,
|
||||
// icon: <NotificationOutlined/>,
|
||||
// label: `公告管理`,
|
||||
// },
|
||||
{
|
||||
key: `/data-maintenance`,
|
||||
icon: <DatabaseOutlined/>,
|
||||
label: <NavLink to={'/data-maintenance'}>数据维护</NavLink>
|
||||
},
|
||||
// {
|
||||
// key: `settings`,
|
||||
// icon: <SettingOutlined/>,
|
||||
// label: <NavLink to={'/system-settings'}>系统设置</NavLink>
|
||||
// }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: `/about-us`,
|
||||
icon: <UsergroupAddOutlined/>,
|
||||
label: <NavLink to={'/about-us'}>关于我们</NavLink>
|
||||
}
|
||||
];
|
||||
|
||||
export default DashboardMenuItems;
|
||||
23
src/menu/SettingsMenu.js
Normal file
23
src/menu/SettingsMenu.js
Normal file
@ -0,0 +1,23 @@
|
||||
import {AppstoreOutlined, BuildOutlined, DashboardOutlined, SecurityScanOutlined} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
|
||||
const SettingsMenu = [
|
||||
{
|
||||
key: '/commons',
|
||||
icon: <AppstoreOutlined />,
|
||||
label: '综合设置'
|
||||
},
|
||||
{
|
||||
key: '/security',
|
||||
icon: <SecurityScanOutlined />,
|
||||
label: '安全设置'
|
||||
},
|
||||
{
|
||||
key: '/ui',
|
||||
icon: <BuildOutlined />,
|
||||
label: '系统UI设定'
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
export default SettingsMenu;
|
||||
3
src/page/Authentication/Login/Login.css
Normal file
3
src/page/Authentication/Login/Login.css
Normal file
@ -0,0 +1,3 @@
|
||||
.login-layout{
|
||||
text-align: center;
|
||||
}
|
||||
133
src/page/Authentication/Login/index.jsx
Normal file
133
src/page/Authentication/Login/index.jsx
Normal file
@ -0,0 +1,133 @@
|
||||
import React, {useState} from 'react';
|
||||
import './Login.css'
|
||||
import {Button, Col, Flex, Input, Layout, message, Row, Space, Typography} from "antd";
|
||||
import {KeyOutlined, UserOutlined} from '@ant-design/icons';
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import authorizeAxios from "../../../http/AuthorizeAxios";
|
||||
import ChangePasswordModal from "../../../component/Header/ChangePasswordModal";
|
||||
import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios";
|
||||
|
||||
const {Content, Footer} = Layout;
|
||||
|
||||
const loginPageStyle = {
|
||||
color: 'blue',
|
||||
backgroundImage: 'url(/login-background.jpg)',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
height: 'auto',
|
||||
width: '100%'
|
||||
};
|
||||
|
||||
function Login(props) {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const commonAxios = creatMessageCommonAxios(messageApi);
|
||||
|
||||
const [loginParam, setLoginParam] = useState({
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
const [changePasswordModalOpen, setChangePasswordModalOpen] = useState(false);
|
||||
|
||||
const doLogin = () => {
|
||||
const username = document.getElementById('username-input').value;
|
||||
const password = document.getElementById('password-input').value;
|
||||
setLoginParam({username: username, password: password});
|
||||
// console.log(loginParam);
|
||||
authorizeAxios.post('/api/auth/login', loginParam).then((response) => {
|
||||
if (response.data.data) {
|
||||
localStorage.setItem('token', response.data.data.token);
|
||||
messageApi.success('登录成功');
|
||||
if (response.data.data.needChangePassword === true) {
|
||||
setChangePasswordModalOpen(true);
|
||||
} else navigate('/overview')
|
||||
} else {
|
||||
console.log(response);
|
||||
messageApi.error(response.data.errorDetails.message + '(' + response.data.errorDetails.code + ')');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{height: '100%'}}>
|
||||
<ChangePasswordModal commonAxios={commonAxios} messageApi={messageApi} open={changePasswordModalOpen}
|
||||
setOpen={setChangePasswordModalOpen} closable={false}/>
|
||||
<Row
|
||||
style={{...loginPageStyle, height: '100%'}}>
|
||||
<Col
|
||||
xs={24}
|
||||
lg={17}
|
||||
/>
|
||||
<Col
|
||||
style={{
|
||||
height: '100%'
|
||||
}}
|
||||
xs={24}
|
||||
lg={7}
|
||||
>
|
||||
{contextHolder}
|
||||
<Layout className={'login-layout'} style={{height: '100%', background: 'rgba(245,245,245,0.9)'}}>
|
||||
<Content style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<Flex justify={"center"} vertical>
|
||||
<div style={{padding: '20%'}}>
|
||||
<Space direction={"vertical"} size={"large"}>
|
||||
<div>
|
||||
<Flex justify={"center"} wrap={"wrap"}>
|
||||
<img style={{width: '100px', height: 'auto'}} src={'/hrbnu_logo.png'}
|
||||
alt={'哈师大logo'}/>
|
||||
<Typography.Title level={2}
|
||||
style={{marginBottom: '20px'}}>工作量精算管家</Typography.Title>
|
||||
</Flex>
|
||||
<Typography.Text type={"secondary"}>您的一站式工作量门户</Typography.Text>
|
||||
</div>
|
||||
<div>
|
||||
<Input id='username-input'
|
||||
placeholder="请输入用户名"
|
||||
prefix={<UserOutlined/>}
|
||||
size={"large"}
|
||||
style={{marginBottom: '20px'}}
|
||||
onChange={(e) => {
|
||||
setLoginParam({...loginParam, username: e.target.value})
|
||||
}}
|
||||
/>
|
||||
<Input.Password id='password-input'
|
||||
placeholder="请输入密码"
|
||||
prefix={<KeyOutlined/>}
|
||||
size={"large"}
|
||||
style={{marginBottom: '20px'}}
|
||||
onChange={(e) => {
|
||||
setLoginParam({...loginParam, password: e.target.value})
|
||||
}}
|
||||
/>
|
||||
<Button type="primary" block size={"large"}
|
||||
onClick={() => doLogin()}>登录</Button>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
</Flex>
|
||||
</Content>
|
||||
<Footer style={{background: 'rgba(0,0,0,0)'}}>
|
||||
<Flex vertical justify={'center'} align={'center'} wrap={"wrap"}>
|
||||
<Typography.Text type={"secondary"}>Powered by ©2023
|
||||
- {new Date().getFullYear()} SimRobot Studio</Typography.Text>
|
||||
<Typography.Text type={"secondary"}>SimRobot Studio
|
||||
来自哈尔滨师范大学计算机科学与信息工程学院软件工程系</Typography.Text>
|
||||
</Flex>
|
||||
</Footer>
|
||||
</Layout>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
||||
129
src/page/Dashboard/AboutUs/index.jsx
Normal file
129
src/page/Dashboard/AboutUs/index.jsx
Normal file
@ -0,0 +1,129 @@
|
||||
import React from 'react';
|
||||
import CardDiv from "../../../component/CardDiv/CardDiv";
|
||||
import {Flex, Table, Typography} from "antd";
|
||||
|
||||
const AboutUs = props => {
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '职责',
|
||||
dataIndex: 'responsibilities',
|
||||
key: 'responsibilities',
|
||||
},
|
||||
{
|
||||
title: '单位或曾单位',
|
||||
dataIndex: 'record',
|
||||
key: 'record',
|
||||
},
|
||||
{
|
||||
title: '联系方式',
|
||||
dataIndex: 'contract',
|
||||
key: 'contract',
|
||||
}
|
||||
];
|
||||
const data = [
|
||||
{
|
||||
key: '1',
|
||||
name: '付伟',
|
||||
responsibilities: '指导教师',
|
||||
record: '计算机科学与信息工程学院软件工程系',
|
||||
contract: '恕不提供'
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
name: '刁衣非',
|
||||
responsibilities: 'Infrastructure Support',
|
||||
record: '哈尔滨师范大学 教务处',
|
||||
contract: '恕不提供'
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
name: '刘小琳',
|
||||
responsibilities: 'Requirements Support',
|
||||
record: '哈尔滨师范大学 教务处',
|
||||
contract: '恕不提供'
|
||||
},
|
||||
{
|
||||
key: '10',
|
||||
name: '李东璋',
|
||||
responsibilities: 'Architect, 后端开发, 前端开发 ',
|
||||
record: '2020级软件工程03班',
|
||||
contract: 'l.dzh@163.com'
|
||||
},
|
||||
{
|
||||
key: '11',
|
||||
name: '马宇彤',
|
||||
responsibilities: '需求分析, 前端开发',
|
||||
record: '2020级软件工程04班',
|
||||
contract: '恕不提供'
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div>
|
||||
<CardDiv>
|
||||
<Title level={3}>关于我们</Title>
|
||||
<Title level={4}>Educational Fusion Cloud</Title>
|
||||
<Paragraph>在当今数字化浪潮席卷全球的时代,高等教育领域也在积极探索如何借助前沿技术提升教学管理效率、优化学生生活体验,并推动教育模式的创新变革。正是在这样的背景下,Educational
|
||||
Fusion Cloud(教育融合云)
|
||||
项目应运而生,它如同一颗璀璨的新星,正以其强大的功能和创新的理念,为高校的教务与生活管理带来前所未有的自动化与智能化变革。</Paragraph>
|
||||
<Paragraph>在 Educational Fusion Cloud
|
||||
项目中,有一个特别值得关注的孵化项目——工作量精算管家。它是整个系统中用于优化高校教师工作量管理与绩效评估的重要组成部分,旨在通过智能化手段解决传统教师工作量计算过程中存在的复杂性、主观性以及效率低下等问题。</Paragraph>
|
||||
<Paragraph>在数字化时代,数据安全至关重要。Educational Fusion Cloud
|
||||
项目在设计之初就将数据安全作为核心考量之一。系统采用了先进的加密技术、访问控制机制和数据备份策略,确保学生、教师和学校的各类数据在存储、传输和使用过程中的安全性。同时,通过严格的权限管理,只有经过授权的用户才能访问和操作相关数据,有效防止了数据泄露和滥用的风险,为高校师生营造了一个安全可靠的数字化环境。</Paragraph>
|
||||
<Paragraph>Educational Fusion Cloud
|
||||
项目正处于快速发展和不断完善的过程中。随着技术的不断进步和高校用户需求的持续变化,项目团队将继续致力于系统的优化升级和功能拓展。未来,Educational
|
||||
Fusion Cloud
|
||||
将进一步深化软件工程技术在高校管理中的应用,探索智能辅导、智能科研协作等更多创新功能;加强与高校外部资源的对接,如与企业合作开展实习实训、与科研机构共享科研成果等,拓宽高校的教育资源渠道;同时,持续提升系统的用户体验和数据安全保障能力,努力打造一个更加智能、高效、开放、安全的高校数字化生态系统。</Paragraph>
|
||||
<Title level={4}>SimRobot Studio</Title>
|
||||
<Flex justify={"space-between"} align={"start"} gap={"middle"} wrap={"wrap"}>
|
||||
<div style={{width: '80%'}}>
|
||||
<Paragraph>东流逝水,叶落纷纷,星辰交替间 SimRobot
|
||||
实验室已经走过20个春秋冬夏。从最初的机器人足球实验室到程序创新实验室再到现在的应用软件研究室,SimRobot
|
||||
紧跟学院、学校乃至国家的需求变更,同样在不断提升不断成长,沿着计算机领域的研究路线不断探索。截至目前,SimRobot
|
||||
实验室也是哈尔滨师范大学计算机科学与信息工程学院最具实力的实验室之一。</Paragraph>
|
||||
<Paragraph>SimRobot实验室是一个充满活力与创新精神的科研团队。
|
||||
这里汇聚了一批对软件工程怀揣热忱、对技术探索永无止境的学生和导师。
|
||||
实验室以软件开发为核心驱动力,致力于构建高效、智能的软件系统,从基础的工具软件到复杂的智能应用平台,从桌面端到移动端,从单机到分布式系统,
|
||||
无一不在他们的开发视野之内。他们用代码编织梦想,用逻辑架构未来,让软件成为连接人类与数字世界的桥梁。</Paragraph>
|
||||
<Paragraph>算法讨论是实验室的另一大特色活动。
|
||||
在这里,每一次算法的探讨都是一场思维的盛宴。
|
||||
无论是经典的排序算法、搜索算法,还是前沿的机器学习算法、深度学习算法,都被拿出来反复剖析、优化。学生们围坐在一起,热烈地交流着各自的想法,
|
||||
从算法的时间复杂度、空间复杂度到其在实际问题中的适用性,从理论的推导到代码的实现,每一个细节都不放过。他们用严谨的思维碰撞出创新的火花,
|
||||
用智慧的火花点燃技术的火焰,让算法成为解决问题的利刃。</Paragraph>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 240,
|
||||
height: 240,
|
||||
backgroundImage: 'url(/dashboard/simrobot.svg)',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Paragraph>在 SimRobot
|
||||
实验室,每一天都有新的挑战,每一天都有新的突破。他们用汗水浇灌着科技之花,用智慧书写着创新篇章。
|
||||
这里不仅是一个实验室,更是一个梦想的孵化器,一个未来科技人才的摇篮。相信在不久的将来,从这里走出的学子们将在计算机科学与软件工程领域绽放出属于自己的光芒,而
|
||||
SimRobot 实验室也将继续在科技的浪潮中乘风破浪,向着更高的目标砥砺前行。</Paragraph>
|
||||
|
||||
<Title level={4}>开发团队</Title>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
pagination={false}
|
||||
/>
|
||||
|
||||
</CardDiv>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AboutUs.propTypes = {};
|
||||
|
||||
export default AboutUs;
|
||||
@ -0,0 +1,58 @@
|
||||
import {Space} from "antd";
|
||||
import CourseTypeTag from "../../../../component/Workload/CourseTypeTag";
|
||||
|
||||
const CheckTableColumn = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
responsive: ['lg'],
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '授课名称',
|
||||
dataIndex: 'courseName',
|
||||
key: 'courseName',
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'teacherName',
|
||||
key: 'teacherName',
|
||||
responsive: ['lg'],
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '课程性质',
|
||||
dataIndex: 'courseNature',
|
||||
key: 'courseNature',
|
||||
responsive: ['sm'],
|
||||
render: (text) => <CourseTypeTag key={text} courseNature={text}/>,
|
||||
},
|
||||
{
|
||||
title: '授课专业',
|
||||
dataIndex: 'teachingMajor',
|
||||
key: 'teachingMajor',
|
||||
responsive: ['lg'],
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '总学时',
|
||||
dataIndex: 'totalClassHours',
|
||||
key: 'totalClassHours',
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
hidden: true,
|
||||
render: (_, record) => (
|
||||
<Space size={"middle"}>
|
||||
<a>申请复核</a>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
export default CheckTableColumn;
|
||||
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import {Pagination, Table} from "antd";
|
||||
import CheckTableColumn from "./CheckTableColumn";
|
||||
|
||||
const DataCheckTable = props => {
|
||||
|
||||
const {queryResult, queryRequest, setQueryRequest} = props;
|
||||
|
||||
const onPaginationChange = (page, pageSize) => {
|
||||
setQueryRequest({...queryRequest, page: page, size: pageSize});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
columns={CheckTableColumn}
|
||||
dataSource={queryResult.list}
|
||||
pagination={false}
|
||||
/>
|
||||
<div style={{width: '100%', marginTop: 10, marginBottom: 10}}/>
|
||||
<Pagination
|
||||
defaultCurrent={1}
|
||||
showSizeChanger
|
||||
total={queryResult.total}
|
||||
align={'end'}
|
||||
onChange={onPaginationChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DataCheckTable.propTypes = {};
|
||||
|
||||
export default DataCheckTable;
|
||||
151
src/page/Dashboard/DataCheck/index.jsx
Normal file
151
src/page/Dashboard/DataCheck/index.jsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import {Button, Form, message, Select, Space, Spin} from "antd";
|
||||
import DataCheckTable from "./DataCheckTable/DataCheckTable";
|
||||
import CardDiv from "../../../component/CardDiv/CardDiv";
|
||||
import ResourceFinder from "../../../util/ResourceFinder";
|
||||
import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios";
|
||||
|
||||
const DataCheck = props => {
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const commonAxios = creatMessageCommonAxios(messageApi);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [semesterList, setSemesterList] = React.useState([]);
|
||||
const [profile, setProfile] = React.useState({});
|
||||
const [queryRequest, setQueryRequest] = React.useState({
|
||||
page: 1,
|
||||
size: 10,
|
||||
startSemester: null,
|
||||
endSemester: null,
|
||||
staffNumber: profile.staffNumber,
|
||||
});
|
||||
const [spinLoading, setSpinLoading] = React.useState(true);
|
||||
const [queryResult, setQueryResult] = React.useState({});
|
||||
|
||||
|
||||
const fetchSemesterList = () => {
|
||||
let resourceFinder = new ResourceFinder('efc.workload.oms.workload', 'efc.workload.oms.workload.semester.list', commonAxios, null);
|
||||
resourceFinder.getResource().then((response) => {
|
||||
let semesterList = response.data.data.data || [];
|
||||
setSemesterList(semesterList);
|
||||
});
|
||||
}
|
||||
|
||||
const fetchWorkloadData = () => {
|
||||
let uri = `/api/v1/workload/query?page=${queryRequest.page}&size=${queryRequest.size}`;
|
||||
if (queryRequest.staffNumber) {
|
||||
uri += `&staffNumber=${queryRequest.staffNumber}`;
|
||||
}
|
||||
if (queryRequest.startSemester) {
|
||||
uri += `&startSemester=${queryRequest.startSemester}`;
|
||||
}
|
||||
if (queryRequest.endSemester) {
|
||||
uri += `&endSemester=${queryRequest.endSemester}`;
|
||||
}
|
||||
commonAxios.get(uri).then((response) => {
|
||||
if (!response.data.data) {
|
||||
setSpinLoading(false)
|
||||
setQueryResult([]);
|
||||
return
|
||||
}
|
||||
let workloadData = response.data.data || {};
|
||||
setQueryResult(workloadData);
|
||||
setSpinLoading(false)
|
||||
});
|
||||
}
|
||||
|
||||
const onFormSubmit = (values) => {
|
||||
console.log(values)
|
||||
setQueryRequest({
|
||||
...queryRequest,
|
||||
page: 1,
|
||||
size: 10,
|
||||
startSemester: values.startTerm,
|
||||
endSemester: values.endTerm,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchSemesterList();
|
||||
commonAxios.get('/api/auth/profile').then((response) => {
|
||||
if (response.data.data != null) {
|
||||
setProfile({
|
||||
id: response.data.data.id,
|
||||
name: response.data.data.name,
|
||||
staffNumber: response.data.data.staffNumber
|
||||
});
|
||||
setQueryRequest({...queryRequest, staffNumber: response.data.data.staffNumber});
|
||||
}
|
||||
});
|
||||
}, [props]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchWorkloadData();
|
||||
}, [queryRequest]);
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
{contextHolder}
|
||||
<Space direction={'vertical'} size={'middle'} style={{display: 'flex'}}>
|
||||
<CardDiv id={'data-check-param'}>
|
||||
<Form
|
||||
layout={"inline"}
|
||||
form={form}
|
||||
onFinish={onFormSubmit}
|
||||
>
|
||||
<Form.Item label={'起始学期'} name={'startTerm'}>
|
||||
<Select
|
||||
style={{
|
||||
width: 200
|
||||
}}
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder={'请选择起始学期'}
|
||||
options={semesterList.map(semester => {
|
||||
return {label: semester, value: semester}
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={'结束学期'} name={'endTerm'}>
|
||||
<Select
|
||||
style={{
|
||||
width: 200
|
||||
}}
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder={'请选择结束学期'}
|
||||
options={semesterList.map(semester => {
|
||||
return {label: semester, value: semester}
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type={"primary"} htmlType={"submit"}>获取数据</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</CardDiv>
|
||||
<CardDiv
|
||||
id={'data-check-table'}
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
<Spin spinning={spinLoading}>
|
||||
<DataCheckTable queryResult={queryResult} queryRequest={queryRequest}
|
||||
setQueryRequest={setQueryRequest}/>
|
||||
</Spin>
|
||||
</CardDiv>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DataCheck.propTypes = {};
|
||||
|
||||
export default DataCheck;
|
||||
99
src/page/Dashboard/DataManager/DataManageTable.jsx
Normal file
99
src/page/Dashboard/DataManager/DataManageTable.jsx
Normal file
@ -0,0 +1,99 @@
|
||||
import React, {useState} from 'react';
|
||||
import {Button, Flex, message, Popconfirm, Table, Typography} from "antd";
|
||||
import ManageTableColumn from "./ManageTableColumn";
|
||||
import ImportDataDrawer from "./ImportDataDrawer";
|
||||
import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios";
|
||||
|
||||
const DataManageTable = props => {
|
||||
|
||||
const {workloadData, fetWorkload} = props;
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const commonAxios = creatMessageCommonAxios(messageApi);
|
||||
|
||||
const [selectIds, setSelectIds] = useState([]);
|
||||
|
||||
const rowSelection = {
|
||||
type: 'checkbox',
|
||||
onChange: (selectedRowKeys, selectedRows) => {
|
||||
setSelectIds(selectedRowKeys);
|
||||
console.log(selectIds)
|
||||
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
|
||||
},
|
||||
};
|
||||
|
||||
const onBatchDelete = () => {
|
||||
let url = `/api/v1/workload/delete`
|
||||
commonAxios.delete(url, {data: selectIds}).then(res => {
|
||||
if (res.data.data && res.data.data === true) {
|
||||
messageApi.success("删除成功");
|
||||
fetWorkload()
|
||||
setSelectIds([]);
|
||||
} else {
|
||||
messageApi.warning('您要删除的数据不存在或已被删除!');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const [importDataOpen, setImportDataOpen] = useState(false);
|
||||
|
||||
const importDataDrawerOnClose = () => {
|
||||
setImportDataOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{contextHolder}
|
||||
<ImportDataDrawer
|
||||
open={importDataOpen}
|
||||
onClose={importDataDrawerOnClose}
|
||||
fetchWorkloadData={fetWorkload}
|
||||
/>
|
||||
<Flex justify={"space-between"} align={"center"} gap={"middle"}>
|
||||
<h3>数据管理</h3>
|
||||
<Flex justify={"flex-end"} align={"center"} gap={"middle"}>
|
||||
<Typography.Text>共{workloadData.total}条数据</Typography.Text>
|
||||
<Button type={"primary"} onClick={() => setImportDataOpen(true)}>导入</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Table
|
||||
rowSelection={
|
||||
rowSelection
|
||||
}
|
||||
rowKey={(record) => record.id}
|
||||
columns={ManageTableColumn(commonAxios, messageApi, fetWorkload)}
|
||||
dataSource={workloadData.list}
|
||||
pagination={false}
|
||||
/>
|
||||
<div id={'batch-operation'} style={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
left: '0',
|
||||
width: '100%',
|
||||
height: '60px',
|
||||
background: 'rgb(245,245,245,0.8)',
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
display: selectIds.length !== 0 ? 'block' : 'none'
|
||||
}}>
|
||||
<Flex justify={"space-between"} align={"center"} style={{width: '100%', height: '100%'}}>
|
||||
<Typography.Text>已选择<Typography.Text
|
||||
type={'danger'}>{selectIds.length}</Typography.Text>项</Typography.Text>
|
||||
<Popconfirm
|
||||
title="批量删除工作量"
|
||||
description={`确定批量删除这${selectIds.length}条记录吗?`}
|
||||
onConfirm={() => onBatchDelete()}
|
||||
okText="确定"
|
||||
cancelText="算了"
|
||||
>
|
||||
<Button type={"primary"} danger>批量删除</Button>
|
||||
</Popconfirm>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DataManageTable.propTypes = {};
|
||||
|
||||
export default DataManageTable;
|
||||
137
src/page/Dashboard/DataManager/ImportDataDrawer.jsx
Normal file
137
src/page/Dashboard/DataManager/ImportDataDrawer.jsx
Normal file
@ -0,0 +1,137 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, Divider, Drawer, Form, Input, message, Radio, Space, Upload} from "antd";
|
||||
import {UploadOutlined} from "@ant-design/icons";
|
||||
import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios";
|
||||
import ImportHistoryTable from "./ImportHistoryTable";
|
||||
|
||||
const ImportDataDrawer = props => {
|
||||
|
||||
const {open, onClose, fetchWorkloadData} = props;
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const commonAxios = creatMessageCommonAxios(messageApi);
|
||||
|
||||
const [drawerParam, setDrawerParam] = useState({
|
||||
open: false
|
||||
});
|
||||
const [fileList, setFileList] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('open drawer', open)
|
||||
setDrawerParam(prevDrawerParam => ({...prevDrawerParam, open: open}))
|
||||
}, [open]);
|
||||
|
||||
const onReset = () => {
|
||||
form.resetFields();
|
||||
setFileList([]);
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
form.submit();
|
||||
}
|
||||
|
||||
const onFinish = (values) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileList[0]);
|
||||
formData.append('startYear', values.startYear);
|
||||
formData.append('endYear', parseInt(values.startYear) + 1);
|
||||
formData.append('semester', values.term);
|
||||
commonAxios.post('/api/v1/workload/import', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}).then((res) => {
|
||||
if (res.data.data && res.data.success === true && res.data.data.uploadStatus === '03') {
|
||||
messageApi.success("导入成功");
|
||||
onReset();
|
||||
onClose();
|
||||
fetchWorkloadData();
|
||||
} else {
|
||||
messageApi.warning('导入失败,请检查文件格式或数据');
|
||||
onClose();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{contextHolder}
|
||||
<Drawer open={drawerParam.open}
|
||||
size={"large"}
|
||||
extra={
|
||||
<Space direction={"horizontal"} size={"middle"}>
|
||||
<Button onClick={onReset}>重置</Button>
|
||||
<Button type={"primary"} onClick={onSubmit}>提交</Button>
|
||||
</Space>
|
||||
}
|
||||
title={'导入数据'}
|
||||
placement={'right'}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div>
|
||||
<Form
|
||||
form={form}
|
||||
labelAlign={'left'}
|
||||
onFinish={onFinish}
|
||||
>
|
||||
<Form.Item
|
||||
label={'起始年度'}
|
||||
name={'startYear'}
|
||||
labelCol={{span: 6}}
|
||||
wrapperCol={{span: 18}}
|
||||
rules={[{required: true, message: '请输入起始年度'},
|
||||
{pattern: /^20[0-9]{2}$/, message: '请输入正确的年度'}]}
|
||||
>
|
||||
<Input placeholder={'请输入起始年度'}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={'学期'}
|
||||
name={'term'}
|
||||
labelCol={{span: 6}}
|
||||
wrapperCol={{span: 18}}
|
||||
rules={[{required: true, message: '请选择学期'}]}
|
||||
>
|
||||
<Radio.Group
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
>
|
||||
<Radio value={1}>第一学期</Radio>
|
||||
<Radio value={2}>第二学期</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={'file'}
|
||||
label={'数据文件'}
|
||||
labelCol={{span: 6}}
|
||||
wrapperCol={{span: 18}}
|
||||
rules={[{required: true, message: '请上传数据文件'}]}
|
||||
>
|
||||
<Upload
|
||||
beforeUpload={(file) => {
|
||||
setFileList([file]);
|
||||
return false;
|
||||
}}
|
||||
onRemove={() => setFileList([])}
|
||||
multiple={false}
|
||||
fileList={fileList}
|
||||
>
|
||||
<Button icon={<UploadOutlined/>}>点击此处上传</Button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
<Divider/>
|
||||
<ImportHistoryTable messageApi={messageApi} commonAxios={commonAxios}
|
||||
fetchWorkloadData={fetchWorkloadData}/>
|
||||
</Drawer>
|
||||
</div>);
|
||||
};
|
||||
|
||||
ImportDataDrawer.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
|
||||
export default ImportDataDrawer;
|
||||
56
src/page/Dashboard/DataManager/ImportHistoryTable.jsx
Normal file
56
src/page/Dashboard/DataManager/ImportHistoryTable.jsx
Normal file
@ -0,0 +1,56 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Pagination, Table, Typography} from "antd";
|
||||
import ImportHistoryTableColumn from "./ImportHistoryTableColumn";
|
||||
|
||||
const ImportHistoryTable = props => {
|
||||
const {messageApi, commonAxios, fetchWorkloadData} = props;
|
||||
|
||||
const {Title, Text} = Typography;
|
||||
|
||||
const [historyList, setHistoryList] = useState({});
|
||||
const [queryRequest, setQueryRequest] = useState({
|
||||
page: 1,
|
||||
size: 10
|
||||
});
|
||||
|
||||
const fetchHistoryInfo = () => {
|
||||
commonAxios.get(`/api/v1/workload/upload-record/page?page=${queryRequest.page}&size=${queryRequest.size}`).then((res) => {
|
||||
if (res.data.success === true) {
|
||||
setHistoryList(res.data.data);
|
||||
} else {
|
||||
messageApi.warning('获取导入历史失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const onPaginationChange = (page, pageSize) => {
|
||||
setQueryRequest({...queryRequest, page: page, size: pageSize});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchHistoryInfo();
|
||||
}, [queryRequest, props]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/*<Title level={5}>导入历史</Title>*/}
|
||||
<Table
|
||||
columns={ImportHistoryTableColumn(commonAxios, messageApi, fetchHistoryInfo, fetchWorkloadData)}
|
||||
dataSource={historyList.list}
|
||||
pagination={false}
|
||||
/>
|
||||
<div style={{width: '100%', marginTop: 20}}/>
|
||||
<Pagination
|
||||
defaultCurrent={1}
|
||||
showSizeChanger
|
||||
total={historyList.total}
|
||||
align={'end'}
|
||||
onChange={onPaginationChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ImportHistoryTable.propTypes = {};
|
||||
|
||||
export default ImportHistoryTable;
|
||||
58
src/page/Dashboard/DataManager/ImportHistoryTableColumn.js
Normal file
58
src/page/Dashboard/DataManager/ImportHistoryTableColumn.js
Normal file
@ -0,0 +1,58 @@
|
||||
import {Button, Space, Tooltip} from "antd";
|
||||
import UploadStatus from "./UploadStatus";
|
||||
|
||||
const ManageTableColumn = (commonAxios, messageApi, fetchHistory, fetchWorkloadData) => [
|
||||
{
|
||||
title: '文件名',
|
||||
dataIndex: 'fileName',
|
||||
key: 'fileName',
|
||||
ellipsis: {
|
||||
showTitle: false,
|
||||
},
|
||||
render: (text) => (
|
||||
<Tooltip placement="topLeft" title={text}>
|
||||
{text}
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '学期',
|
||||
dataIndex: 'semester',
|
||||
key: 'semester',
|
||||
responsive: ['lg'],
|
||||
render: (_, record) => <span key={`${record.startYear}-${record.endYear}-${record.semester}`}>
|
||||
{`${record.startYear}-${record.endYear}-${record.semester}`}</span>
|
||||
},
|
||||
{
|
||||
title: '上传状态',
|
||||
dataIndex: 'uploadStatus',
|
||||
key: 'uploadStatus',
|
||||
responsive: ['lg'],
|
||||
render: (text) => <span key={text}><UploadStatus originText={text}/></span>
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
render: (_, record) => (
|
||||
record.uploadStatus === '03' ? (<Space size={"middle"}>
|
||||
<Button danger onClick={() => {
|
||||
let url = '/api/v1/workload/withdraw';
|
||||
let param = [];
|
||||
param.push(record.id);
|
||||
commonAxios.put(url, param).then(res => {
|
||||
if (res.data.data) {
|
||||
messageApi.success("撤回成功");
|
||||
fetchHistory()
|
||||
fetchWorkloadData()
|
||||
} else if (res.data.success === true) {
|
||||
messageApi.warning('您要撤回的数据不存在或已被撤回!');
|
||||
}
|
||||
})
|
||||
}}>撤回</Button>
|
||||
</Space>) : <></>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
export default ManageTableColumn;
|
||||
70
src/page/Dashboard/DataManager/ManageTableColumn.js
Normal file
70
src/page/Dashboard/DataManager/ManageTableColumn.js
Normal file
@ -0,0 +1,70 @@
|
||||
import {Button, Space} from "antd";
|
||||
import CourseTypeTag from "../../../component/Workload/CourseTypeTag";
|
||||
|
||||
const ManageTableColumn = (commonAxios, messageApi, fetchWorkload) => [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
responsive: ['lg'],
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '授课名称',
|
||||
dataIndex: 'courseName',
|
||||
key: 'courseName',
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'teacherName',
|
||||
key: 'teacherName',
|
||||
responsive: ['lg'],
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '课程性质',
|
||||
dataIndex: 'courseNature',
|
||||
key: 'courseNature',
|
||||
responsive: ['lg'],
|
||||
render: (text) => <CourseTypeTag key={text} courseNature={text}/>,
|
||||
},
|
||||
{
|
||||
title: '授课专业',
|
||||
dataIndex: 'teachingMajor',
|
||||
key: 'teachingMajor',
|
||||
responsive: ['lg'],
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '工作量',
|
||||
dataIndex: 'totalClassHours',
|
||||
key: 'totalClassHours',
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
render: (_, record) => (
|
||||
<Space size={"middle"}>
|
||||
<Button danger onClick={() => {
|
||||
let id = record.id;
|
||||
let param = [];
|
||||
param.push(id);
|
||||
let url = `/api/v1/workload/delete`
|
||||
commonAxios.delete(url, {data: param}).then(res => {
|
||||
if (res.data.data && res.data.data === true) {
|
||||
messageApi.success("删除成功");
|
||||
fetchWorkload()
|
||||
} else {
|
||||
messageApi.warning('您要删除的数据不存在或已被删除!');
|
||||
}
|
||||
})
|
||||
}}>删除</Button>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
export default ManageTableColumn;
|
||||
40
src/page/Dashboard/DataManager/UploadStatus.jsx
Normal file
40
src/page/Dashboard/DataManager/UploadStatus.jsx
Normal file
@ -0,0 +1,40 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import {Badge} from "antd";
|
||||
|
||||
const UploadStatus = props => {
|
||||
const {originText} = props;
|
||||
|
||||
const [status, setStatus] = React.useState('success');
|
||||
const [text, setText] = React.useState('Success');
|
||||
|
||||
useEffect(() => {
|
||||
if (originText === '01') {
|
||||
setText('准备上传')
|
||||
setStatus('default');
|
||||
} else if (originText === '02') {
|
||||
setText('上传中')
|
||||
setStatus('processing');
|
||||
} else if (originText === '03') {
|
||||
setText('上传成功')
|
||||
setStatus('success');
|
||||
} else if (originText === '04') {
|
||||
setText('上传失败')
|
||||
setStatus('error');
|
||||
} else if (originText === '05') {
|
||||
setText('已撤销')
|
||||
setStatus('warning');
|
||||
} else {
|
||||
setText('未知')
|
||||
setStatus('default');
|
||||
}
|
||||
}, [props]);
|
||||
return (
|
||||
<div>
|
||||
<Badge status={status} text={text}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
UploadStatus.propTypes = {};
|
||||
|
||||
export default UploadStatus;
|
||||
214
src/page/Dashboard/DataManager/index.jsx
Normal file
214
src/page/Dashboard/DataManager/index.jsx
Normal file
@ -0,0 +1,214 @@
|
||||
import React, {useCallback, useEffect} from 'react';
|
||||
import {Button, Flex, Form, message, Pagination, Select, Space, Spin} from "antd";
|
||||
import DataManageTable from "./DataManageTable";
|
||||
import CardDiv from "../../../component/CardDiv/CardDiv";
|
||||
import {debounce} from "lodash";
|
||||
import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios";
|
||||
import ResourceFinder from "../../../util/ResourceFinder";
|
||||
|
||||
const DataManager = props => {
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const commonAxios = creatMessageCommonAxios(messageApi);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [staffInfo, setStaffInfo] = React.useState([]);
|
||||
const [semesterList, setSemesterList] = React.useState([]);
|
||||
const [queryRequest, setQueryRequest] = React.useState({
|
||||
page: 1,
|
||||
size: 10,
|
||||
staffNumber: null,
|
||||
startSemester: null,
|
||||
endSemester: null,
|
||||
});
|
||||
const [queryResult, setQueryResult] = React.useState([]);
|
||||
const [spinLoading, setSpinLoading] = React.useState(false);
|
||||
|
||||
|
||||
const fetchStaffInfo = (value) => {
|
||||
var url = `/api/auth/query/registered?page=1&size=10&staffNumber=${value}&precise=false`;
|
||||
if (!value || value === '') {
|
||||
url = `/api/auth/query/registered?page=1&size=10&precise=false`;
|
||||
}
|
||||
commonAxios.get(url)
|
||||
.then((response) => {
|
||||
let staffInfoList = response.data.data.list || [];
|
||||
setStaffInfo(staffInfoList);
|
||||
})
|
||||
}
|
||||
const debouncedFetchStaffInfo = useCallback(debounce(fetchStaffInfo, 150), []);
|
||||
|
||||
const fetchSemesterList = () => {
|
||||
let resourceFinder = new ResourceFinder('efc.workload.oms.workload', 'efc.workload.oms.workload.semester.list', commonAxios, null);
|
||||
resourceFinder.getResource().then((response) => {
|
||||
let semesterList = response.data.data.data || [];
|
||||
setSemesterList(semesterList);
|
||||
});
|
||||
}
|
||||
|
||||
const fetchWorkloadData = () => {
|
||||
let uri = `/api/v1/workload/query?page=${queryRequest.page}&size=${queryRequest.size}`;
|
||||
if (queryRequest.staffNumber) {
|
||||
uri += `&staffNumber=${queryRequest.staffNumber}`;
|
||||
}
|
||||
if (queryRequest.startSemester) {
|
||||
uri += `&startSemester=${queryRequest.startSemester}`;
|
||||
}
|
||||
if (queryRequest.endSemester) {
|
||||
uri += `&endSemester=${queryRequest.endSemester}`;
|
||||
}
|
||||
commonAxios.get(uri).then((response) => {
|
||||
if (!response.data.data) {
|
||||
setSpinLoading(false)
|
||||
setQueryResult([]);
|
||||
return
|
||||
}
|
||||
let workloadData = response.data.data || {};
|
||||
setQueryResult(workloadData);
|
||||
setSpinLoading(false)
|
||||
});
|
||||
}
|
||||
|
||||
const onPaginationChange = (page, pageSize) => {
|
||||
setQueryRequest({...queryRequest, page: page, size: pageSize});
|
||||
};
|
||||
|
||||
const onFormSubmit = (values) => {
|
||||
console.log(values)
|
||||
setQueryRequest({
|
||||
page: 1,
|
||||
size: 10,
|
||||
staffNumber: values.teacherId,
|
||||
startSemester: values.startTerm,
|
||||
endSemester: values.endTerm,
|
||||
})
|
||||
}
|
||||
|
||||
const onFormReset = () => {
|
||||
form.resetFields();
|
||||
setQueryRequest({
|
||||
page: 1,
|
||||
size: 10,
|
||||
staffNumber: null,
|
||||
startSemester: null,
|
||||
endSemester: null,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchStaffInfo();
|
||||
fetchSemesterList();
|
||||
fetchWorkloadData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setSpinLoading(true)
|
||||
fetchWorkloadData();
|
||||
console.log(queryRequest)
|
||||
}, [queryRequest]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{contextHolder}
|
||||
<Space
|
||||
style={{
|
||||
width: '100%'
|
||||
}}
|
||||
direction={"vertical"}
|
||||
size={"middle"}
|
||||
>
|
||||
<CardDiv>
|
||||
<Form
|
||||
layout={"inline"}
|
||||
style={{
|
||||
width: '100%',
|
||||
display: "flex",
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
labelAlign={'left'}
|
||||
form={form}
|
||||
onFinish={onFormSubmit}
|
||||
>
|
||||
<Flex justify={"flex-start"} align={"center"} gap={"middle"} wrap={"wrap"}>
|
||||
<Form.Item
|
||||
label={'教师'}
|
||||
name={'teacherId'}>
|
||||
<Select
|
||||
style={{
|
||||
width: '200px'
|
||||
}}
|
||||
showSearch
|
||||
allowClear
|
||||
onSearch={debouncedFetchStaffInfo}
|
||||
placeholder={'输入工号搜索'}
|
||||
options={(staffInfo || []).map(item => ({
|
||||
value: item.staffNumber,
|
||||
label: `${item.name} (${item.staffNumber})`
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={'起始学期'}
|
||||
name={'startTerm'}
|
||||
>
|
||||
<Select
|
||||
style={{
|
||||
width: '200px'
|
||||
}}
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder={'请选择起始学期'}
|
||||
options={semesterList.map(item => ({label: item, value: item}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={'结束学期'}
|
||||
name={'endTerm'}
|
||||
>
|
||||
<Select
|
||||
style={{
|
||||
width: '200px'
|
||||
}}
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder={'请选择结束学期'}
|
||||
options={semesterList.map(item => ({label: item, value: item}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Flex justify={"flex-start"} align={"center"} gap={"middle"}>
|
||||
<Form.Item>
|
||||
<Flex justify={"flex-start"} align={"center"} gap={"middle"}>
|
||||
<Button onClick={onFormReset}>重置</Button>
|
||||
<Button type={'primary'} htmlType={'submit'}>检索</Button>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form>
|
||||
</CardDiv>
|
||||
|
||||
<CardDiv>
|
||||
<Space direction={"vertical"} size={"middle"} style={{width: '100%'}}>
|
||||
<Spin spinning={spinLoading}>
|
||||
<DataManageTable workloadData={queryResult} fetWorkload={fetchWorkloadData}/>
|
||||
</Spin>
|
||||
<Pagination
|
||||
defaultCurrent={1}
|
||||
showSizeChanger
|
||||
total={queryResult.total}
|
||||
align={'end'}
|
||||
onChange={onPaginationChange}
|
||||
/>
|
||||
</Space>
|
||||
</CardDiv>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DataManager.propTypes = {};
|
||||
|
||||
export default DataManager;
|
||||
171
src/page/Dashboard/DataPrint/QueryConditionBox/index.jsx
Normal file
171
src/page/Dashboard/DataPrint/QueryConditionBox/index.jsx
Normal file
@ -0,0 +1,171 @@
|
||||
import React, {useCallback, useEffect} from 'react';
|
||||
import {Button, Col, DatePicker, Flex, Form, Input, message, Row, Select} from "antd";
|
||||
import {useForm} from "antd/es/form/Form";
|
||||
import {PlusOutlined, RedoOutlined, SearchOutlined} from "@ant-design/icons";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import creatMessageCommonAxios from "../../../../http/CreatMessageCommonAxios";
|
||||
import {debounce} from "lodash";
|
||||
|
||||
QueryConditionBox.propTypes = {};
|
||||
|
||||
function QueryConditionBox(props) {
|
||||
|
||||
const {setQueryRequest, setLoading} = props;
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
const [form] = useForm();
|
||||
|
||||
const commonAxios = creatMessageCommonAxios(messageApi);
|
||||
|
||||
const [staffInfo, setStaffInfo] = React.useState([]);
|
||||
|
||||
const onReset = () => {
|
||||
form.resetFields();
|
||||
setQueryRequest({
|
||||
page: 1,
|
||||
size: 6,
|
||||
id: null,
|
||||
staffNumber: null,
|
||||
fileType: null,
|
||||
status: null,
|
||||
startTime: null,
|
||||
endTime: null
|
||||
})
|
||||
setLoading(true)
|
||||
}
|
||||
|
||||
const onFinish = () => {
|
||||
const newRequest = {
|
||||
page: 1,
|
||||
size: 6,
|
||||
id: form.getFieldValue('id'),
|
||||
staffNumber: form.getFieldValue('staffNumber'),
|
||||
fileType: form.getFieldValue('fileType'),
|
||||
status: form.getFieldValue('status'),
|
||||
startTime: form.getFieldValue('time-range') ? form.getFieldValue('time-range')[0] : null,
|
||||
endTime: form.getFieldValue('time-range') ? form.getFieldValue('time-range')[1] : null
|
||||
};
|
||||
setQueryRequest(newRequest);
|
||||
setLoading(true)
|
||||
}
|
||||
|
||||
const fetchStaffInfo = (value) => {
|
||||
commonAxios.get(`/api/auth/query/registered?page=1&size=10&staffNumber=${value}&precise=false`)
|
||||
.then((response) => {
|
||||
let staffInfoList = response.data.data.list || [];
|
||||
setStaffInfo(staffInfoList);
|
||||
})
|
||||
}
|
||||
|
||||
const debouncedFetchStaffInfo = useCallback(debounce(fetchStaffInfo, 150), []);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
commonAxios.get(`/api/auth/query/registered?page=1&size=10&precise=false`)
|
||||
.then((response) => {
|
||||
let staffInfoList = response.data.data.list || [];
|
||||
setStaffInfo(staffInfoList);
|
||||
})
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={'query-condition-box'}>
|
||||
{contextHolder}
|
||||
<Form form={form}
|
||||
onFinish={onFinish}
|
||||
>
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<Form.Item label={"编号"} name='id'>
|
||||
<Input
|
||||
allowClear
|
||||
placeholder={'证明编号'}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item label={"教师"} name='staffNumber'>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={'输入工号搜索'}
|
||||
showSearch
|
||||
allowClear
|
||||
maxTagCount={'responsive'}
|
||||
onSearch={debouncedFetchStaffInfo}
|
||||
options={(staffInfo || []).map(item => ({
|
||||
value: item.staffNumber,
|
||||
label: `${item.name} (${item.staffNumber})`
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item label={"类型"} name='fileType'>
|
||||
<Select
|
||||
placeholder="请选择证明类型"
|
||||
options={[
|
||||
{
|
||||
value: '01',
|
||||
label: '本科教学课时证明'
|
||||
},
|
||||
{
|
||||
value: '02',
|
||||
label: '任职后工作情况证明'
|
||||
}
|
||||
]}
|
||||
style={{minWidth: 180}}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item label={"状态"} name='status'>
|
||||
<Select
|
||||
placeholder="请选择生成状态"
|
||||
options={[
|
||||
{
|
||||
value: '01',
|
||||
label: '生成中'
|
||||
},
|
||||
{
|
||||
value: '02',
|
||||
label: '生成成功'
|
||||
},
|
||||
{
|
||||
value: '03',
|
||||
label: '生成失败'
|
||||
},
|
||||
{
|
||||
value: '04',
|
||||
label: '失效'
|
||||
},
|
||||
]}
|
||||
style={{minWidth: 120}}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Form.Item label={"申请时间"} name='time-range'>
|
||||
<DatePicker.RangePicker showTime/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Flex justify={"space-between"} align={"center"}>
|
||||
<Flex justify={"start"} align={"center"}>
|
||||
<Button icon={<PlusOutlined/>}
|
||||
onClick={() => navigate('/generate-certificate')}>生成新报告</Button>
|
||||
</Flex>
|
||||
<Flex justify={"center"} align={"center"} gap={"large"}>
|
||||
<Button type={"primary"} htmlType={"submit"} icon={<SearchOutlined/>}>搜索</Button>
|
||||
<Button type={"default"} onClick={onReset} icon={<RedoOutlined/>}>重置</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueryConditionBox;
|
||||
101
src/page/Dashboard/DataPrint/RecordCard/CardAction.js
Normal file
101
src/page/Dashboard/DataPrint/RecordCard/CardAction.js
Normal file
@ -0,0 +1,101 @@
|
||||
import {DeleteOutlined, DownloadOutlined, EditOutlined, RetweetOutlined} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import creatMessageCommonAxios from "../../../../http/CreatMessageCommonAxios";
|
||||
import baseWebConfig from "../../../../config/BaseWebConfig";
|
||||
import {Popconfirm} from "antd";
|
||||
|
||||
const CardAction = (record, messageApi, navigator) => {
|
||||
|
||||
const commonAxios = creatMessageCommonAxios(messageApi);
|
||||
|
||||
const downloadRecord = (recordId) => {
|
||||
let baseUrl = window.BACKEND_ADDRESS || baseWebConfig.baseUrl
|
||||
commonAxios.get(`${baseUrl}/api/v1/workload/certificate/download/${recordId}`, {responseType: 'blob'}).then((response) => {
|
||||
if (response.data.type === 'application/json') {
|
||||
messageApi.error('证明已失效 (GENERATE_CERTIFICATE_ERROR_007)');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
let fileName = `certificate-${recordId}.pdf`;
|
||||
const blob = new Blob([response.data], {type: 'application/octet-stream'});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
const reGenerateRecord = (record) => {
|
||||
if (record.status === '01' || record.status === '02') {
|
||||
messageApi.error('证明已经生成或者正在生成,无法重新生成');
|
||||
return;
|
||||
}
|
||||
commonAxios.post(`/api/v1/workload/certificate/re-generate/${record.id}`).then((response) => {
|
||||
if (response.data.success) {
|
||||
messageApi.success('已进入队列');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const onEdit = (record) => {
|
||||
commonAxios.get(`/api/v1/workload/detail/edit/${record.id}`).then((response) => {
|
||||
let recordRes = response.data.data || {};
|
||||
sessionStorage.setItem('certificateParam', JSON.stringify(recordRes.certificateParam || {}));
|
||||
sessionStorage.setItem('chooseUser', recordRes.chooseUser || '');
|
||||
sessionStorage.setItem('nowStep', recordRes.nowStep || '1');
|
||||
sessionStorage.setItem('generate-request', JSON.stringify(recordRes.generateRequest || {}));
|
||||
sessionStorage.setItem('targetKeys', JSON.stringify(recordRes.targetKeys || []));
|
||||
sessionStorage.setItem('edit-flag', "YES");
|
||||
navigator('/generate-certificate')
|
||||
})
|
||||
}
|
||||
|
||||
const onDelete = (record) => {
|
||||
commonAxios.delete(`/api/v1/workload/certificate/delete/${record.id}`).then((response) => {
|
||||
if (response.data.success) {
|
||||
messageApi.success('删除成功');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
<EditOutlined onClick={() => onEdit(record)}/>,
|
||||
...(record.status === '04'
|
||||
? [<Popconfirm
|
||||
title="重新生成证明"
|
||||
description="确定重新生成这份证明吗?"
|
||||
onConfirm={() => reGenerateRecord(record)}
|
||||
okText="确定"
|
||||
cancelText="算了"
|
||||
>
|
||||
<RetweetOutlined/>
|
||||
</Popconfirm>] : []),
|
||||
...(record.status === '02' ? [<DownloadOutlined onClick={() => downloadRecord(record.id)}/>] : []),
|
||||
...(record.status === '03' || record.status === '04'
|
||||
? [<Popconfirm
|
||||
title="删除证明记录"
|
||||
description="确定删除这个记录吗?"
|
||||
onConfirm={() => onDelete(record)}
|
||||
okText="确定"
|
||||
cancelText="算了"
|
||||
>
|
||||
<DeleteOutlined/>
|
||||
</Popconfirm>]
|
||||
: []),
|
||||
]
|
||||
}
|
||||
|
||||
export default CardAction;
|
||||
70
src/page/Dashboard/DataPrint/RecordCard/PrintRecordCard.jsx
Normal file
70
src/page/Dashboard/DataPrint/RecordCard/PrintRecordCard.jsx
Normal file
@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Avatar, Card, Divider, Flex, Skeleton, Tooltip, Typography} from "antd";
|
||||
import RecordStatus from "./RecordStatus";
|
||||
import DateFormater from "../../../../util/DateFormater";
|
||||
import CardAction from "./CardAction";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
PrintRecordCard.propTypes = {
|
||||
fileType: PropTypes.number.isRequired,
|
||||
record: PropTypes.object.isRequired,
|
||||
|
||||
};
|
||||
|
||||
function PrintRecordCard(props) {
|
||||
|
||||
const {Meta} = Card;
|
||||
const {fileType, record, messageApi} = props;
|
||||
const {Title, Text, Paragraph} = Typography;
|
||||
const recordName = record.recordType === '01' ? '本科课堂教学课时证明' : '任现职后工作情况证明';
|
||||
const recordIdSuffix = record.id.slice(-6);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Skeleton active={true} loading={false}>
|
||||
<Card
|
||||
bordered={true}
|
||||
bodyStyle={{
|
||||
height: 200
|
||||
}}
|
||||
style={{minWidth: 400}}
|
||||
hoverable={true}
|
||||
actions={CardAction(record, messageApi, navigate)}
|
||||
>
|
||||
<Flex justify={"space-between"} align={"start"}>
|
||||
<Meta
|
||||
avatar={<Avatar src={'/data-print/pdf_icon.svg'}/>}
|
||||
title={recordName}
|
||||
description={
|
||||
<div>
|
||||
<span>编号:</span>
|
||||
<Text type={"secondary"} copyable>{record.id}</Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Tooltip title={'ss'}>
|
||||
|
||||
</Tooltip>
|
||||
<Tooltip title={record.failReason}>
|
||||
<span></span>
|
||||
<RecordStatus status={record.status}/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Divider/>
|
||||
<Flex justify={"start"} align={"start"} vertical={true}>
|
||||
<div>
|
||||
<Text strong>教师工号: </Text><Text>{record.stuffNumber}</Text>
|
||||
</div>
|
||||
<div><Text strong>请求时间: </Text><Text>{DateFormater(record.requestTime)}</Text></div>
|
||||
<div><Text
|
||||
strong>生成时间: </Text><Text>{record.status === '02' ? DateFormater(record.madeTime) : 'N/A'}</Text>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
</Card>
|
||||
</Skeleton>)
|
||||
;
|
||||
}
|
||||
|
||||
export default PrintRecordCard;
|
||||
42
src/page/Dashboard/DataPrint/RecordCard/RecordStatus.jsx
Normal file
42
src/page/Dashboard/DataPrint/RecordCard/RecordStatus.jsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Badge} from "antd";
|
||||
|
||||
RecordStatus.propTypes = {
|
||||
status: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function RecordStatus({ status }) {
|
||||
const [statusValue, setStatusValue] = useState({
|
||||
color: 'green',
|
||||
text: '生成成功'
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
switch (status) {
|
||||
case '02':
|
||||
setStatusValue({color: 'success', text: '生成成功'});
|
||||
break;
|
||||
case '01':
|
||||
setStatusValue({color: 'processing', text: '生成中'});
|
||||
break;
|
||||
case '03':
|
||||
setStatusValue({color: 'error', text: '生成失败'});
|
||||
break;
|
||||
case '04':
|
||||
setStatusValue({color: 'default', text: '已失效'});
|
||||
break;
|
||||
default:
|
||||
setStatusValue({color: 'warning', text: '未知状态'});
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<Badge
|
||||
status={statusValue.color}
|
||||
text={statusValue.text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default RecordStatus;
|
||||
139
src/page/Dashboard/DataPrint/index.jsx
Normal file
139
src/page/Dashboard/DataPrint/index.jsx
Normal file
@ -0,0 +1,139 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Button, Col, Empty, FloatButton, message, Pagination, Row, Skeleton, Space, Spin, Typography} from "antd";
|
||||
import PrintRecordCard from "./RecordCard/PrintRecordCard";
|
||||
import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios";
|
||||
import QueryConditionBox from "./QueryConditionBox";
|
||||
import CardDiv from "../../../component/CardDiv/CardDiv";
|
||||
import {PlusOutlined} from "@ant-design/icons";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
const DataPrint = props => {
|
||||
const [downloadDisabled, setDownloadDisabled] = useState(true);
|
||||
const previewClicked = () => {
|
||||
setDownloadDisabled(false);
|
||||
}
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [spinLoading, setSpinLoading] = useState(false);
|
||||
|
||||
const commonAxios = creatMessageCommonAxios(messageApi);
|
||||
|
||||
const [queryRequest, setQueryRequest] = useState({
|
||||
page: 1,
|
||||
size: 6,
|
||||
id: null,
|
||||
staffNumber: null,
|
||||
fileType: null,
|
||||
status: null,
|
||||
startTime: null,
|
||||
endTime: null
|
||||
});
|
||||
|
||||
const [records, setRecords] = useState([{
|
||||
"id": "",
|
||||
"stuffNumber": "",
|
||||
"currentOperatorUser": "",
|
||||
"recordType": "",
|
||||
"status": "",
|
||||
"failReason": null,
|
||||
"requestTime": "",
|
||||
"madeTime": "",
|
||||
"extraInfo": null
|
||||
}]);
|
||||
|
||||
const [pageInfo, setPageInfo] = useState({
|
||||
current: 1,
|
||||
pageSize: 6,
|
||||
total: 2
|
||||
});
|
||||
|
||||
const onPageChange = (page) => {
|
||||
setQueryRequest({...queryRequest, page: page});
|
||||
setSpinLoading(true)
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
commonAxios.post('/api/v1/workload/certificate/record', queryRequest).then((response) => {
|
||||
if (response.data.data != null) {
|
||||
setRecords(response.data.data.list);
|
||||
setPageInfo({...pageInfo, current: response.data.data.pageNum, total: response.data.data.total});
|
||||
} else {
|
||||
setRecords([]);
|
||||
}
|
||||
setLoading(false)
|
||||
setSpinLoading(false)
|
||||
});
|
||||
// 动态调整
|
||||
}, 0)
|
||||
|
||||
}, [queryRequest]);
|
||||
|
||||
return (
|
||||
<div style={{width: '100%'}}>
|
||||
{contextHolder}
|
||||
<Space direction="vertical" size="large" style={{display: "flex"}}>
|
||||
<CardDiv>
|
||||
<QueryConditionBox setQueryRequest={(req) => setQueryRequest(req)}
|
||||
setLoading={(loading) => setLoading(loading)}/>
|
||||
</CardDiv>
|
||||
<Skeleton active={true} loading={loading}>
|
||||
<CardDiv>
|
||||
{
|
||||
records && records.length > 0 ? <Spin spinning={spinLoading}>
|
||||
<Space direction="vertical" size="large" style={{display: "flex"}}>
|
||||
<Row gutter={[16, {xs: 8, sm: 16, md: 24, lg: 32}]}>
|
||||
{
|
||||
records.map((record, index) => {
|
||||
return (<Col xxl={8} xl={12} lg={12} md={24} sm={24} xs={24}>
|
||||
<PrintRecordCard key={index} record={record} fileType={1}
|
||||
messageApi={messageApi}/>
|
||||
</Col>)
|
||||
})
|
||||
}
|
||||
</Row>
|
||||
<Pagination
|
||||
showQuickJumper
|
||||
defaultCurrent={1}
|
||||
showSizeChanger={false}
|
||||
total={pageInfo.total}
|
||||
pageSize={pageInfo.pageSize}
|
||||
onChange={onPageChange}
|
||||
align={'end'}
|
||||
/>
|
||||
</Space>
|
||||
</Spin> : <>
|
||||
<Empty
|
||||
image="https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg"
|
||||
styles={{
|
||||
image: {
|
||||
height: 60,
|
||||
},
|
||||
}}
|
||||
description={
|
||||
<Typography.Text>
|
||||
暂无历史证明生成记录数据
|
||||
</Typography.Text>
|
||||
}
|
||||
>
|
||||
<Button type="primary" icon={<PlusOutlined/>}
|
||||
onClick={() => navigate('/generate-certificate')}>生成报告</Button>
|
||||
</Empty>
|
||||
</>
|
||||
}
|
||||
</CardDiv>
|
||||
</Skeleton>
|
||||
<FloatButton.BackTop visibilityHeight={1000}/>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DataPrint.propTypes = {};
|
||||
|
||||
export default DataPrint;
|
||||
146
src/page/Dashboard/GenerateCertificate/ChooseUser.jsx
Normal file
146
src/page/Dashboard/GenerateCertificate/ChooseUser.jsx
Normal file
@ -0,0 +1,146 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import {Button, Flex, Form, Input, message, Spin, Table, Typography} from "antd";
|
||||
import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios";
|
||||
|
||||
const ChooseUser = props => {
|
||||
|
||||
const {allowNext, request, setRequest} = props;
|
||||
|
||||
const {Title, Text} = Typography;
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const commonAxios = creatMessageCommonAxios(messageApi);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
hidden: true,
|
||||
width: 0,
|
||||
},
|
||||
{
|
||||
title: '工号',
|
||||
dataIndex: 'staffNumber',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '学院',
|
||||
dataIndex: 'college',
|
||||
},
|
||||
{
|
||||
title: '系别',
|
||||
dataIndex: 'department',
|
||||
width: 300
|
||||
},
|
||||
];
|
||||
|
||||
const [staffInfo, setStaffInfo] = React.useState([]);
|
||||
|
||||
const [staffNumberPrefix, setStaffNumberPrefix] = React.useState("");
|
||||
const [spinning, setSpinning] = React.useState(true);
|
||||
|
||||
const rowSelection = {
|
||||
onChange: (selectedRowKeys, selectedRows) => {
|
||||
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
|
||||
allowNext(selectedRows.length > 0);
|
||||
setRequest({...request, stuffNumber: selectedRows[0].staffNumber});
|
||||
sessionStorage.setItem('chooseUser', selectedRowKeys)
|
||||
sessionStorage.removeItem('certificateParam');
|
||||
sessionStorage.removeItem('targetKeys');
|
||||
sessionStorage.removeItem('generate-request');
|
||||
},
|
||||
defaultSelectedRowKeys: () => {
|
||||
const selectedRowKeys = [];
|
||||
let selected = sessionStorage.getItem('chooseUser');
|
||||
if (selected) {
|
||||
// 转换为number
|
||||
selectedRowKeys.push(selected);
|
||||
allowNext(selectedRowKeys.length > 0);
|
||||
}
|
||||
return selectedRowKeys;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
commonAxios.get(`/api/auth/query/registered?page=1&size=10`).then((response) => {
|
||||
if (response.data.data != null) {
|
||||
let list = response.data.data.list;
|
||||
// 为list设置key=id
|
||||
list.forEach((item) => {
|
||||
item.key = item.id;
|
||||
});
|
||||
setStaffInfo(list);
|
||||
} else {
|
||||
setStaffInfo([]);
|
||||
}
|
||||
});
|
||||
setSpinning(false);
|
||||
}, []);
|
||||
|
||||
const onSearch = () => {
|
||||
setSpinning(true);
|
||||
commonAxios.get(`/api/auth/query/registered?staffNumber=${staffNumberPrefix}&page=1&size=10`).then((response) => {
|
||||
if (response.data.data != null) {
|
||||
let list = response.data.data.list;
|
||||
// 为list设置key=id
|
||||
list.forEach((item) => {
|
||||
item.key = item.id;
|
||||
});
|
||||
setStaffInfo(list);
|
||||
} else {
|
||||
setStaffInfo([]);
|
||||
}
|
||||
setSpinning(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{contextHolder}
|
||||
<Flex vertical justiffy={"start"} align={"start"} gap={"large"}>
|
||||
<div>
|
||||
<Title level={4}>选择用户</Title>
|
||||
<Text level={4}
|
||||
type={'secondary'}>指示EFC系统为何人生成报告,选择后这份报告将会出现在您和该同事的EFC系统证明管理画面中。</Text>
|
||||
</div>
|
||||
<Form
|
||||
form={form}
|
||||
layout={"inline"}
|
||||
>
|
||||
<Form.Item
|
||||
name='staff-number'
|
||||
>
|
||||
<Input placeholder={"工号"} onChange={(event) => setStaffNumberPrefix(event.target.value)}/>
|
||||
</Form.Item>
|
||||
<Button type={"primary"} onClick={() => onSearch()}>搜索</Button>
|
||||
</Form>
|
||||
<div style={{width: '100%'}}>
|
||||
<Spin spinning={spinning}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={staffInfo}
|
||||
pagination={false}
|
||||
rowSelection={{
|
||||
type: 'radio',
|
||||
...rowSelection,
|
||||
}}
|
||||
/>
|
||||
</Spin>
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
)
|
||||
;
|
||||
}
|
||||
;
|
||||
|
||||
ChooseUser.propTypes = {};
|
||||
|
||||
export default ChooseUser;
|
||||
203
src/page/Dashboard/GenerateCertificate/ContentConfig.jsx
Normal file
203
src/page/Dashboard/GenerateCertificate/ContentConfig.jsx
Normal file
@ -0,0 +1,203 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Flex, message, Spin, Tag, Typography} from "antd";
|
||||
import TableTransfer from "../../../component/TableTransfer";
|
||||
import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios";
|
||||
|
||||
const ContentConfig = props => {
|
||||
|
||||
const {allowNext, request, setRequest} = props;
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const [messageApi, contentHolder] = message.useMessage();
|
||||
const commonAxios = creatMessageCommonAxios(messageApi);
|
||||
|
||||
const generateTag = (courseNature) => {
|
||||
let color = '';
|
||||
let text = '';
|
||||
if (courseNature === '01') {
|
||||
color = 'green';
|
||||
text = '公共必修';
|
||||
}
|
||||
if (courseNature === '02') {
|
||||
color = 'blue';
|
||||
text = '院选修';
|
||||
}
|
||||
if (courseNature === '03') {
|
||||
color = 'purple';
|
||||
text = '专业必修';
|
||||
}
|
||||
return (
|
||||
<Tag color={color} key={courseNature}>
|
||||
{text}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
const tableColumns = [
|
||||
{
|
||||
dataIndex: 'semester',
|
||||
title: '学期',
|
||||
},
|
||||
{
|
||||
dataIndex: 'courseNature',
|
||||
title: '课程性质',
|
||||
render: (_, {courseNature}) => (
|
||||
<>
|
||||
{
|
||||
generateTag(courseNature)
|
||||
}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
dataIndex: 'courseName',
|
||||
title: '课程名称',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
dataIndex: 'actualClassSize',
|
||||
title: '学生人数',
|
||||
},
|
||||
{
|
||||
dataIndex: 'totalClassHours',
|
||||
title: '总学时数',
|
||||
},
|
||||
]
|
||||
|
||||
const mockData = [
|
||||
{
|
||||
key: '1',
|
||||
semester: '2020-2021-1',
|
||||
courseNature: '必修',
|
||||
courseName: '计算机网络',
|
||||
actualClassSize: '30',
|
||||
totalClassHours: '48',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
semester: '2020-2021-1',
|
||||
courseNature: '必修',
|
||||
courseName: '计算机网络',
|
||||
actualClassSize: '30',
|
||||
totalClassHours: '48',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
semester: '2020-2021-1',
|
||||
courseNature: '必修',
|
||||
courseName: '计算机网络',
|
||||
actualClassSize: '30',
|
||||
totalClassHours: '48',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
semester: '2020-2021-1',
|
||||
courseNature: '必修',
|
||||
courseName: '计算机网络',
|
||||
actualClassSize: '30',
|
||||
totalClassHours: '48',
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
semester: '2020-2021-1',
|
||||
courseNature: '必修',
|
||||
courseName: '计算机网络',
|
||||
actualClassSize: '30',
|
||||
totalClassHours: '48',
|
||||
},
|
||||
]
|
||||
|
||||
const [targetKeys, setTargetKeys] = useState([]);
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [spinning, setSpinning] = useState(true);
|
||||
const [workloadList, setWorkloadList] = useState([
|
||||
{
|
||||
key: '',
|
||||
semester: '',
|
||||
courseNature: '',
|
||||
courseName: '',
|
||||
actualClassSize: '',
|
||||
totalClassHours: '',
|
||||
}
|
||||
]);
|
||||
|
||||
const onChange = (nextTargetKeys) => {
|
||||
setTargetKeys(nextTargetKeys);
|
||||
sessionStorage.setItem('targetKeys', JSON.stringify(nextTargetKeys));
|
||||
setRequest({...request, ids: nextTargetKeys});
|
||||
if (nextTargetKeys.length === 0) {
|
||||
allowNext(false)
|
||||
} else {
|
||||
allowNext(true)
|
||||
}
|
||||
};
|
||||
const toggleDisabled = (checked) => {
|
||||
setDisabled(checked);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
commonAxios.get(`/api/v1/workload/detail/queryList/${request.stuffNumber}`)
|
||||
.then((response) => {
|
||||
if (response.data.data) {
|
||||
console.log(response.data.data);
|
||||
let dataInfo = response.data.data.map((item, index) => {
|
||||
return {
|
||||
key: item.id,
|
||||
semester: item.semesterInfo,
|
||||
courseNature: item.courseNature,
|
||||
courseName: item.courseName,
|
||||
actualClassSize: item.actualClassSize,
|
||||
totalClassHours: item.totalClassHours,
|
||||
}
|
||||
}
|
||||
);
|
||||
setWorkloadList(dataInfo);
|
||||
setSpinning(false)
|
||||
}
|
||||
});
|
||||
|
||||
let item = sessionStorage.getItem('targetKeys');
|
||||
if (item !== null) {
|
||||
item = JSON.parse(item);
|
||||
setTargetKeys(item);
|
||||
if (item.length === 0) {
|
||||
allowNext(false)
|
||||
} else {
|
||||
allowNext(true)
|
||||
}
|
||||
}
|
||||
}, [request]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{contentHolder}
|
||||
<Flex vertical justify={"start"} align={"start"} gap={"middle"}>
|
||||
<div>
|
||||
<Title level={4}>证书内容配置</Title>
|
||||
<Text level={4}
|
||||
type={'secondary'}>选择您想体现在证明上的工作量数据,无论是课时证明还是任职后工作情况证明,都将会通过您的选择进而渲染合适的数据到您需要的报告中。</Text>
|
||||
</div>
|
||||
<div style={{width: '100%'}}>
|
||||
<Spin spinning={spinning}>
|
||||
<TableTransfer
|
||||
dataSource={workloadList}
|
||||
targetKeys={targetKeys}
|
||||
disabled={disabled}
|
||||
// showSearch
|
||||
showSelectAll={false}
|
||||
onChange={onChange}
|
||||
// filterOption={filterOptxion}
|
||||
leftColumns={tableColumns}
|
||||
rightColumns={tableColumns}
|
||||
/>
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ContentConfig.propTypes = {};
|
||||
|
||||
export default ContentConfig;
|
||||
175
src/page/Dashboard/GenerateCertificate/ParameterConfig.jsx
Normal file
175
src/page/Dashboard/GenerateCertificate/ParameterConfig.jsx
Normal file
@ -0,0 +1,175 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Flex, Form, Input, Radio, Typography} from "antd";
|
||||
|
||||
const ParameterConfig = props => {
|
||||
|
||||
const {allowNext, request, setRequest} = props;
|
||||
|
||||
const {Title, Text} = Typography;
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [hideWorkloadParam, setHideWorkloadParam] = React.useState(true);
|
||||
const [admin, setAdmin] = React.useState(true);
|
||||
|
||||
const [tempParam, setTempParam] = useState({
|
||||
recordType: null,
|
||||
total: '',
|
||||
annual: '',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
let item = sessionStorage.getItem("certificateParam");
|
||||
|
||||
// TODO 判断是否是管理员
|
||||
setAdmin(true);
|
||||
|
||||
if (item !== null) {
|
||||
item = JSON.parse(item);
|
||||
setTempParam(item);
|
||||
allowNext(item.recordType !== null)
|
||||
if (item.recordType === '02' && admin) {
|
||||
setHideWorkloadParam(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Flex vertical justify={"start"} align={"start"} gap={"middle"}>
|
||||
<div>
|
||||
<Title level={4}>参数配置</Title>
|
||||
<Text level={4}
|
||||
type={'secondary'}>配置这份证明,如指定这份证明的类型、手动设定它的计算数据。这将决定您即将看到的报告的样式和数据。</Text>
|
||||
</div>
|
||||
<div style={{width: '100%'}}>
|
||||
<Form
|
||||
form={form}
|
||||
labelCol={{
|
||||
span: 4,
|
||||
}}
|
||||
|
||||
style={{
|
||||
maxWidth: 500,
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
label={"证书类型"}
|
||||
name='recordType'
|
||||
rules={[{required: true, message: '请选择证书类型'}]}
|
||||
>
|
||||
<Radio.Group
|
||||
onChange={(event) => {
|
||||
const recordType = event.target.value;
|
||||
let item = sessionStorage.getItem("certificateParam");
|
||||
if (item !== null) {
|
||||
item = JSON.parse(item);
|
||||
} else item = tempParam;
|
||||
item.recordType = recordType;
|
||||
setTempParam(item)
|
||||
if (recordType === '02' && admin) {
|
||||
setHideWorkloadParam(false);
|
||||
setRequest({...request, recordType: recordType});
|
||||
} else {
|
||||
setHideWorkloadParam(true);
|
||||
setRequest({
|
||||
...request,
|
||||
recordType: recordType,
|
||||
totalTeachingWorkload: '',
|
||||
annualAverageTeachingWorkload: ''
|
||||
});
|
||||
item.total = '';
|
||||
item.annual = '';
|
||||
form.setFieldsValue({
|
||||
total: '',
|
||||
annual: ''
|
||||
});
|
||||
|
||||
}
|
||||
sessionStorage.setItem("certificateParam", JSON.stringify(item))
|
||||
allowNext(true);
|
||||
}}
|
||||
defaultValue={() => {
|
||||
let item = sessionStorage.getItem("certificateParam");
|
||||
if (item !== null) {
|
||||
item = JSON.parse(item);
|
||||
return item.recordType;
|
||||
}
|
||||
}}
|
||||
options={[
|
||||
{label: '本科课堂工时证明', value: '01'},
|
||||
{label: '任现职后工作情况证明', value: '02'}
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={"总工作量"}
|
||||
name='total'
|
||||
hidden={hideWorkloadParam}
|
||||
>
|
||||
<Input
|
||||
type={'number'}
|
||||
addonAfter={'课时'}
|
||||
allowClear
|
||||
onChange={(event) => {
|
||||
const total = event.target.value;
|
||||
let item = sessionStorage.getItem("certificateParam");
|
||||
if (item !== null) {
|
||||
item = JSON.parse(item);
|
||||
} else item = tempParam;
|
||||
item.total = total;
|
||||
setTempParam(item)
|
||||
sessionStorage.setItem("certificateParam", JSON.stringify(item))
|
||||
setRequest({...request, totalTeachingWorkload: total});
|
||||
}}
|
||||
defaultValue={() => {
|
||||
let item = sessionStorage.getItem("certificateParam");
|
||||
if (item !== null) {
|
||||
item = JSON.parse(item);
|
||||
return item.total;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={"年均工作量"}
|
||||
name='annual'
|
||||
hidden={hideWorkloadParam}
|
||||
>
|
||||
<Input
|
||||
addonAfter={'课时'}
|
||||
allowClear
|
||||
type={'number'}
|
||||
onChange={(event) => {
|
||||
const annualHours = event.target.value;
|
||||
let item = sessionStorage.getItem("certificateParam");
|
||||
if (item !== null) {
|
||||
item = JSON.parse(item);
|
||||
} else item = tempParam;
|
||||
item.annual = annualHours;
|
||||
setTempParam(item)
|
||||
sessionStorage.setItem("certificateParam", JSON.stringify(item))
|
||||
setRequest({
|
||||
...request,
|
||||
annualAverageTeachingWorkload: annualHours
|
||||
});
|
||||
}}
|
||||
defaultValue={() => {
|
||||
let item = sessionStorage.getItem("certificateParam");
|
||||
if (item !== null) {
|
||||
item = JSON.parse(item);
|
||||
return item.annual;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ParameterConfig.propTypes = {};
|
||||
|
||||
export default ParameterConfig;
|
||||
228
src/page/Dashboard/GenerateCertificate/ReadyToGenerate.jsx
Normal file
228
src/page/Dashboard/GenerateCertificate/ReadyToGenerate.jsx
Normal file
@ -0,0 +1,228 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Descriptions, Flex, message, Skeleton, Table, Tag, Typography} from "antd";
|
||||
import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios";
|
||||
|
||||
const generateTag = (courseNature) => {
|
||||
let color = '';
|
||||
let text = '';
|
||||
if (courseNature === '01') {
|
||||
color = 'green';
|
||||
text = '公共必修';
|
||||
}
|
||||
if (courseNature === '02') {
|
||||
color = 'blue';
|
||||
text = '院选修';
|
||||
}
|
||||
if (courseNature === '03') {
|
||||
color = 'purple';
|
||||
text = '专业必修';
|
||||
}
|
||||
return (
|
||||
<Tag color={color} key={courseNature}>
|
||||
{text}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
const tableColumns = [
|
||||
{
|
||||
dataIndex: 'semester',
|
||||
title: '学期',
|
||||
},
|
||||
{
|
||||
dataIndex: 'courseNature',
|
||||
title: '课程性质',
|
||||
render: (_, {courseNature}) => (
|
||||
<>
|
||||
{
|
||||
generateTag(courseNature)
|
||||
}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
dataIndex: 'courseName',
|
||||
title: '课程名称',
|
||||
},
|
||||
{
|
||||
dataIndex: 'actualClassSize',
|
||||
title: '学生人数',
|
||||
},
|
||||
{
|
||||
dataIndex: 'totalClassHours',
|
||||
title: '总学时数',
|
||||
},
|
||||
]
|
||||
|
||||
const mockData = [
|
||||
{
|
||||
key: '1',
|
||||
semester: '2020-2021-1',
|
||||
courseNature: '必修',
|
||||
courseName: '计算机网络',
|
||||
actualClassSize: '30',
|
||||
totalClassHours: '48',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
semester: '2020-2021-1',
|
||||
courseNature: '必修',
|
||||
courseName: '计算机网络',
|
||||
actualClassSize: '30',
|
||||
totalClassHours: '48',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
semester: '2020-2021-1',
|
||||
courseNature: '必修',
|
||||
courseName: '计算机网络',
|
||||
actualClassSize: '30',
|
||||
totalClassHours: '48',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
semester: '2020-2021-1',
|
||||
courseNature: '必修',
|
||||
courseName: '计算机网络',
|
||||
actualClassSize: '30',
|
||||
totalClassHours: '48',
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
semester: '2020-2021-1',
|
||||
courseNature: '必修',
|
||||
courseName: '计算机网络',
|
||||
actualClassSize: '30',
|
||||
totalClassHours: '48',
|
||||
},
|
||||
]
|
||||
|
||||
const ReadyToGenerate = props => {
|
||||
|
||||
const {allowNext, request, setRequest} = props;
|
||||
|
||||
const [messageApi, contentHolder] = message.useMessage();
|
||||
const commonAxios = creatMessageCommonAxios(messageApi);
|
||||
const {Title, Text} = Typography;
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [user, setUser] = React.useState({
|
||||
id: "",
|
||||
name: "",
|
||||
staffNumber: "",
|
||||
gender: "",
|
||||
college: "",
|
||||
department: "",
|
||||
researchRoom: ""
|
||||
});
|
||||
const [workloadList, setWorkloadList] = React.useState([{
|
||||
key: '',
|
||||
semester: '',
|
||||
courseNature: '',
|
||||
courseName: '',
|
||||
actualClassSize: '',
|
||||
totalClassHours: '',
|
||||
}]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
allowNext(true)
|
||||
console.log(request);
|
||||
commonAxios.get(`/api/auth/query/registered?staffNumber=${request.stuffNumber}`).then(response => {
|
||||
if (response.data.data.list) {
|
||||
console.log(response.data.data.list[0])
|
||||
let userInfo = response.data.data.list[0];
|
||||
setUser(userInfo);
|
||||
}
|
||||
});
|
||||
|
||||
commonAxios.post(`/api/v1/workload/detail/queryListByIds`, request.ids)
|
||||
.then((response) => {
|
||||
if (response.data.data) {
|
||||
console.log(response.data.data);
|
||||
let dataInfo = response.data.data.map((item, index) => {
|
||||
return {
|
||||
key: item.id,
|
||||
semester: item.semesterInfo,
|
||||
courseNature: item.courseNature,
|
||||
courseName: item.courseName,
|
||||
actualClassSize: item.actualClassSize,
|
||||
totalClassHours: item.totalClassHours,
|
||||
}
|
||||
}
|
||||
);
|
||||
setWorkloadList(dataInfo);
|
||||
setLoading(false)
|
||||
}
|
||||
});
|
||||
}, [request]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('数据写入=》', user)
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{contentHolder}
|
||||
<Flex vertical justify={"start"} align={"start"} gap={"middle"}>
|
||||
<div>
|
||||
<Title level={4}>数据确认</Title>
|
||||
<Text level={4} type={'secondary'}>请确认您的数据,点击生成按钮生成证书。</Text>
|
||||
</div>
|
||||
<div style={{width: '100%'}}>
|
||||
<Skeleton active={true} loading={loading}>
|
||||
<Descriptions
|
||||
bordered
|
||||
items={[
|
||||
{
|
||||
key: 'staffNumber',
|
||||
label: '工号',
|
||||
children: user.staffNumber,
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
label: '姓名',
|
||||
children: user.name,
|
||||
},
|
||||
{
|
||||
key: 'department',
|
||||
label: '学院',
|
||||
children: user.college,
|
||||
},
|
||||
{
|
||||
key: 'recordType',
|
||||
label: '证明类型',
|
||||
children: request.recordType === '01' ? '本科教学课时证明' : '任职后工作情况证明',
|
||||
},
|
||||
{
|
||||
key: 'totalTeachingWorkload',
|
||||
label: '总学时数',
|
||||
children: request.totalTeachingWorkload || '自动生成',
|
||||
// span: 2,
|
||||
},
|
||||
{
|
||||
key: 'annualAverageTeachingWorkload',
|
||||
label: '年均工作量',
|
||||
children: request.annualAverageTeachingWorkload || '自动生成',
|
||||
},
|
||||
{
|
||||
key: 'workloadList',
|
||||
label: '证明内容',
|
||||
children: (
|
||||
<>
|
||||
<Table columns={tableColumns} dataSource={workloadList} pagination={false}/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}/>
|
||||
</Skeleton>
|
||||
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ReadyToGenerate.propTypes = {};
|
||||
|
||||
export default ReadyToGenerate;
|
||||
228
src/page/Dashboard/GenerateCertificate/index.jsx
Normal file
228
src/page/Dashboard/GenerateCertificate/index.jsx
Normal file
@ -0,0 +1,228 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import {Button, Flex, message, Result, Space, Spin, Steps} from "antd";
|
||||
import CardDiv from "../../../component/CardDiv/CardDiv";
|
||||
import ChooseUser from "./ChooseUser";
|
||||
import ParameterConfig from "./ParameterConfig";
|
||||
import ContentConfig from "./ContentConfig";
|
||||
import ReadyToGenerate from "./ReadyToGenerate";
|
||||
import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
const GenerateCertificate = props => {
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const commonAxios = creatMessageCommonAxios(messageApi);
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
const [current, setCurrent] = React.useState(() => {
|
||||
let nowStep = sessionStorage.getItem('nowStep');
|
||||
if (nowStep === null) {
|
||||
return 0;
|
||||
} else {
|
||||
return parseInt(nowStep);
|
||||
}
|
||||
});
|
||||
|
||||
const [request, setRequest] = React.useState({
|
||||
ids: [],
|
||||
stuffNumber: "",
|
||||
recordType: "",
|
||||
totalTeachingWorkload: "",
|
||||
annualAverageTeachingWorkload: "",
|
||||
});
|
||||
|
||||
const [resultStatus, setResultStatus] = React.useState('error');
|
||||
const [result, setResult] = React.useState({});
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const [pageConfig, setPageConfig] = React.useState({});
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: '选择用户',
|
||||
description: '选择生成证书的用户',
|
||||
},
|
||||
{
|
||||
title: '证书参数配置',
|
||||
description: '证书类型、参数配置',
|
||||
},
|
||||
{
|
||||
title: '证书内容配置',
|
||||
description: '选择证书的工作量记录',
|
||||
},
|
||||
{
|
||||
title: '数据确认',
|
||||
description: '确认数据,准备生成',
|
||||
},
|
||||
{
|
||||
title: '生成证明',
|
||||
description: '生成进行中',
|
||||
},
|
||||
];
|
||||
|
||||
const stepMap = {
|
||||
0:
|
||||
<ChooseUser
|
||||
allowNext={(allow) => allowNext(allow)}
|
||||
request={request}
|
||||
setRequest={req => setRequest(req)}
|
||||
/>,
|
||||
1:
|
||||
<ParameterConfig
|
||||
allowNext={(allow) => allowNext(allow)}
|
||||
request={request}
|
||||
setRequest={req => setRequest(req)}
|
||||
/>,
|
||||
2:
|
||||
<ContentConfig
|
||||
allowNext={(allow) => allowNext(allow)}
|
||||
request={request}
|
||||
setRequest={req => setRequest(req)}
|
||||
/>,
|
||||
3:
|
||||
<ReadyToGenerate
|
||||
allowNext={(allow) => allowNext(allow)}
|
||||
request={request}
|
||||
setRequest={req => setRequest(req)}
|
||||
/>,
|
||||
4:
|
||||
<Result
|
||||
status={resultStatus}
|
||||
title={resultStatus === 'success' ? '成功' : '失败'}
|
||||
subTitle={resultStatus === 'success' ?
|
||||
`编号: ${result.id} 的证书打印请求已成功进入队列,正在生成证书,请前往证书打印页面查看。` :
|
||||
'证书生成失败'}
|
||||
extra={[
|
||||
<Button type="primary" key="console" onClick={() => navigate('/data-print')}>
|
||||
{resultStatus === 'success' ? '查看结果' : '返回'}
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
}
|
||||
|
||||
const [preButton, setPreButton] = React.useState({
|
||||
type: "default",
|
||||
disabled: true,
|
||||
hidden: false,
|
||||
text: "上一步",
|
||||
});
|
||||
|
||||
const [nextButton, setNextButton] = React.useState({
|
||||
type: "primary",
|
||||
disabled: true,
|
||||
hidden: false,
|
||||
text: "下一步",
|
||||
})
|
||||
|
||||
|
||||
const onNext = () => {
|
||||
let nextCurrent = current + 1;
|
||||
setPreButton({...preButton, disabled: false,});
|
||||
|
||||
if (nextCurrent === 3) {
|
||||
setCurrent(nextCurrent);
|
||||
sessionStorage.setItem('nowStep', nextCurrent.toString());
|
||||
}
|
||||
if (nextCurrent >= 4) {
|
||||
setLoading(true);
|
||||
commonAxios.post('/api/v1/workload/certificate/generate', request).then((response) => {
|
||||
console.log(response);
|
||||
console.log(response.data.data !== null)
|
||||
console.log(response.data.data.success === true)
|
||||
if (response.data.data !== null && response.data.success === true) {
|
||||
setResultStatus('success');
|
||||
setResult(response.data.data);
|
||||
} else {
|
||||
setResultStatus('error');
|
||||
setResult(response.data.errorDetails);
|
||||
messageApi.error(response.data.errorDetails.message + `(${response.data.errorDetails.code})`);
|
||||
}
|
||||
setPreButton({...preButton, hidden: true,});
|
||||
setNextButton({...nextButton, hidden: true,});
|
||||
setCurrent(4);
|
||||
setLoading(false);
|
||||
});
|
||||
sessionStorage.removeItem('nowStep');
|
||||
sessionStorage.removeItem('chooseUser');
|
||||
sessionStorage.removeItem('certificateParam');
|
||||
sessionStorage.removeItem('targetKeys');
|
||||
sessionStorage.removeItem('generate-request');
|
||||
} else {
|
||||
setCurrent(nextCurrent);
|
||||
allowNext(false);
|
||||
sessionStorage.setItem('nowStep', nextCurrent.toString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const onPrev = () => {
|
||||
let nextCurrent = current - 1;
|
||||
setCurrent(nextCurrent);
|
||||
let editFlag = sessionStorage.getItem('edit-flag');
|
||||
if (nextCurrent <= 0) {
|
||||
setCurrent(0)
|
||||
setPreButton({...preButton, disabled: true,})
|
||||
}
|
||||
sessionStorage.setItem('nowStep', nextCurrent.toString());
|
||||
}
|
||||
|
||||
const allowNext = (allow) => {
|
||||
setNextButton({...nextButton, disabled: !allow})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log(request);
|
||||
if (request.stuffNumber !== '') {
|
||||
sessionStorage.setItem('generate-request', JSON.stringify(request));
|
||||
}
|
||||
}, [request]);
|
||||
|
||||
useEffect(() => {
|
||||
let request = sessionStorage.getItem('generate-request');
|
||||
if (request !== null) {
|
||||
request = JSON.parse(request);
|
||||
setRequest(request);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{contextHolder}
|
||||
<Space direction="vertical" size="large" style={{display: "flex"}}>
|
||||
<CardDiv>
|
||||
<Steps
|
||||
current={current}
|
||||
items={steps}
|
||||
/>
|
||||
</CardDiv>
|
||||
<Spin spinning={loading}>
|
||||
<CardDiv>
|
||||
{stepMap[current]}
|
||||
<Flex justify={"start"} align={"center"} gap={"middle"} style={{marginTop: 20}}
|
||||
hidden={current >= 4}>
|
||||
<Button
|
||||
type={preButton.type}
|
||||
disabled={preButton.disabled}
|
||||
onClick={onPrev}
|
||||
style={{display: current === 4 ? 'none' : 'inline-block'}}>
|
||||
{preButton.text}
|
||||
</Button>
|
||||
<Button
|
||||
type={nextButton.type}
|
||||
disabled={nextButton.disabled}
|
||||
onClick={onNext}
|
||||
style={{display: current === 4 ? 'none' : 'inline-block'}}>
|
||||
{current === 3 ? '开始生成' : nextButton.text}
|
||||
</Button>
|
||||
</Flex>
|
||||
</CardDiv>
|
||||
</Spin>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GenerateCertificate.propTypes = {};
|
||||
|
||||
export default GenerateCertificate;
|
||||
13
src/page/Dashboard/NoticeManager/index.jsx
Normal file
13
src/page/Dashboard/NoticeManager/index.jsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
const NoticeManager = props => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NoticeManager.propTypes = {};
|
||||
|
||||
export default NoticeManager;
|
||||
254
src/page/Dashboard/Overview/index.jsx
Normal file
254
src/page/Dashboard/Overview/index.jsx
Normal file
@ -0,0 +1,254 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import {Avatar, Card, Col, Flex, message, Row, Statistic, Typography} from "antd";
|
||||
import Meta from "antd/es/card/Meta";
|
||||
import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios";
|
||||
import CardDiv from "../../../component/CardDiv/CardDiv";
|
||||
import CountUp from 'react-countup';
|
||||
|
||||
|
||||
const Overview = props => {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const commonAxios = creatMessageCommonAxios(messageApi);
|
||||
const {Title, Text} = Typography;
|
||||
const formatter = value => <CountUp end={value} separator=","/>;
|
||||
|
||||
const [profile, setProfile] = React.useState({
|
||||
id: '',
|
||||
name: '',
|
||||
staffNumber: '',
|
||||
college: '',
|
||||
department: '',
|
||||
researchRoom: '',
|
||||
});
|
||||
const [userStatistic, setUserStatistic] = React.useState({
|
||||
workloadDataCount: '0',
|
||||
reportCount: '0',
|
||||
});
|
||||
const [permissions, setPermissions] = React.useState([]);
|
||||
const [isAdmin, setIsAdmin] = React.useState(false);
|
||||
|
||||
const fetchUserProfile = () => {
|
||||
commonAxios.get('/api/auth/profile').then((response) => {
|
||||
if (response.data.data != null) {
|
||||
setProfile({
|
||||
id: response.data.data.id,
|
||||
name: response.data.data.name,
|
||||
staffNumber: response.data.data.staffNumber,
|
||||
college: response.data.data.college,
|
||||
department: response.data.data.department,
|
||||
researchRoom: response.data.data.researchRoom,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fetchPermission = () => {
|
||||
commonAxios.get('/api/auth/permissions').then((response) => {
|
||||
if (response.data.data != null) {
|
||||
setPermissions(response.data.data)
|
||||
if (response.data.data.includes('ROLE_ADMIN')) {
|
||||
setIsAdmin(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fetWorkloadStatistic = () => {
|
||||
commonAxios.get('/api/v1/workload/query-statistic').then((response) => {
|
||||
if (response.data.data != null) {
|
||||
setUserStatistic({
|
||||
workloadDataCount: response.data.data.workloadDataCount,
|
||||
reportCount: response.data.data.reportCount,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserProfile()
|
||||
fetWorkloadStatistic()
|
||||
fetchPermission()
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
{contextHolder}
|
||||
<Row>
|
||||
<Col xs={0} sm={0} lg={24}>
|
||||
<CardDiv style={{padding: 10}}>
|
||||
<Flex justify={"space-between"} align={"center"} wrap={"nowrap"}>
|
||||
<div>
|
||||
<Flex justify={"start"} align={"center"} wrap={"nowrap"} gap={"large"}>
|
||||
<div>
|
||||
<Avatar style={{backgroundColor: '#fde3cf', color: '#f56a00'}}
|
||||
size={42}>{profile.name.slice(-2) || 'U'}</Avatar>
|
||||
</div>
|
||||
<div>
|
||||
<Title level={3} style={{marginTop: 10}}>你好,{profile.name}!欢迎使用Education
|
||||
Fusion
|
||||
Cloud Platform!</Title>
|
||||
<Text
|
||||
type={"secondary"}>{profile.college} / {profile.department} / {profile.researchRoom}</Text>
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
<Flex justify={"center"} align={"center"} wrap={"nowrap"} gap={"large"}>
|
||||
<Statistic title="工作量数据条数" value={userStatistic.workloadDataCount}
|
||||
formatter={formatter}/>
|
||||
<Statistic title="生成报告数" value={userStatistic.reportCount} formatter={formatter}/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
</CardDiv>
|
||||
</Col>
|
||||
<Col sm={24} lg={0} xxl={0}>
|
||||
<CardDiv style={{padding: 10}}>
|
||||
<Flex justify={"space-between"} align={"center"} wrap={"wrap"}>
|
||||
<div>
|
||||
<Flex justify={"start"} align={"center"} wrap={"nowrap"} gap={"large"}>
|
||||
<div>
|
||||
<Avatar style={{backgroundColor: '#fde3cf', color: '#f56a00'}}
|
||||
size={42}>{profile.name.slice(-2) || 'U'}</Avatar>
|
||||
</div>
|
||||
<div>
|
||||
<Title level={3} style={{marginTop: 10}}>你好,{profile.name}!</Title>
|
||||
<Text
|
||||
type={"secondary"}>{profile.college} / {profile.department} / {profile.researchRoom}</Text>
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
<Flex justify={"start"} align={"center"} wrap={"nowrap"} gap={"large"}
|
||||
style={{marginTop: 10}}>
|
||||
<Statistic title="工作量数据条数" value={userStatistic.workloadDataCount}
|
||||
formatter={formatter}/>
|
||||
<Statistic title="生成报告数" value={userStatistic.reportCount} formatter={formatter}/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</CardDiv>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{
|
||||
padding: 0
|
||||
}}>
|
||||
<Row gutter={[16, {xs: 8, sm: 16, md: 24, lg: 32}]}>
|
||||
<Col lg={6} xs={24}>
|
||||
<a href={'/data-check'}>
|
||||
<Card
|
||||
hoverable
|
||||
cover={
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
minHeight: '240px',
|
||||
backgroundImage: 'url(/dashboard/work.svg)',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Meta title="数据核对" description="核对系统内工作量数据"/>
|
||||
</Card>
|
||||
</a>
|
||||
</Col>
|
||||
<Col lg={6} xs={24}>
|
||||
<a href={'/data-print'}>
|
||||
<Card
|
||||
hoverable
|
||||
cover={
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
minHeight: '240px',
|
||||
backgroundImage: 'url(/dashboard/print.svg)',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Meta title="数据打印" description="打印工作量统计报表"/>
|
||||
</Card>
|
||||
</a>
|
||||
</Col>
|
||||
{
|
||||
!isAdmin ? <></> :
|
||||
<Col lg={6} xs={24}>
|
||||
<a href={'/data-maintenance'}>
|
||||
<Card
|
||||
hoverable
|
||||
cover={
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
minHeight: '240px',
|
||||
backgroundImage: 'url(/dashboard/data-manage.svg)',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Meta title="数据维护" description="管理系统内工作量数据"/>
|
||||
</Card>
|
||||
</a>
|
||||
</Col>
|
||||
}
|
||||
{
|
||||
!isAdmin ? <></> :
|
||||
<Col lg={6} xs={24}>
|
||||
<a href={'/user-management'}>
|
||||
<Card
|
||||
hoverable
|
||||
cover={
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
minHeight: '240px',
|
||||
backgroundImage: 'url(/dashboard/user-manage.svg)',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Meta title="用户管理" description="管理系统内用户"/>
|
||||
</Card>
|
||||
</a>
|
||||
</Col>
|
||||
}
|
||||
|
||||
<Col lg={6} xs={24}>
|
||||
<a href={'/about-us'}>
|
||||
<Card
|
||||
hoverable
|
||||
cover={
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
minHeight: '240px',
|
||||
backgroundImage: 'url(/dashboard/simrobot.svg)',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Meta title="关于本系统" description="了解系统背景、开发团队"/>
|
||||
</Card>
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Overview.propTypes = {};
|
||||
|
||||
export default Overview;
|
||||
22
src/page/Dashboard/SystemSettings/index.jsx
Normal file
22
src/page/Dashboard/SystemSettings/index.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import {Col, Flex, Menu, Row, Switch, Typography} from 'antd';
|
||||
import Sider from "antd/es/layout/Sider";
|
||||
import DashboardMenuItems from "../../../menu/DashboardMenuItems";
|
||||
import SettingsMenu from "../../../menu/SettingsMenu";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
|
||||
const {Title} = Typography;
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Menu
|
||||
mode={'horizontal'}
|
||||
items={SettingsMenu}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
267
src/page/Dashboard/UserManagement/AccountInfoDrawer.jsx
Normal file
267
src/page/Dashboard/UserManagement/AccountInfoDrawer.jsx
Normal file
@ -0,0 +1,267 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Badge, Button, Descriptions, Divider, Drawer, Empty, Flex, Popconfirm, Space, Typography} from "antd";
|
||||
import DeleteTeacherModal from "./DeleteTeacherModal";
|
||||
import ResetPasswordModal from "./ResetPasswordModal";
|
||||
|
||||
const AccountInfoDrawer = props => {
|
||||
const {open, setOpen, userInfoDetails, commonAxios, messageApi, fetchUserInfoList} = props;
|
||||
|
||||
const {Title, Text, Paragraph} = Typography;
|
||||
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [userInfo, setUserInfo] = React.useState({});
|
||||
const [profile, setProfile] = React.useState({});
|
||||
const [modalOpen, setModalOpen] = React.useState(false);
|
||||
const [isSelf, setIsSelf] = React.useState(false);
|
||||
const [newPassword, setNewPassword] = React.useState('');
|
||||
const [resetPasswordModalOpen, setResetPasswordModalOpen] = React.useState(false);
|
||||
|
||||
const fetchAccountInfo = () => {
|
||||
setUserInfo(userInfoDetails)
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
fetchUserInfoList();
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
const fetchProfile = () => {
|
||||
commonAxios.get('/api/auth/profile').then((response) => {
|
||||
if (response.data.data != null) {
|
||||
setProfile({
|
||||
id: response.data.data.id,
|
||||
name: response.data.data.name,
|
||||
staffNumber: response.data.data.staffNumber
|
||||
});
|
||||
let propsUsername = userInfoDetails.staffNumber || '';
|
||||
setIsSelf(response.data.data.staffNumber === propsUsername)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const changeTeacherInfo = (code, afterValue) => {
|
||||
console.log('code', code);
|
||||
console.log('afterValue', afterValue);
|
||||
const url = '/api/v1/teacher/update';
|
||||
let params = {
|
||||
staffNumber: userInfoDetails.staffNumber,
|
||||
changes: [
|
||||
{
|
||||
changeOption: code,
|
||||
value: afterValue,
|
||||
}
|
||||
]
|
||||
}
|
||||
commonAxios.put(url, params)
|
||||
.then(res => {
|
||||
if (res.status === 200) {
|
||||
let newUserInfo = res.data.data || {};
|
||||
setUserInfo(newUserInfo);
|
||||
messageApi.success('修改成功');
|
||||
} else {
|
||||
messageApi.error('修改失败');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
messageApi.error('修改失败');
|
||||
});
|
||||
}
|
||||
|
||||
const changeAccountInfo = (code, afterValue) => {
|
||||
console.log('code', code);
|
||||
console.log('afterValue', afterValue);
|
||||
const url = '/api/v1/teacher/update-account';
|
||||
let params = {
|
||||
username: userInfoDetails.staffNumber,
|
||||
changes: [
|
||||
{
|
||||
changeOption: code,
|
||||
value: afterValue,
|
||||
}
|
||||
]
|
||||
}
|
||||
commonAxios.put(url, params)
|
||||
.then(res => {
|
||||
if (res.status === 200) {
|
||||
let newUserInfo = res.data.data || {};
|
||||
setUserInfo(newUserInfo);
|
||||
messageApi.success('修改成功');
|
||||
} else {
|
||||
messageApi.error('修改失败');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
messageApi.error('修改失败');
|
||||
});
|
||||
}
|
||||
|
||||
const resetPassword = () => {
|
||||
commonAxios.post('/api/auth/reset/password', {username: userInfoDetails.staffNumber})
|
||||
.then(res => {
|
||||
if (res && res.data.success) {
|
||||
setNewPassword(res.data.data.newPassword);
|
||||
setResetPasswordModalOpen(true);
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccountInfo();
|
||||
fetchProfile();
|
||||
}, [props]);
|
||||
return (
|
||||
<div>
|
||||
<Drawer
|
||||
open={open}
|
||||
loading={loading}
|
||||
onClose={onClose}
|
||||
size={"large"}
|
||||
closable
|
||||
destroyOnClose
|
||||
title={`${userInfo.name} (${userInfo.staffNumber})`}
|
||||
extra={
|
||||
<Space>
|
||||
{
|
||||
isSelf ? <></>
|
||||
: <Button
|
||||
color="danger"
|
||||
variant="dashed"
|
||||
onClick={() => setModalOpen(true)}
|
||||
>
|
||||
删除教师
|
||||
</Button>
|
||||
}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<DeleteTeacherModal
|
||||
open={modalOpen}
|
||||
setOpen={setModalOpen}
|
||||
setDrawerOpen={setOpen}
|
||||
commonAxios={commonAxios}
|
||||
messageApi={messageApi}
|
||||
userInfo={userInfo}
|
||||
fetchUserInfo={fetchUserInfoList}
|
||||
/>
|
||||
<ResetPasswordModal newPassword={newPassword} open={resetPasswordModalOpen}
|
||||
setOpen={setResetPasswordModalOpen} setNewPassword={setNewPassword}/>
|
||||
<Descriptions title={'教师信息'}>
|
||||
<Descriptions.Item label={'编号'}>
|
||||
{userInfo.id}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'姓名'}>
|
||||
<Text
|
||||
editable={{onChange: (value) => changeTeacherInfo('changeInfo.teacher.name', value)}}
|
||||
>{userInfo.name}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'工号'}>
|
||||
{userInfo.staffNumber}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'性别'}>
|
||||
{userInfo.gender}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'学院'} span={2}>
|
||||
<Text
|
||||
editable={{onChange: (value) => changeTeacherInfo('changeInfo.teacher.college', value)}}
|
||||
>{userInfo.college}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'专业'}>
|
||||
<Text
|
||||
editable={{onChange: (value) => changeTeacherInfo('changeInfo.teacher.department', value)}}
|
||||
>{userInfo.department}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'研究室'}>
|
||||
<Text
|
||||
editable={{onChange: (value) => changeTeacherInfo('changeInfo.teacher.researchRoom', value)}}
|
||||
>{userInfo.researchRoom}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'职称'}>
|
||||
<Text
|
||||
editable={{onChange: (value) => changeTeacherInfo('changeInfo.teacher.jobTitle', value)}}
|
||||
>{userInfo.jobTitle}</Text>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Divider/>
|
||||
|
||||
{
|
||||
!userInfo.registered
|
||||
? <>
|
||||
<Title level={5}>账号信息</Title>
|
||||
<Empty description={'该教师尚未注册账号'}/>
|
||||
</>
|
||||
: <>
|
||||
<Descriptions title={'账号信息'}>
|
||||
<Descriptions.Item label={'账户id'}>{userInfo.accountInfo.id}</Descriptions.Item>
|
||||
<Descriptions.Item label={'账号'}>{userInfo.accountInfo.username}</Descriptions.Item>
|
||||
<Descriptions.Item label={'账户状态'}>{userInfo.accountInfo.enable
|
||||
? <Badge status={'success'} text={'已启用'}/>
|
||||
: <Badge status={'error'} text={'已锁定'}/>}</Descriptions.Item>
|
||||
<Descriptions.Item label={'手机号'}>
|
||||
<Text
|
||||
editable={{onChange: (value) => changeAccountInfo('changeInfo.account.phone', value)}}
|
||||
>{userInfo.accountInfo.phone}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={'密码状态'}>{userInfo.accountInfo.resetPassword
|
||||
? <Badge status={'error'} text={'已过期'}/>
|
||||
: <Badge status={'success'} text={'有效'}/>}</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
label={'用户角色'}>{userInfo.accountInfo.roleList.some(role => role.code === 'ADMIN') ? '管理员' : '普通用户'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{
|
||||
profile.staffNumber === userInfo.accountInfo.username ? <><Paragraph type={'secondary'}
|
||||
style={{marginTop: 20}}>这是您自己的账号,您不能对自己进行任何可能影响账号运转的操作。</Paragraph> </>
|
||||
: <Flex justify={"start"} align={"center"} wrap={true} gap={"middle"}
|
||||
style={{marginTop: 20}}>
|
||||
{
|
||||
userInfo.accountInfo.enable
|
||||
? <Button
|
||||
onClick={() => changeAccountInfo('changeInfo.account.enabled', 'false')}
|
||||
>禁用用户</Button>
|
||||
: <Button
|
||||
onClick={() => changeAccountInfo('changeInfo.account.enabled', 'true')}
|
||||
>启用用户</Button>
|
||||
}
|
||||
<Popconfirm
|
||||
title={'重置密码'}
|
||||
description={'重置密码后,用户需要重新登录。'}
|
||||
okText={'确认'}
|
||||
cancelText={'算了'}
|
||||
onConfirm={() => {
|
||||
resetPassword();
|
||||
}}
|
||||
>
|
||||
<Button>重置密码</Button>
|
||||
</Popconfirm>
|
||||
{
|
||||
!userInfo.accountInfo.roleList.some(role => role.code === 'ADMIN')
|
||||
? <Button
|
||||
onClick={() => changeAccountInfo('changeInfo.account.role', 'ADMIN')}
|
||||
>提升为管理员</Button>
|
||||
: <Button
|
||||
onClick={() => changeAccountInfo('changeInfo.account.role', 'UN-ADMIN')}
|
||||
>降级为普通用户</Button>
|
||||
}
|
||||
|
||||
</Flex>
|
||||
}
|
||||
|
||||
</>
|
||||
}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AccountInfoDrawer.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
setOpen: PropTypes.func.isRequired,
|
||||
userInfoDetails: PropTypes.object.isRequired,
|
||||
commonAxios: PropTypes.object.isRequired,
|
||||
messageApi: PropTypes.object.isRequired,
|
||||
fetchUserInfoList: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AccountInfoDrawer;
|
||||
153
src/page/Dashboard/UserManagement/AddUserDrawer.jsx
Normal file
153
src/page/Dashboard/UserManagement/AddUserDrawer.jsx
Normal file
@ -0,0 +1,153 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, Divider, Drawer, Flex, Form, Input, Select, Typography, Upload} from "antd";
|
||||
import {UploadOutlined} from "@ant-design/icons";
|
||||
import baseWebConfig from "../../../config/BaseWebConfig";
|
||||
|
||||
const AddUserDrawer = props => {
|
||||
const {open, setOpen, commonAxios, fetchUserInfoList, messageApi} = props;
|
||||
|
||||
const formLayout = {
|
||||
labelCol: {span: 5},
|
||||
wrapperCol: {span: 20},
|
||||
};
|
||||
const fileUploadProps = {
|
||||
name: 'file',
|
||||
action: window.BACKEND_ADDRESS || baseWebConfig.baseUrl + '/api/v1/teacher/import',
|
||||
headers: {
|
||||
authorization: `Bearer ${localStorage.getItem('token') || ''}`,
|
||||
},
|
||||
onChange(info) {
|
||||
console.log(info);
|
||||
if (info.file.status !== 'uploading') {
|
||||
console.log(info.file, info.fileList);
|
||||
}
|
||||
if (info.file.status === 'done') {
|
||||
messageApi.success(`${info.file.name} file uploaded successfully`);
|
||||
} else if (info.file.status === 'error') {
|
||||
messageApi.error(`${info.file.name} file upload failed.`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const {Title, Paragraph} = Typography;
|
||||
|
||||
const onSubmit = (values) => {
|
||||
commonAxios.post('/api/v1/teacher/add', values).then(response => {
|
||||
let result = response.data.data || false;
|
||||
if (result) {
|
||||
messageApi.success('添加教师信息成功');
|
||||
setOpen(false);
|
||||
fetchUserInfoList();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Drawer
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
title={'添加教师'}
|
||||
destroyOnClose
|
||||
>
|
||||
<div>
|
||||
<Title level={5}>单个添加</Title>
|
||||
<Form
|
||||
form={form}
|
||||
{...formLayout}
|
||||
onFinish={onSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
label="工号"
|
||||
name="staffNumber"
|
||||
rules={[{required: true, message: '请输入工号'}]}
|
||||
>
|
||||
<Input placeholder="请输入工号"/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="姓名"
|
||||
name="name"
|
||||
rules={[{required: true, message: '请输入姓名'}]}
|
||||
>
|
||||
<Input placeholder="请输入姓名"/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="性别"
|
||||
name="gender"
|
||||
rules={[{required: true, message: '请选择性别'}]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择性别"
|
||||
options={[
|
||||
{label: '男', value: '男'},
|
||||
{label: '女', value: '女'},
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="学院"
|
||||
name="college"
|
||||
rules={[{required: true, message: '请输入学院'}]}
|
||||
>
|
||||
<Input placeholder="请输入学院"/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="专业"
|
||||
name="department"
|
||||
rules={[{required: true, message: '请输入专业名称'}]}
|
||||
>
|
||||
<Input placeholder="请输入专业"/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="研究室"
|
||||
name="researchRoom"
|
||||
rules={[{required: true, message: '请输入研究室名称'}]}
|
||||
>
|
||||
<Input placeholder="请输入研究室名称"/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="职称"
|
||||
name="jobTitle"
|
||||
rules={[{required: true, message: '请输入职称'}]}
|
||||
>
|
||||
<Input placeholder="请输入职称"/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="身份证号"
|
||||
name="idNumber"
|
||||
rules={[{required: true, message: '请输入身份证号'}]}
|
||||
>
|
||||
<Input placeholder="请输入身份证号"/>
|
||||
</Form.Item>
|
||||
<Flex justify={"center"} align={"center"}>
|
||||
<Button htmlType={'submit'} type={'primary'}>提交</Button>
|
||||
</Flex>
|
||||
</Form>
|
||||
</div>
|
||||
<Divider/>
|
||||
<div>
|
||||
<Title level={5}>批量添加</Title>
|
||||
<Paragraph type={'danger'}>请使用Excel模板进行批量添加,模板下载请点击<a
|
||||
href={'/api/v1/teacher/download-template'}>这里</a></Paragraph>
|
||||
<Upload {...fileUploadProps}>
|
||||
<Button icon={<UploadOutlined/>}>上传Excel</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AddUserDrawer.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
setOpen: PropTypes.func.isRequired,
|
||||
commonAxios: PropTypes.object.isRequired,
|
||||
fetchUserInfoList: PropTypes.func.isRequired,
|
||||
messageApi: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default AddUserDrawer;
|
||||
69
src/page/Dashboard/UserManagement/DeleteTeacherModal.jsx
Normal file
69
src/page/Dashboard/UserManagement/DeleteTeacherModal.jsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Modal, Typography} from "antd";
|
||||
|
||||
const DeleteTeacherModal = props => {
|
||||
const {open, setOpen, commonAxios, userInfo, fetchUserInfo, setDrawerOpen, messageApi} = props;
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
|
||||
const [confirmLoading, setConfirmLoading] = React.useState(false);
|
||||
const [cancelButtonProps, setCancelButtonProps] = React.useState({
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
setConfirmLoading(true)
|
||||
setCancelButtonProps({...cancelButtonProps, disabled: true});
|
||||
let url = '/api/v1/teacher/delete/'
|
||||
url += userInfo.staffNumber;
|
||||
commonAxios.delete(url).then(response => {
|
||||
let result = response.data.data || false;
|
||||
if (result) {
|
||||
messageApi.success('删除教师信息成功');
|
||||
setDrawerOpen(false);
|
||||
fetchUserInfo();
|
||||
}
|
||||
setOpen(false);
|
||||
setConfirmLoading(false);
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
title={'删除教师'}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
confirmLoading={confirmLoading}
|
||||
onOk={onSubmit}
|
||||
cancelButtonProps={cancelButtonProps}
|
||||
closable={false}
|
||||
maskClosable={false}
|
||||
cancelText={'取消'}
|
||||
okText={'删除'}
|
||||
>
|
||||
<Text>确认删除该教师吗?</Text>
|
||||
<Paragraph type={'danger'}
|
||||
strong={true}>删除后会同时删除该教师的账号,但不会删除已生成的记录、工作量记录等数据。</Paragraph>
|
||||
</Modal>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DeleteTeacherModal.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
setOpen: PropTypes.func.isRequired,
|
||||
commonAxios: PropTypes.object.isRequired,
|
||||
userInfo: PropTypes.object.isRequired,
|
||||
fetchUserInfo: PropTypes.func.isRequired,
|
||||
setDrawerOpen: PropTypes.func.isRequired,
|
||||
messageApi: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default DeleteTeacherModal;
|
||||
110
src/page/Dashboard/UserManagement/QueryConditionForm.jsx
Normal file
110
src/page/Dashboard/UserManagement/QueryConditionForm.jsx
Normal file
@ -0,0 +1,110 @@
|
||||
import React, {useCallback, useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, Flex, Form, Input, Select} from "antd";
|
||||
import ResourceFinder from "../../../util/ResourceFinder";
|
||||
import {debounce} from "lodash";
|
||||
import {SearchOutlined, UndoOutlined} from "@ant-design/icons";
|
||||
|
||||
const QueryConditionForm = props => {
|
||||
const {queryRequest, setQueryRequest, commonAxios} = props;
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [collegeOptions, setCollegeOptions] = React.useState([]);
|
||||
|
||||
const fetchCollegeList = (keyword) => {
|
||||
let attributes = null;
|
||||
if (keyword) {
|
||||
attributes = {
|
||||
"keyword": keyword
|
||||
}
|
||||
}
|
||||
let resourceFinder = new ResourceFinder('efc.workload.oms.user', 'efc.workload.oms.user.college.list', commonAxios, attributes);
|
||||
resourceFinder.getResource().then(response => {
|
||||
let collegeList = response.data.data.data || [];
|
||||
let options = collegeList.map(item => {
|
||||
return {
|
||||
label: item,
|
||||
value: item
|
||||
}
|
||||
})
|
||||
setCollegeOptions(options);
|
||||
})
|
||||
}
|
||||
|
||||
const onSubmit = (values) => {
|
||||
console.log(values)
|
||||
setQueryRequest({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
staffNumber: values.staffNumber,
|
||||
college: values.college,
|
||||
})
|
||||
}
|
||||
|
||||
const debouncedFetchCollegeList = useCallback(debounce(fetchCollegeList, 150), []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCollegeList()
|
||||
}, [props]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onSubmit}
|
||||
layout={"inline"}
|
||||
style={{
|
||||
width: '100%',
|
||||
display: "flex",
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Flex justify={"start"} align={"start"} gap={"small"}>
|
||||
<Form.Item
|
||||
label={'工号'}
|
||||
name={'staffNumber'}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={'输入工号或工号前缀查询'}
|
||||
allowClear
|
||||
style={{width: 240}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={'学院'}
|
||||
name={'college'}
|
||||
>
|
||||
<Select
|
||||
showSearch={true}
|
||||
onSearch={debouncedFetchCollegeList}
|
||||
allowClear
|
||||
placeholder={'请选择学院'}
|
||||
options={collegeOptions}
|
||||
style={{width: 240}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
|
||||
<Flex justify={"flex-start"} align={"center"} gap={"middle"}>
|
||||
<Form.Item>
|
||||
<Flex justify={"flex-start"} align={"center"} gap={"middle"}>
|
||||
<Button htmlType={"reset"} icon={<UndoOutlined/>}>重置</Button>
|
||||
<Button htmlType={"submit"} type={'primary'} icon={<SearchOutlined/>}>搜索</Button>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
QueryConditionForm.propTypes = {
|
||||
queryRequest: PropTypes.object.isRequired,
|
||||
setQueryRequest: PropTypes.func.isRequired,
|
||||
commonAxios: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default QueryConditionForm;
|
||||
44
src/page/Dashboard/UserManagement/ResetPasswordModal.jsx
Normal file
44
src/page/Dashboard/UserManagement/ResetPasswordModal.jsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, Flex, Modal, Typography} from "antd";
|
||||
|
||||
const ResetPasswordModal = props => {
|
||||
const {newPassword, open, setOpen, setNewPassword} = props;
|
||||
|
||||
const {Paragraph, Title} = Typography;
|
||||
|
||||
const onClose = () => {
|
||||
setOpen(false);
|
||||
setNewPassword('');
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
title={'新密码'}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
onOk={onClose}
|
||||
closable={false}
|
||||
maskClosable={false}
|
||||
okText={'关闭'}
|
||||
footer={<Button key={'submit'} type={"primary"} onClick={onClose}>关闭</Button>}
|
||||
>
|
||||
<Flex justify={"center"} align={"center"}
|
||||
style={{backgroundColor: '#f0f2f5', padding: 10, borderRadius: 8, margin: 10}}>
|
||||
<Title level={1} copyable={true}>{newPassword}</Title>
|
||||
</Flex>
|
||||
<Paragraph type={'danger'}
|
||||
strong={true}>新密码仅会显示一次,请妥善保管,未来不可再次查看。</Paragraph>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ResetPasswordModal.propTypes = {
|
||||
newPassword: PropTypes.string.isRequired,
|
||||
open: PropTypes.bool.isRequired,
|
||||
setOpen: PropTypes.func.isRequired,
|
||||
setNewPassword: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ResetPasswordModal;
|
||||
18
src/page/Dashboard/UserManagement/TeacherRegistered.jsx
Normal file
18
src/page/Dashboard/UserManagement/TeacherRegistered.jsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Badge} from "antd";
|
||||
|
||||
const TeacherRegistered = props => {
|
||||
const {registered} = props;
|
||||
return (
|
||||
<div>
|
||||
<Badge text={registered ? '已注册' : '未注册'} color={registered ? 'green' : 'red'}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TeacherRegistered.propTypes = {
|
||||
registered: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default TeacherRegistered;
|
||||
64
src/page/Dashboard/UserManagement/UserInfoTable.jsx
Normal file
64
src/page/Dashboard/UserManagement/UserInfoTable.jsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, Flex, Pagination, Spin, Table} from "antd";
|
||||
import UserInfoTableColumn from "./UserInfoTableColumn";
|
||||
import AccountInfoDrawer from "./AccountInfoDrawer";
|
||||
import {UserAddOutlined} from "@ant-design/icons";
|
||||
import AddUserDrawer from "./AddUserDrawer";
|
||||
|
||||
const UserInfoTable = props => {
|
||||
|
||||
const {fetchUserInfo, spinLoading, queryRequest, setQueryRequest, queryResponse, messageApi, commonAxios} = props;
|
||||
|
||||
const [drawerOpen, setDrawerOpen] = React.useState(false);
|
||||
const [drawerUserInfo, setDrawerUserInfo] = React.useState({});
|
||||
const [addUserDrawerOpen, setAddUserDrawerOpen] = React.useState(false);
|
||||
|
||||
const openUserInfoDetails = (record) => {
|
||||
setDrawerOpen(true);
|
||||
setDrawerUserInfo(record);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Flex justify={"space-between"} align={"center"} style={{marginBottom: 10}}>
|
||||
<h3>教师管理</h3>
|
||||
<Button shape={'round'} icon={<UserAddOutlined/>} type={'default'}
|
||||
onClick={() => setAddUserDrawerOpen(true)}>添加教师</Button>
|
||||
</Flex>
|
||||
<AccountInfoDrawer fetchUserInfoList={fetchUserInfo} open={drawerOpen} setOpen={setDrawerOpen}
|
||||
userInfoDetails={drawerUserInfo} commonAxios={commonAxios} messageApi={messageApi}/>
|
||||
<AddUserDrawer open={addUserDrawerOpen} setOpen={setAddUserDrawerOpen} commonAxios={commonAxios}
|
||||
fetchUserInfoList={fetchUserInfo} messageApi={messageApi}/>
|
||||
<Spin spinning={spinLoading}>
|
||||
<Table
|
||||
columns={UserInfoTableColumn(openUserInfoDetails)}
|
||||
dataSource={queryResponse.list}
|
||||
pagination={false}
|
||||
/>
|
||||
</Spin>
|
||||
<Pagination
|
||||
defaultCurrent={1}
|
||||
showSizeChanger
|
||||
total={queryResponse.total}
|
||||
align={'end'}
|
||||
style={{marginTop: 20}}
|
||||
onChange={(page, pageSize) => {
|
||||
setQueryRequest({...queryRequest, page: page, size: pageSize});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
UserInfoTable.propTypes = {
|
||||
fetchUserInfo: PropTypes.func.isRequired,
|
||||
spinLoading: PropTypes.bool.isRequired,
|
||||
queryRequest: PropTypes.object.isRequired,
|
||||
setQueryRequest: PropTypes.func.isRequired,
|
||||
queryResponse: PropTypes.object.isRequired,
|
||||
messageApi: PropTypes.object.isRequired,
|
||||
commonAxios: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default UserInfoTable;
|
||||
67
src/page/Dashboard/UserManagement/UserInfoTableColumn.js
Normal file
67
src/page/Dashboard/UserManagement/UserInfoTableColumn.js
Normal file
@ -0,0 +1,67 @@
|
||||
import {Button, Flex} from "antd";
|
||||
import TeacherRegistered from "./TeacherRegistered";
|
||||
|
||||
const UserInfoTableColumn = (openUserInfoDetails) => [
|
||||
{
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
responsive: ['lg'],
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '工号',
|
||||
dataIndex: 'staffNumber',
|
||||
key: 'staffNumber',
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '学院',
|
||||
dataIndex: 'college',
|
||||
key: 'college',
|
||||
responsive: ['lg'],
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '专业',
|
||||
dataIndex: 'department',
|
||||
key: 'department',
|
||||
responsive: ['lg'],
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '研究室',
|
||||
dataIndex: 'researchRoom',
|
||||
key: 'researchRoom',
|
||||
responsive: ['lg'],
|
||||
render: (text) => <span key={text}>{text}</span>
|
||||
},
|
||||
{
|
||||
title: '注册状态',
|
||||
dataIndex: 'registered',
|
||||
key: 'registered',
|
||||
render: (text, record) => <span key={text}><TeacherRegistered registered={record.registered}/></span>
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'options',
|
||||
key: 'options',
|
||||
responsive: ['lg'],
|
||||
render: (text, record) => (
|
||||
<>
|
||||
<Flex justify={"start"} align={"center"}>
|
||||
<Button type={"link"} onClick={() => openUserInfoDetails(record)}>查看详情</Button>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
export default UserInfoTableColumn;
|
||||
72
src/page/Dashboard/UserManagement/index.jsx
Normal file
72
src/page/Dashboard/UserManagement/index.jsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import CardDiv from "../../../component/CardDiv/CardDiv";
|
||||
import {message} from "antd";
|
||||
import UserInfoTable from "./UserInfoTable";
|
||||
import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios";
|
||||
import QueryConditionForm from "./QueryConditionForm";
|
||||
|
||||
const UserManagement = props => {
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const commonAxios = creatMessageCommonAxios(messageApi);
|
||||
|
||||
const [queryRequest, setQueryRequest] = React.useState({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
staffNumber: null,
|
||||
college: null,
|
||||
});
|
||||
const [queryResponse, setQueryResponse] = React.useState({});
|
||||
const [spinLoading, setSpinLoading] = React.useState(true);
|
||||
|
||||
const fetchUserInfo = () => {
|
||||
let url = `/api/v1/teacher/query`;
|
||||
url += `?page=${queryRequest.page}&size=${queryRequest.pageSize}`;
|
||||
if (queryRequest.staffNumber) {
|
||||
url += `&staffNumber=${queryRequest.staffNumber}`;
|
||||
}
|
||||
if (queryRequest.college) {
|
||||
url += `&college=${queryRequest.college}`;
|
||||
}
|
||||
commonAxios.get(url).then((response) => {
|
||||
if (!response.data.data) {
|
||||
setSpinLoading(false)
|
||||
setQueryResponse({});
|
||||
return
|
||||
}
|
||||
let workloadData = response.data.data || {};
|
||||
console.log(workloadData)
|
||||
setQueryResponse(workloadData);
|
||||
setSpinLoading(false)
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setSpinLoading(true)
|
||||
fetchUserInfo();
|
||||
}, [queryRequest]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{contextHolder}
|
||||
<CardDiv>
|
||||
<QueryConditionForm queryRequest={queryRequest} setQueryRequest={setQueryRequest}
|
||||
commonAxios={commonAxios}/>
|
||||
</CardDiv>
|
||||
<CardDiv>
|
||||
<UserInfoTable
|
||||
fetchUserInfo={fetchUserInfo}
|
||||
spinLoading={spinLoading}
|
||||
queryRequest={queryRequest}
|
||||
setQueryRequest={setQueryRequest}
|
||||
queryResponse={queryResponse}
|
||||
messageApi={messageApi}
|
||||
commonAxios={commonAxios}/>
|
||||
</CardDiv>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
UserManagement.propTypes = {};
|
||||
|
||||
export default UserManagement;
|
||||
118
src/page/Dashboard/index.jsx
Normal file
118
src/page/Dashboard/index.jsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Col, Divider, Flex, Layout, message, Row, theme, Typography} from 'antd';
|
||||
import MobileHeader from "../../component/Header/MobileHeader";
|
||||
import LayoutHeader from "../../component/Header/LayoutHeader";
|
||||
import DashboardMenu from "../../component/Menu/DashboardMenu";
|
||||
import {Outlet, useLocation} from 'react-router-dom';
|
||||
import creatMessageCommonAxios from "../../http/CreatMessageCommonAxios";
|
||||
|
||||
const {Content, Sider} = Layout;
|
||||
|
||||
const App = () => {
|
||||
const {
|
||||
token: {colorBgContainer, borderRadiusLG, colorBgBase},
|
||||
} = theme.useToken();
|
||||
const location = useLocation();
|
||||
const pathnames = location.pathname.split('/').filter(x => x);
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const [profile, setProfile] = useState(
|
||||
{
|
||||
id: 3,
|
||||
name: '',
|
||||
staffNumber: '',
|
||||
}
|
||||
);
|
||||
|
||||
const commonAxios = creatMessageCommonAxios(messageApi);
|
||||
|
||||
useEffect(() => {
|
||||
commonAxios.get('/api/auth/profile').then((response) => {
|
||||
if(response.data.data != null) {
|
||||
setProfile({
|
||||
id: response.data.data.id,
|
||||
name: response.data.data.name,
|
||||
staffNumber: response.data.data.staffNumber
|
||||
});
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<Layout style={{height: '100%'}}>
|
||||
{contextHolder}
|
||||
<Row>
|
||||
<Col xs={0} lg={24}>
|
||||
<LayoutHeader profile={profile} commonAxios={commonAxios} messageApi={messageApi}/>
|
||||
</Col>
|
||||
<Col xs={24} lg={0}>
|
||||
<MobileHeader profile={profile} commonAxios={commonAxios} messageApi={messageApi}/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Layout style={{height: '100%'}}>
|
||||
<Sider
|
||||
width={200}
|
||||
style={{
|
||||
background: colorBgContainer
|
||||
}}
|
||||
breakpoint="lg"
|
||||
collapsedWidth="0"
|
||||
onBreakpoint={(broken) => {
|
||||
console.log(broken);
|
||||
}}
|
||||
onCollapse={(collapsed, type) => {
|
||||
console.log(collapsed, type);
|
||||
}}
|
||||
>
|
||||
<DashboardMenu/>
|
||||
</Sider>
|
||||
<Layout
|
||||
style={{
|
||||
padding: '0 24px 24px',
|
||||
}}
|
||||
>
|
||||
<Content
|
||||
style={{
|
||||
padding: 24,
|
||||
marginTop: 20,
|
||||
minHeight: 280,
|
||||
borderRadius: borderRadiusLG,
|
||||
overflow: 'auto',
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: '#888 #f5f5f5',
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: '#f5f5f5',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: '#888',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
background: '#555',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Outlet style={{position: 'relative'}}/>
|
||||
<Layout.Footer style={{background: 'rgba(0,0,0,0)'}}>
|
||||
<Divider/>
|
||||
<Flex vertical justify={'center'} align={'center'} wrap={"wrap"}>
|
||||
<Typography.Text type={"secondary"}>
|
||||
Version 1.2.x</Typography.Text>
|
||||
<Typography.Text type={"secondary"}>Educational Fusion Cloud -- Workload
|
||||
Statistics</Typography.Text>
|
||||
<Typography.Text type={"secondary"}>Powered by ©2023
|
||||
- {new Date().getFullYear()} SimRobot Studio</Typography.Text>
|
||||
</Flex>
|
||||
</Layout.Footer>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
export default App;
|
||||
13
src/reportWebVitals.js
Normal file
13
src/reportWebVitals.js
Normal file
@ -0,0 +1,13 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
13
src/routes/AuthRoutes.js
Normal file
13
src/routes/AuthRoutes.js
Normal file
@ -0,0 +1,13 @@
|
||||
import Login from "../page/Authentication/Login";
|
||||
|
||||
const AuthRoutes = {
|
||||
path: '/auth',
|
||||
children: [
|
||||
{
|
||||
path: 'login',
|
||||
element: <Login/>
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default AuthRoutes;
|
||||
56
src/routes/DashboardRoutes.js
Normal file
56
src/routes/DashboardRoutes.js
Normal file
@ -0,0 +1,56 @@
|
||||
import Dashboard from "../page/Dashboard";
|
||||
import Overview from "../page/Dashboard/Overview";
|
||||
import DataCheck from "../page/Dashboard/DataCheck";
|
||||
import DataPrint from "../page/Dashboard/DataPrint";
|
||||
import DataManager from "../page/Dashboard/DataManager";
|
||||
import {Navigate} from "react-router-dom";
|
||||
import SystemSettings from "../page/Dashboard/SystemSettings";
|
||||
import GenerateCertificate from "../page/Dashboard/GenerateCertificate";
|
||||
import UserManagement from "../page/Dashboard/UserManagement";
|
||||
import AboutUs from "../page/Dashboard/AboutUs";
|
||||
|
||||
const DashboardRoutes = {
|
||||
path: '/',
|
||||
element: <Dashboard/>,
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
element: <Navigate to="/overview" replace/>,
|
||||
index: true
|
||||
},
|
||||
{
|
||||
path: 'overview',
|
||||
element: <Overview/>
|
||||
},
|
||||
{
|
||||
path: 'data-check',
|
||||
element: <DataCheck/>
|
||||
},
|
||||
{
|
||||
path: 'data-print',
|
||||
element: <DataPrint/>,
|
||||
},
|
||||
{
|
||||
path: 'data-maintenance',
|
||||
element: <DataManager/>
|
||||
},
|
||||
{
|
||||
path: 'system-settings',
|
||||
element: <SystemSettings/>
|
||||
},
|
||||
{
|
||||
path: 'generate-certificate',
|
||||
element: <GenerateCertificate />
|
||||
},
|
||||
{
|
||||
path: 'user-management',
|
||||
element: <UserManagement/>
|
||||
},
|
||||
{
|
||||
path: 'about-us',
|
||||
element: <AboutUs/>
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export default DashboardRoutes;
|
||||
7
src/routes/index.js
Normal file
7
src/routes/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import {useRoutes} from "react-router-dom";
|
||||
import AuthRoutes from "./AuthRoutes";
|
||||
import DashboardRoutes from "./DashboardRoutes";
|
||||
|
||||
export default function Routers() {
|
||||
return useRoutes([DashboardRoutes, AuthRoutes])
|
||||
}
|
||||
5
src/setupTests.js
Normal file
5
src/setupTests.js
Normal file
@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
13
src/util/DateFormater.js
Normal file
13
src/util/DateFormater.js
Normal file
@ -0,0 +1,13 @@
|
||||
const DateFormater = (isoDate) => {
|
||||
const date = new Date(isoDate);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
export default DateFormater;
|
||||
23
src/util/ResourceFinder.js
Normal file
23
src/util/ResourceFinder.js
Normal file
@ -0,0 +1,23 @@
|
||||
class ResourceFinder {
|
||||
|
||||
constructor(type, subType, axiosInstance, attributes) {
|
||||
this.type = type;
|
||||
this.subType = subType;
|
||||
this.attributes = attributes;
|
||||
this.axiosInstance = axiosInstance;
|
||||
}
|
||||
|
||||
getResource() {
|
||||
let body = {
|
||||
type: this.type,
|
||||
subType: this.subType,
|
||||
attributes: null,
|
||||
};
|
||||
if (this.attributes) {
|
||||
body.attributes = this.attributes;
|
||||
}
|
||||
return this.axiosInstance.post(`/api/v1/common/res/query`, body);
|
||||
}
|
||||
}
|
||||
|
||||
export default ResourceFinder;
|
||||
Reference in New Issue
Block a user