Compare commits

..

13 Commits

20 changed files with 800 additions and 185 deletions

4
.env.development Normal file
View File

@ -0,0 +1,4 @@
# 本地后端地址
REACT_APP_BACKEND_ADDRESS=http://localhost:8080
# 超时时间
REACT_APP_BACKEND_TIMEOUT=10000

4
.env.production Normal file
View File

@ -0,0 +1,4 @@
# 远程后端地址
REACT_APP_BACKEND_ADDRESS=http://43.138.83.20:10001
# 超时时间
REACT_APP_BACKEND_TIMEOUT=10000

88
nginx配置文件.txt Normal file
View File

@ -0,0 +1,88 @@
server
{
listen 80;
server_name www.workload.hrbnu.club;
merge_slashes on; # 合并连续斜杠(默认开启,但显式声明更稳妥)
index index.php index.html index.htm default.php default.htm default.html;
root /www/wwwroot/EFC/dist/build;
#CERT-APPLY-CHECK--START
# 用于SSL证书申请时的文件验证相关配置 -- 请勿删除
include /www/server/panel/vhost/nginx/well-known/www.workload.hrbnu.club.conf;
#CERT-APPLY-CHECK--END
#SSL-START SSL相关配置请勿删除或修改下一行带注释的404规则
#error_page 404/404.html;
#SSL-END
#ERROR-PAGE-START 错误页配置,可以注释、删除或修改
error_page 404 /404.html;
#error_page 502 /502.html;
#ERROR-PAGE-END
#PHP-INFO-START PHP引用配置可以注释或修改
include enable-php-80.conf;
#PHP-INFO-END
#REWRITE-START URL重写规则引用,修改后将导致面板设置的伪静态规则失效
include /www/server/panel/vhost/rewrite/www.workload.hrbnu.club.conf;
#REWRITE-END
#禁止访问的文件或目录
location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md)
{
return 404;
}
# 处理 history 模式路由:所有请求 fallback 到 index.html
location / {
try_files $uri /index.html;
index index.html;
}
#一键申请SSL证书验证目录相关设置
location ~ \.well-known{
allow all;
}
#禁止在证书验证目录放入敏感文件
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
return 403;
}
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
{
expires 30d;
error_log /dev/null;
access_log /dev/null;
}
location ~ .*\.(js|css)?$
{
expires 12h;
error_log /dev/null;
access_log /dev/null;
}
# 代理后端API请求
# 所有以/api/开头的请求都会被代理到后端服务
location /api/ {
# 后端服务地址
proxy_pass http://43.138.83.20:10001;
# 代理相关设置
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 超时设置
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
# 允许客户端请求体大小
client_max_body_size 10M;
}
access_log /www/wwwlogs/www.workload.hrbnu.club.log;
error_log /www/wwwlogs/www.workload.hrbnu.club.error.log;
}

BIN
public/data-print/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -1,3 +1,3 @@
window.BACKEND_ADDRESS = "http://localhost:8080"; //window.BACKEND_ADDRESS = "http://localhost:8080";
//window.BACKEND_ADDRESS = "http://43.138.83.20:10001"; window.BACKEND_ADDRESS = "http://43.138.83.20:10001";
window.BACKEND_TIMEOUT = 10000; window.BACKEND_TIMEOUT = 10000;

View File

@ -1,7 +1,10 @@
const baseWebConfig ={ // baseWebConfig.js
baseUrl: 'http://localhost:8080', const baseWebConfig = {
//baseUrl: 'http://43.138.83.20:10001', // 从环境变量读取baseUrl本地开发用localhostbuild用ip:10001
timeout: 10000, baseUrl: process.env.REACT_APP_BACKEND_ADDRESS,
} // 读取超时时间(转成数字类型)
timeout: Number(process.env.REACT_APP_BACKEND_TIMEOUT)
export default baseWebConfig; };
console.log('当前环境 baseUrl:', baseWebConfig.baseUrl);
console.log('当前超时时间:', baseWebConfig.timeout);
export default baseWebConfig;

View File

@ -54,7 +54,7 @@ const DashboardMenuItems = [
{ {
key: `/generate-certificate`, key: `/generate-certificate`,
icon: <NotificationOutlined/>, icon: <NotificationOutlined/>,
label: <NavLink to={'/generate-certificate'}>生成</NavLink> label: <NavLink to={'/generate-certificate'}>生成</NavLink>
}, },
{ {
key: `settings`, key: `settings`,

View File

@ -2,10 +2,17 @@ import {Space} from "antd";
import CourseTypeTag from "../../../../component/Workload/CourseTypeTag"; import CourseTypeTag from "../../../../component/Workload/CourseTypeTag";
const CheckTableColumn = [ const CheckTableColumn = [
//{
// title: 'ID',
// dataIndex: 'id',
// key: 'id',
// responsive: ['lg'],
// render: (text) => <span key={text}>{text}</span>
//},
{ {
title: 'ID', title: '学期',
dataIndex: 'id', dataIndex: 'semesterInfo',
key: 'id', key: 'semesterInfo',
responsive: ['lg'], responsive: ['lg'],
render: (text) => <span key={text}>{text}</span> render: (text) => <span key={text}>{text}</span>
}, },
@ -36,6 +43,20 @@ const CheckTableColumn = [
responsive: ['lg'], responsive: ['lg'],
render: (text) => <span key={text}>{text}</span> render: (text) => <span key={text}>{text}</span>
}, },
{
title: '学生数',
dataIndex: 'actualClassSize',
key: 'actualClassSize',
responsive: ['lg'],
render: (text) => <span key={text}>{text}</span>
},
{
title: '授课对象',
dataIndex: 'teachingGrade',
key: 'teachingGrade',
responsive: ['lg'],
render: (text) => <span key={text}>{text}</span>
},
{ {
title: '总学时', title: '总学时',
dataIndex: 'totalClassHours', dataIndex: 'totalClassHours',

View File

@ -0,0 +1,158 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Drawer, Flex, Form, Input, Select, Typography } from "antd";
import CourseTypeTag from "../../../component/Workload/CourseTypeTag";
const AddDataManageDrawer = props => {
const { open, setOpen, commonAxios, fetchWorkload, messageApi } = props;
// 表单布局配置
const formLayout = {
labelCol: { span: 5 },
wrapperCol: { span: 20 },
};
const [form] = Form.useForm();
const { Title } = Typography;
// 课程性质选项
const courseNatureOptions = [
{ label: '公共课', value: '01' },
{ label: '专业课', value: '02' },
{ label: '校选课', value: '03' },
];
// 当抽屉关闭时重置表单
React.useEffect(() => {
if (!open) {
form.resetFields();
}
}, [open, form]);
// 提交添加表单
const onSubmit = (values) => {
commonAxios.post('/api/v1/workload/addWorkload', values).then(response => {
const result = response.data.data || false;
if (result) {
messageApi.success('添加工作量信息成功');
setOpen(false);
fetchWorkload(); // 刷新列表
} else {
messageApi.error('添加工作量信息失败');
}
}).catch(error => {
messageApi.error('网络错误,添加失败');
});
};
return (
<Drawer
open={open}
onClose={() => setOpen(false)}
title={'添加工作量'}
destroyOnClose
>
<div>
<Title level={5}>添加工作量信息</Title>
<Form
form={form}
{...formLayout}
onFinish={onSubmit}
>
<Form.Item
label="学期"
name="semesterInfo"
rules={[{ required: true, message: '请输入学期' }]}
>
<Input placeholder="请输入学期(示例2023-2024-1)"/>
</Form.Item>
<Form.Item
label="授课名称"
name="courseName"
rules={[{ required: true, message: '请输入授课名称' }]}
>
<Input placeholder="请输入授课名称"/>
</Form.Item>
<Form.Item
label="姓名"
name="teacherName"
rules={[{ required: true, message: '请输入姓名' }]}
>
<Input placeholder="请输入姓名"/>
</Form.Item>
<Form.Item
label="工号"
name="stuffNumber"
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="courseNature"
rules={[{ required: true, message: '请选择课程性质' }]}
>
<Select
placeholder="请选择课程性质"
options={courseNatureOptions}
formatLabel={(option) => <CourseTypeTag courseNature={option.value} />}
/>
</Form.Item>
<Form.Item
label="授课专业"
name="teachingMajor"
rules={[{ required: true, message: '请输入授课专业' }]}
>
<Input placeholder="请输入授课专业"/>
</Form.Item>
<Form.Item
label="学生数"
name="actualClassSize"
rules={[
{ required: true, message: '请输入学生数' }
]}
>
<Input type="number" placeholder="请输入学生数"/>
</Form.Item>
<Form.Item
label="授课对象"
name="teachingGrade"
rules={[{ required: true, message: '请输入授课对象' }]}
>
<Input placeholder="请输入授课对象"/>
</Form.Item>
<Form.Item
label="工作量"
name="totalClassHours"
rules={[
{ required: true, message: '请输入工作量' }
]}
>
<Input type="number" placeholder="请输入工作量"/>
</Form.Item>
<Flex justify={"center"} align={"center"}>
<Button onClick={() => setOpen(false)} style={{ marginRight: 8 }}>取消</Button>
<Button htmlType={'submit'} type={'primary'}>添加</Button>
</Flex>
</Form>
</div>
</Drawer>
);
};
AddDataManageDrawer.propTypes = {
open: PropTypes.bool.isRequired,
setOpen: PropTypes.func.isRequired,
commonAxios: PropTypes.func.isRequired,
fetchWorkload: PropTypes.func.isRequired,
messageApi: PropTypes.object.isRequired
};
export default AddDataManageDrawer;

View File

@ -2,7 +2,9 @@ import React, {useState} from 'react';
import {Button, Flex, message, Popconfirm, Table, Typography} from "antd"; import {Button, Flex, message, Popconfirm, Table, Typography} from "antd";
import ManageTableColumn from "./ManageTableColumn"; import ManageTableColumn from "./ManageTableColumn";
import ImportDataDrawer from "./ImportDataDrawer"; import ImportDataDrawer from "./ImportDataDrawer";
import EditDataManageDrawer from './EditDataManageDrawer';
import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios"; import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios";
import AddDataManageDrawer from './AddDataManageDrawer'; // 导入新增的组件
const DataManageTable = props => { const DataManageTable = props => {
@ -13,6 +15,12 @@ const DataManageTable = props => {
const [selectIds, setSelectIds] = useState([]); const [selectIds, setSelectIds] = useState([]);
// 新增编辑抽屉相关状态
const [editDrawerOpen, setEditDrawerOpen] = useState(false);
const [currentEditData, setCurrentEditData] = useState({});
// 添加添加数据抽屉的状态
const [addDrawerOpen, setAddDrawerOpen] = useState(false);
const rowSelection = { const rowSelection = {
type: 'checkbox', type: 'checkbox',
onChange: (selectedRowKeys, selectedRows) => { onChange: (selectedRowKeys, selectedRows) => {
@ -34,7 +42,12 @@ const DataManageTable = props => {
} }
}) })
} }
// 编辑按钮点击处理(关键:此函数将打开抽屉)
const handleEdit = (record) => {
console.log("编辑数据:", record); // 用于调试,确认是否触发
setCurrentEditData(record);
setEditDrawerOpen(true); // 打开抽屉
};
const [importDataOpen, setImportDataOpen] = useState(false); const [importDataOpen, setImportDataOpen] = useState(false);
const importDataDrawerOnClose = () => { const importDataDrawerOnClose = () => {
@ -48,20 +61,38 @@ const DataManageTable = props => {
open={importDataOpen} open={importDataOpen}
onClose={importDataDrawerOnClose} onClose={importDataDrawerOnClose}
fetchWorkloadData={fetWorkload} fetchWorkloadData={fetWorkload}
/>
{/* 编辑抽屉 */}
<EditDataManageDrawer
open={editDrawerOpen}
setOpen={setEditDrawerOpen}
commonAxios={commonAxios}
fetchWorkload={fetWorkload}
messageApi={messageApi}
initialValues={currentEditData}
/>
{/* 添加数据抽屉 */}
<AddDataManageDrawer
open={addDrawerOpen}
setOpen={setAddDrawerOpen}
commonAxios={commonAxios}
fetchWorkload={fetWorkload}
messageApi={messageApi}
/> />
<Flex justify={"space-between"} align={"center"} gap={"middle"}> <Flex justify={"space-between"} align={"center"} gap={"middle"}>
<h3>数据管理</h3> <h3>数据管理</h3>
<Flex justify={"flex-end"} align={"center"} gap={"middle"}> <Flex justify={"flex-end"} align={"center"} gap={"middle"}>
<Typography.Text>{workloadData.total}条数据</Typography.Text> <Typography.Text>{workloadData.total}条数据</Typography.Text>
{/* 添加"添加数据"按钮 */}
<Button type={"primary"} onClick={() => setAddDrawerOpen(true)}>添加数据</Button>
<Button type={"primary"} onClick={() => setImportDataOpen(true)}>导入</Button> <Button type={"primary"} onClick={() => setImportDataOpen(true)}>导入</Button>
</Flex> </Flex>
</Flex> </Flex>
{/* 关键修复:传递 handleEdit 到表格列 */}
<Table <Table
rowSelection={ rowSelection={rowSelection}
rowSelection
}
rowKey={(record) => record.id} rowKey={(record) => record.id}
columns={ManageTableColumn(commonAxios, messageApi, fetWorkload)} columns={ManageTableColumn(commonAxios, messageApi, fetWorkload, handleEdit)} // 新增 handleEdit 参数
dataSource={workloadData.list} dataSource={workloadData.list}
pagination={false} pagination={false}
/> />

View File

@ -0,0 +1,162 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Drawer, Flex, Form, Input, Select, Typography } from "antd";
import CourseTypeTag from "../../../component/Workload/CourseTypeTag";
const EditDataManageDrawer = props => {
const { open, setOpen, commonAxios, fetchWorkload, messageApi, initialValues } = props;
// 表单布局配置,与添加组件保持一致
const formLayout = {
labelCol: { span: 5 },
wrapperCol: { span: 20 },
};
const [form] = Form.useForm();
const { Title } = Typography;
// 课程性质选项(根据实际业务调整)
const courseNatureOptions = [
{ label: '公共课', value: '01' },
{ label: '专业课', value: '02' },
{ label: '校选课', value: '03' },
];
// 当抽屉打开且有初始值时,填充表单数据
React.useEffect(() => {
if (open && initialValues) {
form.setFieldsValue({
semesterInfo: initialValues.semesterInfo,
courseName: initialValues.courseName,
teacherName: initialValues.teacherName,
courseNature: initialValues.courseNature,
teachingMajor: initialValues.teachingMajor,
actualClassSize: initialValues.actualClassSize,
teachingGrade: initialValues.teachingGrade,
totalClassHours: initialValues.totalClassHours
});
} else {
form.resetFields();
}
}, [open, initialValues, form]);
// 提交编辑表单
const onSubmit = (values) => {
// 拼接ID用于后端识别更新对象
const updateData = { ...values, id: initialValues.id, stuffNumber: initialValues.stuffNumber };
commonAxios.put('/api/v1/workload/updateWordload', updateData).then(response => {
const result = response.data.data || false;
if (result) {
messageApi.success('编辑工作量信息成功');
setOpen(false);
fetchWorkload(); // 刷新列表
} else {
messageApi.error('编辑工作量信息失败');
}
}).catch(error => {
messageApi.error('网络错误,编辑失败');
});
};
return (
<Drawer
open={open}
onClose={() => setOpen(false)}
title={'编辑工作量'}
destroyOnClose
>
<div>
<Title level={5}>编辑工作量信息</Title>
<Form
form={form}
{...formLayout}
onFinish={onSubmit}
>
<Form.Item
label="学期"
name="semesterInfo"
rules={[{ required: true, message: '请输入学期' }]}
>
<Input placeholder="请输入学期"/>
</Form.Item>
<Form.Item
label="授课名称"
name="courseName"
rules={[{ required: true, message: '请输入授课名称' }]}
>
<Input placeholder="请输入授课名称"/>
</Form.Item>
<Form.Item
label="姓名"
name="teacherName"
rules={[{ required: true, message: '请输入姓名' }]}
>
<Input placeholder="请输入姓名"/>
</Form.Item>
<Form.Item
label="课程性质"
name="courseNature"
rules={[{ required: true, message: '请选择课程性质' }]}
>
<Select
placeholder="请选择课程性质"
options={courseNatureOptions}
// 保持与表格中一致的标签显示
formatLabel={(option) => <CourseTypeTag courseNature={option.value} />}
/>
</Form.Item>
<Form.Item
label="授课专业"
name="teachingMajor"
rules={[{ required: true, message: '请输入授课专业' }]}
>
<Input placeholder="请输入授课专业"/>
</Form.Item>
<Form.Item
label="学生数"
name="actualClassSize"
rules={[
{ required: true, message: '请输入学生数' },
//{ type: 'number', message: '请输入数字' }
]}
>
<Input type="number" placeholder="请输入学生数"/>
</Form.Item>
<Form.Item
label="授课对象"
name="teachingGrade"
rules={[{ required: true, message: '请输入授课对象' }]}
>
<Input placeholder="请输入授课对象"/>
</Form.Item>
<Form.Item
label="工作量"
name="totalClassHours"
rules={[
{ required: true, message: '请输入工作量' },
//{ type: 'number', message: '请输入数字' }
]}
>
<Input type="number" placeholder="请输入工作量"/>
</Form.Item>
<Flex justify={"center"} align={"center"}>
<Button onClick={() => setOpen(false)} style={{ marginRight: 8 }}>取消</Button>
<Button htmlType={'submit'} type={'primary'}>保存</Button>
</Flex>
</Form>
</div>
</Drawer>
);
};
// 属性校验,与添加组件保持一致
EditDataManageDrawer.propTypes = {
open: PropTypes.bool.isRequired,
setOpen: PropTypes.func.isRequired,
commonAxios: PropTypes.func.isRequired,
fetchWorkload: PropTypes.func.isRequired,
messageApi: PropTypes.object.isRequired,
initialValues: PropTypes.object // 编辑的初始数据
};
export default EditDataManageDrawer;

View File

@ -4,6 +4,7 @@ import {Button, Divider, Drawer, Form, Input, message, Radio, Space, Upload} fro
import {UploadOutlined} from "@ant-design/icons"; import {UploadOutlined} from "@ant-design/icons";
import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios"; import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios";
import ImportHistoryTable from "./ImportHistoryTable"; import ImportHistoryTable from "./ImportHistoryTable";
import { Content } from 'antd/es/layout/layout';
const ImportDataDrawer = props => { const ImportDataDrawer = props => {
@ -119,7 +120,32 @@ const ImportDataDrawer = props => {
> >
<Button icon={<UploadOutlined/>}>点击此处上传</Button> <Button icon={<UploadOutlined/>}>点击此处上传</Button>
</Upload> </Upload>
</Form.Item> </Form.Item>
<div style={{
textAlign: 'center',
marginTop: '8px',
color: 'red',
fontSize: '12px',
lineHeight: '1.5',
paddingLeft: '2px' // 与按钮左对齐
}}>
<p>文件名格式要求2023-2024-1xxxx.xlsx 支持.xlsx/.xls</p>
<p>教师信息数据不要有合并单元格</p>
</div>
{/* 提示性图片(示例图) */}
<div style={{ marginTop: '8px', border: '1px solid #eee', borderRadius: '4px', display: 'inline-block' }}>
<img
src="/data-print/image.png" // 替换为你的提示图片路径
alt="文件名格式示例"
style={{
maxWidth: '700px', // 控制图片宽度
height: 'auto',
padding: '8px'
}}
/>
</div>
</Form> </Form>
</div> </div>
<Divider/> <Divider/>

View File

@ -1,11 +1,19 @@
import {Button, Space} from "antd"; import {Button, Space} from "antd";
import { EditOutlined } from "@ant-design/icons"; // 引入编辑图标(解决第一个错误)
import CourseTypeTag from "../../../component/Workload/CourseTypeTag"; import CourseTypeTag from "../../../component/Workload/CourseTypeTag";
const ManageTableColumn = (commonAxios, messageApi, fetchWorkload) => [ const ManageTableColumn = (commonAxios, messageApi, fetchWorkload, handleEdit) => [
//{
// title: 'ID',
// dataIndex: 'id',
// key: 'id',
// responsive: ['lg'],
// render: (text) => <span key={text}>{text}</span>
//},
{ {
title: 'ID', title: '学期',
dataIndex: 'id', dataIndex: 'semesterInfo',
key: 'id', key: 'semesterInfo',
responsive: ['lg'], responsive: ['lg'],
render: (text) => <span key={text}>{text}</span> render: (text) => <span key={text}>{text}</span>
}, },
@ -36,6 +44,20 @@ const ManageTableColumn = (commonAxios, messageApi, fetchWorkload) => [
responsive: ['lg'], responsive: ['lg'],
render: (text) => <span key={text}>{text}</span> render: (text) => <span key={text}>{text}</span>
}, },
{
title: '学生数',
dataIndex: 'actualClassSize',
key: 'actualClassSize',
responsive: ['lg'],
render: (text) => <span key={text}>{text}</span>
},
{
title: '授课对象',
dataIndex: 'teachingGrade',
key: 'teachingGrade',
responsive: ['lg'],
render: (text) => <span key={text}>{text}</span>
},
{ {
title: '工作量', title: '工作量',
dataIndex: 'totalClassHours', dataIndex: 'totalClassHours',
@ -46,24 +68,39 @@ const ManageTableColumn = (commonAxios, messageApi, fetchWorkload) => [
title: '操作', title: '操作',
dataIndex: 'operation', dataIndex: 'operation',
key: 'operation', key: 'operation',
render: (_, record) => ( render: (_, record) => {
<Space size={"middle"}> // 确保handleEdit存在再调用避免参数未传递时出错
<Button danger onClick={() => { const handleEditClick = () => {
let id = record.id; if (typeof handleEdit === 'function') {
let param = []; handleEdit(record);
param.push(id); }
let url = `/api/v1/workload/delete` };
commonAxios.delete(url, {data: param}).then(res => {
if (res.data.data && res.data.data === true) { return (
messageApi.success("删除成功"); <Space size={"middle"}>
fetchWorkload() <Button
} else { type="primary"
messageApi.warning('您要删除的数据不存在或已被删除!'); icon={<EditOutlined />}
} onClick={handleEditClick}
}) >
}}>删除</Button> 编辑
</Space> </Button>
) <Button danger onClick={() => {
let id = record.id;
let param = [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>
);
}
}, },
]; ];

View File

@ -154,8 +154,22 @@ function QueryConditionBox(props) {
</Row> </Row>
<Flex justify={"space-between"} align={"center"}> <Flex justify={"space-between"} align={"center"}>
<Flex justify={"start"} align={"center"}> <Flex justify={"start"} align={"center"}>
<Button icon={<PlusOutlined/>} {/*修改生成新证明按钮的点击事件*/}
onClick={() => navigate('/generate-certificate')}>生成新报告</Button> <Button
icon={<PlusOutlined />}
onClick={() => {
const selectedStaffNumbers = form.getFieldValue('staffNumber') || [];
const baseUrl = '/generate-certificate';
// 携带工号和来源标记(用于区分跳转场景)
if (selectedStaffNumbers.length > 0) {
navigate(`${baseUrl}?staffNumber=${selectedStaffNumbers[0]}&from=print`);
} else {
navigate(baseUrl);
}
}}
>
生成新证明
</Button>
</Flex> </Flex>
<Flex justify={"center"} align={"center"} gap={"large"}> <Flex justify={"center"} align={"center"} gap={"large"}>
<Button type={"primary"} htmlType={"submit"} icon={<SearchOutlined/>}>搜索</Button> <Button type={"primary"} htmlType={"submit"} icon={<SearchOutlined/>}>搜索</Button>

View File

@ -31,6 +31,29 @@ const CardAction = (record, messageApi, navigator) => {
}); });
} }
const downloadWordRecord = (recordId) => {
let baseUrl = baseWebConfig.baseUrl
commonAxios.get(`${baseUrl}/api/v1/workload/certificate/downloadWord/${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}.docx`;
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) => { const reGenerateRecord = (record) => {
if (record.status === '01' || record.status === '02') { if (record.status === '01' || record.status === '02') {
@ -83,7 +106,17 @@ const CardAction = (record, messageApi, navigator) => {
> >
<RetweetOutlined/> <RetweetOutlined/>
</Popconfirm>] : []), </Popconfirm>] : []),
...(record.status === '02' ? [<DownloadOutlined onClick={() => downloadRecord(record.id)}/>] : []), ...(record.status === '02' ? [
<span onClick={() => downloadRecord(record.id)} style={{ marginRight: 16, cursor: 'pointer' }}>
<DownloadOutlined style={{ marginRight: 4 }} />
下载PDF
</span>,
//downloadWordRecord
<span onClick={() => downloadWordRecord(record.id)} style={{ cursor: 'pointer' }}>
<DownloadOutlined style={{ marginRight: 4 }} />
下载Word
</span>
] : []),
...(record.status === '03' || record.status === '04' ...(record.status === '03' || record.status === '04'
? [<Popconfirm ? [<Popconfirm
title="删除证明记录" title="删除证明记录"

View File

@ -26,7 +26,7 @@ function PrintRecordCard(props) {
<Card <Card
bordered={true} bordered={true}
bodyStyle={{ bodyStyle={{
height: 200 height: 250
}} }}
style={{minWidth: 400}} style={{minWidth: 400}}
hoverable={true} hoverable={true}
@ -56,6 +56,12 @@ function PrintRecordCard(props) {
<div> <div>
<Text strong>教师工号: </Text><Text>{record.stuffNumber}</Text> <Text strong>教师工号: </Text><Text>{record.stuffNumber}</Text>
</div> </div>
<div>
<Text strong>教师姓名: </Text><Text>{record.teacherName}</Text>
</div>
<div>
<Text strong>学院: </Text><Text>{record.college}</Text>
</div>
<div><Text strong>请求时间: </Text><Text>{DateFormater(record.requestTime)}</Text></div> <div><Text strong>请求时间: </Text><Text>{DateFormater(record.requestTime)}</Text></div>
<div><Text <div><Text
strong>生成时间: </Text><Text>{record.status === '02' ? DateFormater(record.madeTime) : 'N/A'}</Text> strong>生成时间: </Text><Text>{record.status === '02' ? DateFormater(record.madeTime) : 'N/A'}</Text>

View File

@ -8,7 +8,8 @@ import {PlusOutlined} from "@ant-design/icons";
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
const DataPrint = props => { const DataPrint = props => {
const [downloadDisabled, setDownloadDisabled] = useState(true); //const [downloadDisabled, setDownloadDisabled] = useState(true);
const [setDownloadDisabled] = useState(true);
const previewClicked = () => { const previewClicked = () => {
setDownloadDisabled(false); setDownloadDisabled(false);
} }

View File

@ -1,145 +1,171 @@
import React, {useEffect} from 'react'; import React, { useEffect, useState, useRef } from 'react';
import {Button, Flex, Form, Input, message, Spin, Table, Typography} from "antd"; import { Button, Flex, Form, Input, message, Spin, Table, Typography } from "antd";
import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios"; import creatMessageCommonAxios from "../../../http/CreatMessageCommonAxios";
import { useSearchParams } from 'react-router-dom';
const ChooseUser = props => { const ChooseUser = props => {
const { allowNext, request, setRequest } = props;
const [searchParams] = useSearchParams(); // 获取URL参数
const [autoProcessed, setAutoProcessed] = useState(false); // 防止重复处理
const { Title, Text } = Typography;
const [messageApi, contextHolder] = message.useMessage();
const [form] = Form.useForm();
const dataFetchRef = useRef(false); // 防止重复请求
const commonAxios = creatMessageCommonAxios(messageApi);
// 初始加载标记
const [isInitialLoad, setIsInitialLoad] = useState(true);
// 新增用于存储当前选中的行ID控制表格视觉选中状态
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
hidden: true,
},
{
title: '工号',
dataIndex: 'staffNumber',
width: 150
},
{
title: '姓名',
dataIndex: 'name',
width: 200
},
{
title: '学院',
dataIndex: 'college',
},
{
title: '系别',
dataIndex: 'department',
width: 300
},
];
const {allowNext, request, setRequest} = props; const [staffInfo, setStaffInfo] = useState([]);
const [staffNumberPrefix, setStaffNumberPrefix] = useState("");
const [spinning, setSpinning] = useState(true);
const {Title, Text} = Typography; // 处理选中行逻辑(同时更新表格选中状态)
const [messageApi, contextHolder] = message.useMessage(); const handleRowSelect = (selectedRowKeys, selectedRows) => {
const [form] = Form.useForm(); console.log(`选中行: ${selectedRowKeys}`, selectedRows);
// 1. 更新按钮状态和请求参数
allowNext(selectedRows.length > 0);
if (selectedRows.length > 0) {
setRequest({ ...request, stuffNumber: selectedRows[0].staffNumber });
}
// 2. 更新表格选中状态(关键:让表格知道哪些行被选中)
setSelectedRowKeys(selectedRowKeys);
};
const commonAxios = creatMessageCommonAxios(messageApi); // 行选择配置使用selectedRowKeys控制选中状态
const rowSelection = {
onChange: handleRowSelect,
selectedRowKeys: selectedRowKeys, // 绑定选中行ID数组
defaultSelectedRowKeys: () => [],
};
const columns = [ // 通用搜索函数(自动选中时同步更新表格状态)
{ const fetchStaffData = (staffNumber) => {
title: 'ID', if (dataFetchRef.current) return;
dataIndex: 'id', dataFetchRef.current = true;
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([]); setSpinning(true);
const params = staffNumber ? `&staffNumber=${staffNumber}` : '';
const [staffNumberPrefix, setStaffNumberPrefix] = React.useState(""); commonAxios.get(`/api/auth/query/registered?page=1&size=10${params}`)
const [spinning, setSpinning] = React.useState(true); .then((response) => {
if (response.data.data?.list) {
const rowSelection = { const list = response.data.data.list.map(item => ({ ...item, key: item.id }));
onChange: (selectedRowKeys, selectedRows) => { setStaffInfo(list);
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); return list;
allowNext(selectedRows.length > 0); } else {
setRequest({...request, stuffNumber: selectedRows[0].staffNumber}); setStaffInfo([]);
sessionStorage.setItem('chooseUser', selectedRowKeys) return [];
sessionStorage.removeItem('certificateParam'); }
sessionStorage.removeItem('targetKeys'); })
sessionStorage.removeItem('generate-request'); .then((list) => {
}, const fromPrint = searchParams.get('from') === 'print';
defaultSelectedRowKeys: () => { if (fromPrint && list.length === 1 && !autoProcessed) {
const selectedRowKeys = []; // 自动选中时,同步更新表格选中状态
let selected = sessionStorage.getItem('chooseUser'); const targetRowKey = [list[0].id];
if (selected) { handleRowSelect(targetRowKey, [list[0]]); // 触发选中逻辑
// 转换为number setSelectedRowKeys(targetRowKey); // 强制更新表格选中状态
selectedRowKeys.push(selected); setAutoProcessed(true);
allowNext(selectedRowKeys.length > 0); }
} })
return selectedRowKeys; .catch(() => {
} messageApi.error('搜索失败,请重试');
setStaffInfo([]);
})
.finally(() => {
setSpinning(false);
dataFetchRef.current = false;
});
}; };
// 监听URL参数从打印界面跳转时处理
useEffect(() => { useEffect(() => {
commonAxios.get(`/api/auth/query/registered?page=1&size=10`).then((response) => { const staffNumber = searchParams.get('staffNumber');
if (response.data.data != null) { const fromPrint = searchParams.get('from') === 'print'; // 标记来自打印界面
let list = response.data.data.list;
// 为list设置key=id
list.forEach((item) => {
item.key = item.id;
});
setStaffInfo(list);
} else {
setStaffInfo([]);
}
});
setSpinning(false);
}, []);
// 优先处理:如果有工号参数且来自打印界面,执行搜索
if (staffNumber && fromPrint && !autoProcessed) {
form.setFieldsValue({ 'staff-number': staffNumber });
setStaffNumberPrefix(staffNumber);
fetchStaffData(staffNumber);
setIsInitialLoad(false); // 已处理参数,无需再加载全部
}
// 次要处理:无参数时,初始加载全部数据(仅执行一次)
else if (isInitialLoad) {
fetchStaffData(''); // 空参数表示查询全部
setIsInitialLoad(false);
}
}, [searchParams, form, autoProcessed, isInitialLoad]); // 添加isInitialLoad依赖
// 手动搜索
const onSearch = () => { const onSearch = () => {
setSpinning(true); fetchStaffData(staffNumberPrefix);
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} return (
<Flex vertical justiffy={"start"} align={"start"} gap={"large"}> <div>
<div> {contextHolder}
<Title level={4}>选择用户</Title> <Flex vertical justify={"start"} align={"start"} gap={"large"}>
<Text level={4} <div>
type={'secondary'}>指示EFC系统为何人生成报告选择后这份报告将会出现在您和该同事的EFC系统证明管理画面中</Text> <Title level={4}>选择用户</Title>
</div> <Text type={'secondary'}>
<Form 指示EFC系统为何人生成报告选择后这份报告将会出现在您和该同事的EFC系统证明管理画面中
form={form} </Text>
layout={"inline"} </div>
> <Form form={form} layout={"inline"}>
<Form.Item <Form.Item name='staff-number'>
name='staff-number' <Input
> placeholder={"工号"}
<Input placeholder={"工号"} onChange={(event) => setStaffNumberPrefix(event.target.value)}/> onChange={(e) => setStaffNumberPrefix(e.target.value)}
</Form.Item> onPressEnter={onSearch} // 支持回车搜索
<Button type={"primary"} onClick={() => onSearch()}>搜索</Button> />
</Form> </Form.Item>
<div style={{width: '100%'}}> <Button type={"primary"} onClick={onSearch}>搜索</Button>
<Spin spinning={spinning}> </Form>
<Table <div style={{ width: '100%' }}>
columns={columns} <Spin spinning={spinning}>
dataSource={staffInfo} <Table
pagination={false} columns={columns}
rowSelection={{ dataSource={staffInfo}
type: 'radio', pagination={false}
...rowSelection, rowSelection={{ type: 'radio', ...rowSelection }}
}} locale={{ emptyText: '暂无数据,请输入工号搜索' }}
/> />
</Spin> </Spin>
</div> </div>
</Flex> </Flex>
</div> </div>
) );
; };
}
;
ChooseUser.propTypes = {}; ChooseUser.propTypes = {};

View File

@ -97,8 +97,8 @@ const ParameterConfig = props => {
} }
}} }}
options={[ options={[
{label: '本科课堂时证明', value: '01'}, {label: '哈尔滨师范大学本科课堂教学课时证明', value: '01'},
{label: '任现职后工作情况证明', value: '02'} {label: '任现职、近五年完成教育教学工作情况(本科)', value: '02'}
]} ]}
/> />
</Form.Item> </Form.Item>

View File

@ -15,14 +15,15 @@ const GenerateCertificate = props => {
const navigate = useNavigate(); const navigate = useNavigate();
const [current, setCurrent] = React.useState(() => { //const [current, setCurrent] = React.useState(() => {
let nowStep = sessionStorage.getItem('nowStep'); // let nowStep = sessionStorage.getItem('nowStep');
if (nowStep === null) { // if (nowStep === null) {
return 0; // return 0;
} else { // } else {
return parseInt(nowStep); // return parseInt(nowStep);
} // }
}); //});
const [current, setCurrent] = React.useState(0);
const [request, setRequest] = React.useState({ const [request, setRequest] = React.useState({
ids: [], ids: [],
@ -36,7 +37,7 @@ const GenerateCertificate = props => {
const [result, setResult] = React.useState({}); const [result, setResult] = React.useState({});
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const [pageConfig, setPageConfig] = React.useState({}); //const [pageConfig, setPageConfig] = React.useState({});
const steps = [ const steps = [
{ {
@ -159,7 +160,7 @@ const GenerateCertificate = props => {
const onPrev = () => { const onPrev = () => {
let nextCurrent = current - 1; let nextCurrent = current - 1;
setCurrent(nextCurrent); setCurrent(nextCurrent);
let editFlag = sessionStorage.getItem('edit-flag'); //let editFlag = sessionStorage.getItem('edit-flag');
if (nextCurrent <= 0) { if (nextCurrent <= 0) {
setCurrent(0) setCurrent(0)
setPreButton({...preButton, disabled: true,}) setPreButton({...preButton, disabled: true,})