diff --git a/index.html b/index.html index 0c589ec..57e86fa 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,10 @@ - + - Vite + React + 电科院材料所实验室仪器设备预约系统
diff --git a/package.json b/package.json index 54211b9..ee94b9e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "equip-reserve-frontend", "private": true, - "version": "1.0.0", + "version": "1.0.1", "type": "module", "scripts": { "dev": "vite", diff --git a/src/config/menuConfig.js b/src/config/menuConfig.js index 004c546..c691869 100644 --- a/src/config/menuConfig.js +++ b/src/config/menuConfig.js @@ -1,5 +1,6 @@ import { ApartmentOutlined, + ContainerOutlined, DesktopOutlined, ExperimentOutlined, FileDoneOutlined, @@ -14,13 +15,15 @@ const menuConfig = [ path: "/user/reserve", label: "设备预约", icon: DesktopOutlined, - roles: ["USER"], + roles: ["USER", "LEADER", "DEVICE_ADMIN"], + order: 4, }, { path: "/user/my-reservation", label: "我的预约", - icon: UnorderedListOutlined, - roles: ["USER"], + icon: ContainerOutlined, + roles: ["USER", "LEADER", "DEVICE_ADMIN"], + order: 5, }, { @@ -28,36 +31,43 @@ const menuConfig = [ label: "预约审批", icon: FileDoneOutlined, roles: ["LEADER", "DEVICE_ADMIN"], + order: 1, }, { path: "/my-approval", label: "审批记录", icon: UnorderedListOutlined, roles: ["LEADER", "DEVICE_ADMIN"], + order: 2, }, { path: "/device-manage", label: "设备管理", icon: ExperimentOutlined, roles: ["DEVICE_ADMIN"], + order: 3, }, { path: "/admin/user-manage", label: "用户管理", icon: UsergroupAddOutlined, roles: ["ADMIN"], + order: 1, }, { label: "数据统计", icon: PieChartOutlined, + order: 2, children: [ { path: "/admin/stats-device", label: "设备统计", + order: 1, }, { path: "/admin/stats-reservation", label: "使用人统计", + order: 2, }, ], roles: ["ADMIN"], @@ -67,12 +77,14 @@ const menuConfig = [ icon: ApartmentOutlined, label: "团队管理", roles: ["ADMIN"], + order: 3, }, { path: "/userdetail", label: "个人信息", icon: UserOutlined, roles: ["USER", "LEADER", "DEVICE_ADMIN", "ADMIN"], + order: 999, }, ]; diff --git a/src/layouts/CommonLayout.jsx b/src/layouts/CommonLayout.jsx index 6ef8a70..d8f8234 100644 --- a/src/layouts/CommonLayout.jsx +++ b/src/layouts/CommonLayout.jsx @@ -44,8 +44,18 @@ export default function CommonLayout() { } }; + // 递归排序函数 + const sortMenu = (menu) => { + return [...menu] + .sort((a, b) => (a.order ?? 999) - (b.order ?? 999)) + .map((item) => ({ + ...item, + children: item.children ? sortMenu(item.children) : undefined, + })); + }; + const buildMenuItems = (menu) => - menu.map((item) => { + sortMenu(menu).map((item) => { if (item.children) { return { key: item.label, // 可改为 item.path 但注意不冲突 diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index 31be108..3cfaaa8 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -22,7 +22,9 @@ export default function Login() { return (
-

xxxx设备预约系统

+

+ 电科院材料所实验室仪器设备预约系统 +

{ + setLoading(true); + try { + const res = await axiosInstance.get("/device/detail-stats", { + params: { + deviceId: record.deviceId, + start: range[0].format("YYYY-MM-DD"), + end: range[1].format("YYYY-MM-DD"), + }, + }); + setData(res); + } catch (e) { + message.error("获取数据失败"); + } finally { + setLoading(false); + } + }; + + const handleExport = async () => { + setLoading(true); + try { + const response = await axiosInstance.get("/device/detail-stats/export", { + params: { + deviceId: record.deviceId, + start: range[0].format("YYYY-MM-DD"), + end: range[1].format("YYYY-MM-DD"), + }, + responseType: "blob", // 二进制流 + skipInterceptor: true, + }); + + // 从响应头获取文件名,兼容后端返回 + const fileName = `${record?.deviceName}_${range[0].format( + "YYYY.MM.DD" + )}-${range[1].format("YYYY.MM.DD")}_使用详情.xlsx`; + + // 创建blob对象和下载链接 + const blob = new Blob([response.data], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + + message.success("导出成功"); + } catch (error) { + message.error("导出失败,请稍后重试"); + console.error(error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (visible) { + fetchData(); + } + }, [visible, record]); + + const columns = [ + { + title: "使用人", + dataIndex: "applicantName", + key: "applicantName", + sorter: (a, b) => a.applicantName.localeCompare(b.applicantName), + }, + { + title: "所属团队", + dataIndex: "applicantTeam", + key: "applicantTeam", + sorter: (a, b) => a.applicantTeam.localeCompare(b.applicantTeam), + }, + { + title: "开始日期", + dataIndex: "startDay", + key: "startDay", + sorter: (a, b) => a.startDay.localeCompare(b.startDay), + }, + { + title: "结束日期", + dataIndex: "endDay", + key: "endDay", + sorter: (a, b) => a.endDay.localeCompare(b.endDay), + }, + ]; + + return ( + + { + onClose(); + }} + okText="导出Excel" + onOk={handleExport} + > + record.deviceId} + dataSource={data} + columns={columns} + className="mt-4" + /> + + + ); +} diff --git a/src/pages/admin/DeviceStats.jsx b/src/pages/admin/DeviceStats.jsx index b1ada2b..d9a5560 100644 --- a/src/pages/admin/DeviceStats.jsx +++ b/src/pages/admin/DeviceStats.jsx @@ -3,6 +3,7 @@ import dayjs from "dayjs"; import { useEffect, useState } from "react"; import axiosInstance from "../../api/axios"; import { datePresets } from "../../config/datePresetsConfig"; +import DeviceDetailStatsModal from "./DeviceDetailStatsModal"; const { RangePicker } = DatePicker; @@ -15,6 +16,8 @@ export default function DeviceStats() { ]); const [search, setSearch] = useState(""); const [loading, setLoading] = useState(false); + const [visiable, setVisiable] = useState(false); + const [selectedRecord, setSelectedRecord] = useState(null); const fetchData = async () => { setLoading(true); @@ -107,6 +110,23 @@ export default function DeviceStats() { key: "totalUsageDays", sorter: (a, b) => a.totalUsageDays - b.totalUsageDays, }, + { + title: "操作", + key: "action", + render: (_, record) => { + return ( + + ); + }, + }, ]; return ( @@ -135,6 +155,14 @@ export default function DeviceStats() { dataSource={filteredData} columns={columns} /> + { + setVisiable(false); + }} + range={range} + /> ); } diff --git a/src/pages/admin/TeamDetailModal.jsx b/src/pages/admin/TeamDetailModal.jsx new file mode 100644 index 0000000..905a30a --- /dev/null +++ b/src/pages/admin/TeamDetailModal.jsx @@ -0,0 +1,33 @@ +import { List, Modal, Typography } from "antd"; +import { useEffect, useState } from "react"; +import axiosInstance from "../../api/axios"; + +export default function TeamDetailModal({ open, team, onclose }) { + const [data, setData] = useState([]); + + const fetchData = async () => { + const data = await axiosInstance.get(`/user-team/${team.id}`); + setData(data); + }; + + useEffect(() => { + if (open) { + fetchData(); + } + }, [open, team]); + + return ( + { + onclose(); + }} + > + {team?.name}} + dataSource={data} + renderItem={(item) => {item}} + /> + + ); +} diff --git a/src/pages/admin/TeamManage.jsx b/src/pages/admin/TeamManage.jsx index 0639fde..2d6bdab 100644 --- a/src/pages/admin/TeamManage.jsx +++ b/src/pages/admin/TeamManage.jsx @@ -3,6 +3,7 @@ import Column from "antd/es/table/Column"; import { useEffect, useState } from "react"; import axiosInstance from "../../api/axios"; import TeamDeleteButton from "../../components/TeamDeleteButton"; +import TeamDetailModal from "./TeamDetailModal"; export default function TeamManage() { const [teams, setTeams] = useState([]); @@ -11,6 +12,8 @@ export default function TeamManage() { const [editingId, setEditingId] = useState(); const [editingName, setEditingName] = useState(""); const [newTeamName, setNewTeamName] = useState(); + const [open, setOpen] = useState(false); + const [selectedTeam, setSelectedTeam] = useState(null); const fetchData = async () => { const data = await axiosInstance.get("/teams"); @@ -124,6 +127,15 @@ export default function TeamManage() { 编辑 )} + handleDelete(record)} @@ -133,6 +145,14 @@ export default function TeamManage() { }} />
+ { + setOpen(false); + setSelectedTeam(null); + }} + /> ); } diff --git a/src/pages/shared/MyApproval.jsx b/src/pages/shared/MyApproval.jsx index 6ae629b..7e2acec 100644 --- a/src/pages/shared/MyApproval.jsx +++ b/src/pages/shared/MyApproval.jsx @@ -1,10 +1,11 @@ -import { Button, Form, Input, Table, Tag } from "antd"; +import { Button, DatePicker, Form, Input, message, Table, Tag } from "antd"; import { useForm } from "antd/es/form/Form"; import Column from "antd/es/table/Column"; import { useEffect, useState } from "react"; import { useSelector } from "react-redux"; import axiosInstance from "../../api/axios"; -import { selectUserId } from "../../features/auth/authSlice"; +import { selectUserId, selectUserRole } from "../../features/auth/authSlice"; +import dayjs from "dayjs"; export default function MyApproval() { const [approvals, setApprovals] = useState([]); @@ -14,8 +15,11 @@ export default function MyApproval() { pageSize: 10, total: 0, }); + const [editingRow, setEditingRow] = useState(null); + const [tempEndTime, setTempEndTime] = useState(null); const userId = useSelector(selectUserId); + const userRole = useSelector(selectUserRole); const fetchData = async (pagination, searchParam) => { const data = await axiosInstance.get(`/approval/${userId}`, { @@ -53,6 +57,20 @@ export default function MyApproval() { await fetchData(newPagination, values); }; + const handleSubmit = async (record) => { + try { + await axiosInstance.post(`/reservation/endTime/${record.reservationId}`, { + endTime: dayjs(tempEndTime).format("YYYY-MM-DD"), + }); + const values = await form.validateFields(); + await fetchData(pagination, values); + setEditingRow(null); + message.success("修改成功"); + } catch (error) { + message.error("修改失败"); + } + }; + return (
@@ -88,7 +106,57 @@ export default function MyApproval() { /> - + { + const isEditable = + record.decision === 1 && userRole.includes("DEVICE_ADMIN"); + + if (editingRow === record.approvalId) { + return ( +
+ setTempEndTime(date)} + disabledDate={(current) => + current && + current.isBefore(dayjs(record.startTime), "day") + } + size="small" + /> + + +
+ ); + } + + return isEditable ? ( + + {record.endTime} + + + ) : ( + {record.endTime} + ); + }} + /> { + const disabledDate = (current, { from } = {}) => { if (!current) return false; + const today = dayjs().startOf("day"); const currentDay = current.startOf("day"); - if (currentDay.isBefore(today)) { - return true; - } - dayjs.extend(isBetween); - return unavailableTimes.some(({ startTime, endTime }) => { + // 禁用过去日期 + const isPastDate = current.isBefore(today); + + // 禁用不可预约时间段 + dayjs.extend(isBetween); + const isUnavailable = unavailableTimes.some(({ startTime, endTime }) => { const start = dayjs(startTime).startOf("day"); const end = dayjs(endTime).endOf("day"); return currentDay.isBetween(start, end, null, "[]"); }); + + // 限制选择范围为 7 天内(从 from 开始算起) + const isExceedingRange = from && Math.abs(current.diff(from, "day")) >= 7; + + return isPastDate || isUnavailable || isExceedingRange; }; const handleOK = async () => { diff --git a/src/pages/user/MyReservation.jsx b/src/pages/user/MyReservation.jsx index a8e78e1..025af92 100644 --- a/src/pages/user/MyReservation.jsx +++ b/src/pages/user/MyReservation.jsx @@ -58,6 +58,16 @@ export default function MyReservation() { ); }} /> + + - - + { + return ( + + {record.startTime} 至 {record.endTime} + + ); + }} + /> , + element: ( + + ), children: [ { path: "reserve",