初始化

This commit is contained in:
Vectorune
2025-09-13 16:18:30 +08:00
commit 754f4d97b3
91 changed files with 29581 additions and 0 deletions

38
src/App.css Normal file
View 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
View 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
View 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();
});

View 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;

View File

@ -0,0 +1,3 @@
div {
}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -0,0 +1,6 @@
const baseWebConfig ={
baseUrl: 'http://localhost:8080',
timeout: 10000,
}
export default baseWebConfig;

View 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;

View 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
View 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
View 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
View 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

View 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
View 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;

View File

@ -0,0 +1,3 @@
.login-layout{
text-align: center;
}

View 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;

View 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;

View File

@ -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;

View File

@ -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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -0,0 +1,13 @@
import React from 'react';
const NoticeManager = props => {
return (
<div>
</div>
);
};
NoticeManager.propTypes = {};
export default NoticeManager;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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
View 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
View File

@ -0,0 +1,13 @@
import Login from "../page/Authentication/Login";
const AuthRoutes = {
path: '/auth',
children: [
{
path: 'login',
element: <Login/>
}
]
}
export default AuthRoutes;

View 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
View 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
View 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
View 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;

View 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;