From fce4a8da6cbbd46719e23ac7dfa346bd16d4d409 Mon Sep 17 00:00:00 2001 From: BenjaminNH <1249376374@qq.com> Date: Tue, 8 Jul 2025 16:31:16 +0800 Subject: [PATCH 1/9] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E7=89=88=E6=9C=AC=E5=8F=B7=E8=87=B31.0.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From 87cc58be925314bccfdca50be22341cafbf13456 Mon Sep 17 00:00:00 2001 From: BenjaminNH <1249376374@qq.com> Date: Mon, 14 Jul 2025 13:01:24 +0800 Subject: [PATCH 2/9] =?UTF-8?q?refactor:=20=E9=87=8D=E5=86=99=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E7=A9=BA=E9=97=B2=E5=88=A4=E6=96=AD=E9=83=A8=E5=88=86?= =?UTF-8?q?=E4=BD=BF=E4=BB=A3=E7=A0=81=E6=9B=B4=E6=98=8E=E7=A1=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/user/DeviceDetailModal.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/user/DeviceDetailModal.jsx b/src/pages/user/DeviceDetailModal.jsx index e03a20a..36b947a 100644 --- a/src/pages/user/DeviceDetailModal.jsx +++ b/src/pages/user/DeviceDetailModal.jsx @@ -64,16 +64,16 @@ export default function DeviceDetailModal({ visiable, device, onclose }) { if (!current) return false; const today = dayjs().startOf("day"); const currentDay = current.startOf("day"); - if (currentDay.isBefore(today)) { - return true; - } + const isPastDate = current.isBefore(today); + dayjs.extend(isBetween); - return unavailableTimes.some(({ startTime, endTime }) => { + const isUnavailable = unavailableTimes.some(({ startTime, endTime }) => { const start = dayjs(startTime).startOf("day"); const end = dayjs(endTime).endOf("day"); return currentDay.isBetween(start, end, null, "[]"); }); + return isPastDate || isUnavailable; }; const handleOK = async () => { From b34646f8697916b79cc2561b7bcc6cc7ccbd9c62 Mon Sep 17 00:00:00 2001 From: BenjaminNH <1249376374@qq.com> Date: Thu, 17 Jul 2025 19:04:55 +0800 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E4=BD=BF=E7=94=A8=E8=AF=A6=E6=83=85=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/admin/DeviceDetailStatsModal.jsx | 129 +++++++++++++++++++++ src/pages/admin/DeviceStats.jsx | 28 +++++ 2 files changed, 157 insertions(+) create mode 100644 src/pages/admin/DeviceDetailStatsModal.jsx diff --git a/src/pages/admin/DeviceDetailStatsModal.jsx b/src/pages/admin/DeviceDetailStatsModal.jsx new file mode 100644 index 0000000..984e897 --- /dev/null +++ b/src/pages/admin/DeviceDetailStatsModal.jsx @@ -0,0 +1,129 @@ +import { message, Modal, Space, Spin, Table } from "antd"; +import { useEffect, useState } from "react"; +import axiosInstance from "../../api/axios"; + +export default function DeviceDetailStatsModal({ + visible, + record, + range, + onClose, +}) { + const [loading, setLoading] = useState(false); + const [data, setData] = useState([]); + + const fetchData = async () => { + 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} + /> ); } From 087fa440b62769a61b69d888f10d040594c02964 Mon Sep 17 00:00:00 2001 From: BenjaminNH <1249376374@qq.com> Date: Mon, 21 Jul 2025 10:33:55 +0800 Subject: [PATCH 4/9] =?UTF-8?q?chore:=20=E4=BF=AE=E6=94=B9=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 4 ++-- src/pages/Login.jsx | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) 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/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设备预约系统

+

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

Date: Mon, 21 Jul 2025 11:46:39 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=9F=A5?= =?UTF-8?q?=E7=9C=8B=E5=9B=A2=E9=98=9F=E6=88=90=E5=91=98=E6=A8=A1=E6=80=81?= =?UTF-8?q?=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/admin/TeamDetailModal.jsx | 33 +++++++++++++++++++++++++++++ src/pages/admin/TeamManage.jsx | 20 +++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/pages/admin/TeamDetailModal.jsx 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); + }} + /> ); } From 0b2c1432250ee4eb738d705cf78c91dc3638fd44 Mon Sep 17 00:00:00 2001 From: BenjaminNH <1249376374@qq.com> Date: Mon, 21 Jul 2025 12:06:23 +0800 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=B8=80=E6=AC=A1=E6=80=A7=E5=8F=AA=E8=83=BD=E9=A2=84?= =?UTF-8?q?=E7=BA=A6=E4=B8=83=E5=A4=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/user/DeviceDetailModal.jsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pages/user/DeviceDetailModal.jsx b/src/pages/user/DeviceDetailModal.jsx index 36b947a..b87d040 100644 --- a/src/pages/user/DeviceDetailModal.jsx +++ b/src/pages/user/DeviceDetailModal.jsx @@ -60,20 +60,27 @@ export default function DeviceDetailModal({ visiable, device, onclose }) { }, [visiable, device?.deviceId]); const { RangePicker } = DatePicker; - const disabledDate = (current) => { + const disabledDate = (current, { from } = {}) => { if (!current) return false; + const today = dayjs().startOf("day"); const currentDay = current.startOf("day"); + + // 禁用过去日期 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, "[]"); }); - return isPastDate || isUnavailable; + + // 限制选择范围为 7 天内(从 from 开始算起) + const isExceedingRange = from && Math.abs(current.diff(from, "day")) >= 7; + + return isPastDate || isUnavailable || isExceedingRange; }; const handleOK = async () => { From ba0d7cd8e2dafea8451bae6234569749b3adb913 Mon Sep 17 00:00:00 2001 From: BenjaminNH <1249376374@qq.com> Date: Mon, 21 Jul 2025 20:34:33 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20=E6=99=AE=E9=80=9A=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=9F=A5=E8=AF=A2=E9=A2=84=E7=BA=A6=E6=97=B6=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=98=BE=E7=A4=BA=E5=9B=A2=E9=98=9F=E8=B4=9F=E8=B4=A3?= =?UTF-8?q?=E4=BA=BA=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/user/MyReservation.jsx | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) 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} + + ); + }} + /> Date: Mon, 21 Jul 2025 21:24:42 +0800 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E7=AE=A1=E7=90=86=E5=91=98=E4=BF=AE=E6=94=B9=E9=A2=84?= =?UTF-8?q?=E7=BA=A6=E7=BB=93=E6=9D=9F=E6=97=A5=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/shared/MyApproval.jsx | 74 +++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 3 deletions(-) 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} + ); + }} + /> Date: Thu, 31 Jul 2025 11:33:32 +0800 Subject: [PATCH 9/9] =?UTF-8?q?feat:=20=E5=BC=80=E6=94=BE=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E9=A2=84=E7=BA=A6=E5=92=8C=E6=88=91=E7=9A=84=E9=A2=84?= =?UTF-8?q?=E7=BA=A6=E9=A1=B5=E9=9D=A2=E7=BB=99=E5=9B=A2=E9=98=9F=E8=B4=9F?= =?UTF-8?q?=E8=B4=A3=E4=BA=BA=E5=92=8C=E8=AE=BE=E5=A4=87=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=EF=BC=8C=E6=B7=BB=E5=8A=A0=E8=8F=9C=E5=8D=95=E6=A0=8F?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/menuConfig.js | 18 +++++++++++++++--- src/layouts/CommonLayout.jsx | 12 +++++++++++- src/router/index.jsx | 4 +++- 3 files changed, 29 insertions(+), 5 deletions(-) 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/router/index.jsx b/src/router/index.jsx index 3db1f54..f1660ed 100644 --- a/src/router/index.jsx +++ b/src/router/index.jsx @@ -28,7 +28,9 @@ const router = createBrowserRouter([ children: [ { path: "user", - element: , + element: ( + + ), children: [ { path: "reserve",