diff --git a/src/api/axios.js b/src/api/axios.js index df4372f..cff5057 100644 --- a/src/api/axios.js +++ b/src/api/axios.js @@ -22,6 +22,11 @@ axiosInstance.interceptors.request.use( axiosInstance.interceptors.response.use( (response) => { + const config = response.config; + // 如果设置了跳过,直接返回原始 response + if (config.skipInterceptor) { + return response; + } const { code, message: msg, data } = response.data; if (code === 0) { diff --git a/src/config/datePresetsConfig.js b/src/config/datePresetsConfig.js new file mode 100644 index 0000000..41c54f0 --- /dev/null +++ b/src/config/datePresetsConfig.js @@ -0,0 +1,40 @@ +import dayjs from "dayjs"; +import quarterOfYear from "dayjs/plugin/quarterOfYear"; + +dayjs.extend(quarterOfYear); + +export const datePresets = [ + { + label: "本月", + value: [dayjs().startOf("month"), dayjs().endOf("month")], + }, + { + label: "上月", + value: [ + dayjs().subtract(1, "month").startOf("month"), + dayjs().subtract(1, "month").endOf("month"), + ], + }, + { + label: "本季度", + value: [dayjs().startOf("quarter"), dayjs().endOf("quarter")], + }, + { + label: "上季度", + value: [ + dayjs().subtract(1, "quarter").startOf("quarter"), + dayjs().subtract(1, "quarter").endOf("quarter"), + ], + }, + { + label: "本年", + value: [dayjs().startOf("year"), dayjs().endOf("year")], + }, + { + label: "去年", + value: [ + dayjs().subtract(1, "year").startOf("year"), + dayjs().subtract(1, "year").endOf("year"), + ], + }, +]; diff --git a/src/config/menuConfig.js b/src/config/menuConfig.js index 8328944..e38af50 100644 --- a/src/config/menuConfig.js +++ b/src/config/menuConfig.js @@ -2,6 +2,7 @@ import { DesktopOutlined, ExperimentOutlined, FileDoneOutlined, + PieChartOutlined, UnorderedListOutlined, UsergroupAddOutlined, UserOutlined, @@ -45,6 +46,21 @@ const menuConfig = [ icon: UsergroupAddOutlined, roles: ["ADMIN"], }, + { + label: "数据统计", + icon: PieChartOutlined, + children: [ + { + path: "/admin/stats-device", + label: "设备统计", + }, + { + path: "/admin/stats-user", + label: "使用人统计", + }, + ], + roles: ["ADMIN"], + }, { path: "/userdetail", label: "个人信息", diff --git a/src/layouts/CommonLayout.jsx b/src/layouts/CommonLayout.jsx index 351ba2b..6ef8a70 100644 --- a/src/layouts/CommonLayout.jsx +++ b/src/layouts/CommonLayout.jsx @@ -44,6 +44,24 @@ export default function CommonLayout() { } }; + const buildMenuItems = (menu) => + menu.map((item) => { + if (item.children) { + return { + key: item.label, // 可改为 item.path 但注意不冲突 + label: item.label, + icon: item.icon ? React.createElement(item.icon) : null, + children: buildMenuItems(item.children), + }; + } else { + return { + key: item.path, + label: item.label, + icon: item.icon ? React.createElement(item.icon) : null, + }; + } + }); + return ( ({ - key: item.path, - label: item.label, - icon: React.createElement(item.icon), - }))} + items={buildMenuItems(menu)} onClick={({ key }) => naviagte(key)} /> diff --git a/src/pages/admin/DeviceStats.jsx b/src/pages/admin/DeviceStats.jsx new file mode 100644 index 0000000..b1ada2b --- /dev/null +++ b/src/pages/admin/DeviceStats.jsx @@ -0,0 +1,140 @@ +import { Button, DatePicker, Input, Space, Spin, Table, message } from "antd"; +import dayjs from "dayjs"; +import { useEffect, useState } from "react"; +import axiosInstance from "../../api/axios"; +import { datePresets } from "../../config/datePresetsConfig"; + +const { RangePicker } = DatePicker; + +export default function DeviceStats() { + const [data, setData] = useState([]); + const [filteredData, setFilteredData] = useState([]); + const [range, setRange] = useState([ + dayjs().startOf("month"), + dayjs().endOf("month"), + ]); + const [search, setSearch] = useState(""); + const [loading, setLoading] = useState(false); + + const fetchData = async () => { + setLoading(true); + try { + const res = await axiosInstance.get("/device/usage-stats", { + params: { + start: range[0].format("YYYY-MM-DD"), + end: range[1].format("YYYY-MM-DD"), + }, + }); + setData(res); + setFilteredData(res); + } catch (e) { + message.error("获取数据失败"); + } finally { + setLoading(false); + } + }; + + const handleSearch = (value) => { + setSearch(value); + const filtered = data.filter((item) => + item.deviceName.toLowerCase().includes(value.toLowerCase()) + ); + setFilteredData(filtered); + }; + + const handleDownload = async () => { + setLoading(true); + try { + const response = await axiosInstance.get("/device/usage-stats/export", { + params: { + start: range[0].format("YYYY-MM-DD"), + end: range[1].format("YYYY-MM-DD"), + }, + responseType: "blob", // 二进制流 + skipInterceptor: true, + }); + + // 从响应头获取文件名,兼容后端返回 + const disposition = response.headers["content-disposition"]; + let fileName = "设备统计数据.xlsx"; + if (disposition) { + const match = disposition.match(/filename="?([^"]+)"?/); + if (match && match[1]) fileName = decodeURIComponent(match[1]); + } + + // 创建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(() => { + fetchData(); + }, [range]); + + const columns = [ + { + title: "设备名称", + dataIndex: "deviceName", + key: "deviceName", + sorter: (a, b) => a.deviceName.localeCompare(b.deviceName), + }, + { + title: "使用次数", + dataIndex: "usageCount", + key: "usageCount", + sorter: (a, b) => a.usageCount - b.usageCount, + }, + { + title: "使用总时长(天)", + dataIndex: "totalUsageDays", + key: "totalUsageDays", + sorter: (a, b) => a.totalUsageDays - b.totalUsageDays, + }, + ]; + + return ( + + + { + if (dates) setRange(dates); + }} + presets={datePresets} + /> + + + + + record.deviceId} + dataSource={filteredData} + columns={columns} + /> + + ); +} diff --git a/src/pages/user/DeviceDetailModal.jsx b/src/pages/user/DeviceDetailModal.jsx index 15da59a..ae7a205 100644 --- a/src/pages/user/DeviceDetailModal.jsx +++ b/src/pages/user/DeviceDetailModal.jsx @@ -1,10 +1,13 @@ import { DatePicker, Descriptions, + Divider, Form, Image, + Input, message, Modal, + Select, Space, } from "antd"; import dayjs from "dayjs"; @@ -15,6 +18,35 @@ import axiosInstance, { baseURL } from "../../api/axios"; export default function DeviceDetailModal({ visiable, device, onclose }) { const [unavailableTimes, setUnavailableTims] = useState([]); + const [form] = Form.useForm(); + const userId = useSelector((state) => state.auth.userId); + const [teams, setTeams] = useState([]); + const [initialValues, setInitialValues] = useState(); + + const fetchTeams = async () => { + const data = await axiosInstance.get("/teams"); + const teams = data.map((item) => ({ + label: item.label, + value: item.label, + })); + setTeams(teams); + }; + + const fetchUser = async () => { + const data = await axiosInstance.get(`/userdetail/${userId}`); + const values = { + name: data.name, + phone: data.phone, + team: data.team, + }; + setInitialValues(values); + form.setFieldsValue(values); + }; + + useEffect(() => { + fetchTeams(); + }, []); + useEffect(() => { const fetchUnavailableTimes = async (id) => { const data = await axiosInstance.get(`/device/unavailable-times/${id}`); @@ -23,6 +55,7 @@ export default function DeviceDetailModal({ visiable, device, onclose }) { if (visiable && device?.deviceId) { fetchUnavailableTimes(device.deviceId); + fetchUser(); } }, [visiable, device?.deviceId]); @@ -43,9 +76,6 @@ export default function DeviceDetailModal({ visiable, device, onclose }) { }); }; - const [form] = Form.useForm(); - const userId = useSelector((state) => state.auth.userId); - const handleOK = async () => { const values = await form.validateFields(); @@ -56,6 +86,9 @@ export default function DeviceDetailModal({ visiable, device, onclose }) { userId, startTime: startTime.format("YYYY-MM-DD"), endTime: endTime.format("YYYY-MM-DD"), + applicantName: values.name, + applicantContact: values.phone, + applicantTeam: values.team, }; await axiosInstance.post("/reservation", payload); @@ -87,7 +120,9 @@ export default function DeviceDetailModal({ visiable, device, onclose }) { - + + + + + + + + + + +