Compare commits

...

4 Commits

41 changed files with 3105 additions and 465 deletions

View File

@ -1,33 +1,41 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import prettier from "eslint-config-prettier";
export default [
{ ignores: ['dist'] },
js.configs.recommended,
...tseslint.configs.recommended,
{ ignores: ["dist"] },
{
files: ['**/*.{js,jsx}'],
files: ["**/*.{js,jsx,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaVersion: "latest",
ecmaFeatures: { jsx: true },
sourceType: 'module',
sourceType: "module",
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
react,
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
prettier,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
'react-refresh/only-export-components': [
'warn',
"no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"react/react-in-jsx-scope": "off",
},
},
]
];

2254
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,13 +24,19 @@
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@typescript-eslint/eslint-plugin": "^8.41.0",
"@typescript-eslint/parser": "^8.41.0",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"prettier": "^3.6.2",
"typescript-eslint": "^8.41.0",
"vite": "^6.3.5"
}
}

View File

@ -1,10 +1,19 @@
import { useState } from "react";
import { Popconfirm, Button, message } from "antd";
import { Team } from "types/model";
export default function TeamDeleteButton({ record, onConfirm }) {
interface TeamDeleteButtonProps {
record: Team;
onConfirm: () => void;
}
export default function TeamDeleteButton({
record,
onConfirm,
}: TeamDeleteButtonProps) {
const [visiable, setVisiable] = useState(false);
const handleClick = (record) => {
const handleClick = (record: Team) => {
if (record.size > 0) {
message.warning("该团队下还有成员,无法删除");
} else {

View File

@ -1,9 +1,9 @@
import dayjs from "dayjs";
import dayjs, { Dayjs } from "dayjs";
import quarterOfYear from "dayjs/plugin/quarterOfYear";
dayjs.extend(quarterOfYear);
export const datePresets = [
export const datePresets: Array<{ label: string; value: [Dayjs, Dayjs] }> = [
{
label: "本月",
value: [dayjs().startOf("month"), dayjs().endOf("month")],

View File

@ -10,7 +10,16 @@ import {
UserOutlined,
} from "@ant-design/icons";
const menuConfig = [
export interface MenuItem {
path?: string;
label: string;
icon?: React.ComponentType;
roles?: string[];
order: number;
children?: MenuItem[];
}
const menuConfig: MenuItem[] = [
{
path: "/user/reserve",
label: "设备预约",

View File

@ -1,19 +1,29 @@
import { createSlice } from "@reduxjs/toolkit";
import { login } from "./authThunk";
import { RootState } from "store";
interface AuthState {
userId: string | null;
name: string | null;
roles: string[];
token: string | null;
}
const initialState: AuthState = {
userId: "",
name: "",
roles: [],
token: "",
};
const authSlice = createSlice({
name: "authSlice",
initialState: {
userId: null,
name: null,
roles: [],
token: null,
},
initialState: initialState,
reducers: {
logout(state) {
state.userId = null;
state.name = null;
state.token = null;
state.userId = "";
state.name = "";
state.token = "";
state.roles = [];
localStorage.removeItem("userId");
@ -30,7 +40,7 @@ const authSlice = createSlice({
state.roles = payload.roles;
state.token = payload.token;
localStorage.setItem("userId", payload.userId);
localStorage.setItem("userId", payload.userId.toString());
localStorage.setItem("name", payload.name);
localStorage.setItem("roles", JSON.stringify(payload.roles));
localStorage.setItem("token", action.payload.token);
@ -39,7 +49,7 @@ const authSlice = createSlice({
});
export const { logout } = authSlice.actions;
export const selectUserRole = (state) => state.auth.roles;
export const selectUserName = (state) => state.auth.name;
export const selectUserId = (state) => state.auth.userId;
export const selectUserRole = (state: RootState) => state.auth.roles;
export const selectUserName = (state: RootState) => state.auth.name;
export const selectUserId = (state: RootState) => state.auth.userId;
export default authSlice.reducer;

View File

@ -1,11 +0,0 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import axiosInstance from "../../api/axios";
export const login = createAsyncThunk(
"auth/login",
async (values, thunkAPI) => {
const res = await axiosInstance.post("/login", values);
return res;
}
);

View File

@ -0,0 +1,15 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import axiosInstance from "../../api/axios";
import { LoginForm, LoginResponse } from "./types";
export const login = createAsyncThunk<LoginResponse, LoginForm>(
"auth/login",
async (values) => {
const res = await axiosInstance.post<LoginResponse, LoginResponse>(
"/login",
values
);
return res;
}
);

View File

@ -0,0 +1,11 @@
export interface LoginResponse {
userId: string;
name: string;
roles: string[];
token: string;
}
export interface LoginForm {
username: string;
password: string;
}

View File

@ -5,12 +5,13 @@ import Sider from "antd/es/layout/Sider";
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import menuConfig from "../config/menuConfig";
import menuConfig, { MenuItem } from "../config/menuConfig";
import {
logout,
selectUserName,
selectUserRole,
} from "../features/auth/authSlice";
import { ItemType, MenuItemType } from "antd/es/menu/interface";
export default function CommonLayout() {
const location = useLocation();
@ -36,7 +37,7 @@ export default function CommonLayout() {
},
];
const handleMenuClick = ({ key }) => {
const handleMenuClick = ({ key }: { key: string }) => {
if (key === "logout") {
dispatch(logout());
message.success("已退出登录");
@ -45,7 +46,7 @@ export default function CommonLayout() {
};
// 递归排序函数
const sortMenu = (menu) => {
const sortMenu = (menu: MenuItem[]): MenuItem[] => {
return [...menu]
.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
.map((item) => ({
@ -54,8 +55,8 @@ export default function CommonLayout() {
}));
};
const buildMenuItems = (menu) =>
sortMenu(menu).map((item) => {
const buildMenuItems = (menu: MenuItem[]): ItemType<MenuItemType>[] =>
sortMenu(menu).map((item: MenuItem): ItemType<MenuItemType> => {
if (item.children) {
return {
key: item.label, // 可改为 item.path 但注意不冲突
@ -65,7 +66,7 @@ export default function CommonLayout() {
};
} else {
return {
key: item.path,
key: item.path ?? item.label,
label: item.label,
icon: item.icon ? React.createElement(item.icon) : null,
};

View File

@ -3,7 +3,7 @@ import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import { RouterProvider } from "react-router-dom";
import "./index.css";
import router from "./router/index.jsx";
import router from "./router/index.js";
import { store } from "./store/index.js";
import dayjs from "dayjs";
import zhCN from "antd/locale/zh_CN";
@ -11,7 +11,7 @@ import { ConfigProvider } from "antd";
dayjs.locale("zh-cn");
createRoot(document.getElementById("root")).render(
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ConfigProvider locale={zhCN}>
<Provider store={store}>

View File

@ -4,14 +4,18 @@ import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { login } from "../features/auth/authThunk";
import roleRoute from "../config/roleRouteConfig";
import { store } from "store";
import { LoginForm } from "features/auth/types";
export default function Login() {
const dispatch = useDispatch();
const dispatch = useDispatch<typeof store.dispatch>();
const navigate = useNavigate();
const onFinish = async (values) => {
const onFinish = async (values: LoginForm) => {
const res = await dispatch(login(values)).unwrap();
const path = res.roles.map((r) => roleRoute[r]).find(Boolean);
const path = res.roles
.map((r) => roleRoute[r as keyof typeof roleRoute])
.find(Boolean);
if (path) {
message.success("登录成功");
navigate(path);

View File

@ -1,20 +1,40 @@
import { message, Modal, Space, Spin, Table } from "antd";
import { useEffect, useState } from "react";
import { message, Modal, Spin, Table } from "antd";
import { useCallback, useEffect, useState } from "react";
import axiosInstance from "../../api/axios";
import { Dayjs } from "dayjs";
import { UsageStats } from "./DeviceStats";
import { ColumnsType } from "antd/es/table";
interface DeviceDetailStats {
applicantName: string;
applicantTeam: string;
startDay: string;
endDay: string;
}
interface DeviceDetailStatsModalProps {
visible: boolean;
record: UsageStats;
range: [Dayjs, Dayjs];
onClose: () => void;
}
export default function DeviceDetailStatsModal({
visible,
record,
range,
onClose,
}) {
}: DeviceDetailStatsModalProps) {
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [data, setData] = useState<DeviceDetailStats[]>([]);
const fetchData = async () => {
const fetchData = useCallback(async () => {
setLoading(true);
try {
const res = await axiosInstance.get("/device/detail-stats", {
const res = await axiosInstance.get<
DeviceDetailStats[],
DeviceDetailStats[]
>("/device/detail-stats", {
params: {
deviceId: record.deviceId,
start: range[0].format("YYYY-MM-DD"),
@ -22,12 +42,12 @@ export default function DeviceDetailStatsModal({
},
});
setData(res);
} catch (e) {
} catch {
message.error("获取数据失败");
} finally {
setLoading(false);
}
};
}, [record, range]);
const handleExport = async () => {
setLoading(true);
@ -73,9 +93,9 @@ export default function DeviceDetailStatsModal({
if (visible) {
fetchData();
}
}, [visible, record]);
}, [visible, fetchData]);
const columns = [
const columns: ColumnsType<DeviceDetailStats> = [
{
title: "使用人",
dataIndex: "applicantName",
@ -117,8 +137,8 @@ export default function DeviceDetailStatsModal({
okText="导出Excel"
onOk={handleExport}
>
<Table
rowKey={(record) => record.deviceId}
<Table<DeviceDetailStats>
// rowKey={(record) => record.deviceId}
dataSource={data}
columns={columns}
className="mt-4"

View File

@ -1,43 +1,55 @@
import { Button, DatePicker, Input, Space, Spin, Table, message } from "antd";
import dayjs from "dayjs";
import { useEffect, useState } from "react";
import { Button, DatePicker, Input, message, Space, Spin, Table } from "antd";
import { ColumnsType } from "antd/es/table";
import dayjs, { Dayjs } from "dayjs";
import { useCallback, useEffect, useState } from "react";
import axiosInstance from "../../api/axios";
import { datePresets } from "../../config/datePresetsConfig";
import DeviceDetailStatsModal from "./DeviceDetailStatsModal";
export interface UsageStats {
deviceId: string;
deviceName: string;
usageCount: number;
totalUsageDays: number;
}
const { RangePicker } = DatePicker;
export default function DeviceStats() {
const [data, setData] = useState([]);
const [filteredData, setFilteredData] = useState([]);
const [range, setRange] = useState([
const [data, setData] = useState<UsageStats[]>([]);
const [filteredData, setFilteredData] = useState<UsageStats[]>([]);
const [range, setRange] = useState<[Dayjs, Dayjs]>([
dayjs().startOf("month"),
dayjs().endOf("month"),
]);
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
const [visiable, setVisiable] = useState(false);
const [selectedRecord, setSelectedRecord] = useState(null);
const [selectedRecord, setSelectedRecord] = useState<UsageStats | null>(null);
const fetchData = async () => {
const fetchData = useCallback(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"),
},
});
const res = await axiosInstance.get<UsageStats[], UsageStats[]>(
"/device/usage-stats",
{
params: {
start: range[0].format("YYYY-MM-DD"),
end: range[1].format("YYYY-MM-DD"),
},
}
);
setData(res);
setFilteredData(res);
} catch (e) {
} catch {
message.error("获取数据失败");
} finally {
setLoading(false);
}
};
}, [range]);
const handleSearch = (value) => {
const handleSearch = (value: string) => {
setSearch(value);
const filtered = data.filter((item) =>
item.deviceName.toLowerCase().includes(value.toLowerCase())
@ -89,9 +101,9 @@ export default function DeviceStats() {
useEffect(() => {
fetchData();
}, [range]);
}, [fetchData]);
const columns = [
const columns: ColumnsType<UsageStats> = [
{
title: "设备名称",
dataIndex: "deviceName",
@ -135,7 +147,7 @@ export default function DeviceStats() {
<RangePicker
value={range}
onChange={(dates) => {
if (dates) setRange(dates);
if (dates && dates[0] && dates[1]) setRange([dates[0], dates[1]]);
}}
presets={datePresets}
/>
@ -150,19 +162,21 @@ export default function DeviceStats() {
</Button>
</Space>
<Table
<Table<UsageStats>
rowKey={(record) => record.deviceId}
dataSource={filteredData}
columns={columns}
/>
<DeviceDetailStatsModal
visible={visiable}
record={selectedRecord}
onClose={() => {
setVisiable(false);
}}
range={range}
/>
{selectedRecord && (
<DeviceDetailStatsModal
visible={visiable}
record={selectedRecord}
onClose={() => {
setVisiable(false);
}}
range={range}
/>
)}
</Spin>
);
}

View File

@ -1,40 +1,53 @@
import { Button, DatePicker, Input, Space, Spin, Table, message } from "antd";
import dayjs from "dayjs";
import { useEffect, useState } from "react";
import dayjs, { Dayjs } from "dayjs";
import { useCallback, useEffect, useState } from "react";
import axiosInstance from "../../api/axios";
import { datePresets } from "../../config/datePresetsConfig";
import { ColumnsType } from "antd/es/table";
interface ReservationStat {
deviceId: string;
deviceName: string;
applicantName: string;
applicantTeam: string;
usageCount: number;
}
const { RangePicker } = DatePicker;
export default function ReservationStats() {
const [data, setData] = useState([]);
const [filteredData, setFilteredData] = useState([]);
const [range, setRange] = useState([
const [data, setData] = useState<ReservationStat[]>([]);
const [filteredData, setFilteredData] = useState<ReservationStat[]>([]);
const [range, setRange] = useState<[Dayjs, Dayjs]>([
dayjs().startOf("month"),
dayjs().endOf("month"),
]);
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
const fetchData = async () => {
const fetchData = useCallback(async () => {
setLoading(true);
try {
const res = await axiosInstance.get("/reservation/stats", {
params: {
start: range[0].format("YYYY-MM-DD"),
end: range[1].format("YYYY-MM-DD"),
},
});
const res = await axiosInstance.get<ReservationStat[], ReservationStat[]>(
"/reservation/stats",
{
params: {
start: range[0].format("YYYY-MM-DD"),
end: range[1].format("YYYY-MM-DD"),
},
}
);
setData(res);
setFilteredData(res);
} catch (e) {
} catch {
message.error("获取数据失败");
} finally {
setLoading(false);
}
};
}, [range]);
const handleSearch = (value) => {
const handleSearch = (value: string) => {
setSearch(value);
const filtered = data.filter((item) =>
item.deviceName.toLowerCase().includes(value.toLowerCase())
@ -86,9 +99,9 @@ export default function ReservationStats() {
useEffect(() => {
fetchData();
}, [range]);
}, [fetchData]);
const columns = [
const columns: ColumnsType<ReservationStat> = [
{
title: "设备名称",
dataIndex: "deviceName",
@ -121,7 +134,7 @@ export default function ReservationStats() {
<RangePicker
value={range}
onChange={(dates) => {
if (dates) setRange(dates);
if (dates && dates[0] && dates[1]) setRange([dates[0], dates[1]]);
}}
presets={datePresets}
/>

View File

@ -1,33 +0,0 @@
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 (
<Modal
open={open}
onCancel={() => {
onclose();
}}
>
<List
header={<div>{team?.name}</div>}
dataSource={data}
renderItem={(item) => <List.Item>{item}</List.Item>}
/>
</Modal>
);
}

View File

@ -0,0 +1,46 @@
import { List, Modal } from "antd";
import { useCallback, useEffect, useState } from "react";
import axiosInstance from "../../api/axios";
import { Team } from "types/model";
interface TeamDetailModalProps {
open: boolean;
team: Team;
onClose: () => void;
}
export default function TeamDetailModal({
open,
team,
onClose,
}: TeamDetailModalProps) {
const [data, setData] = useState<string[]>([]);
const fetchData = useCallback(async () => {
const data = await axiosInstance.get<string[], string[]>(
`/user-team/${team.id}`
);
setData(data);
}, [team]);
useEffect(() => {
if (open) {
fetchData();
}
}, [open, fetchData]);
return (
<Modal
open={open}
onCancel={() => {
onClose();
}}
>
<List
header={<div>{team?.name}</div>}
dataSource={data}
renderItem={(item) => <List.Item>{item}</List.Item>}
/>
</Modal>
);
}

View File

@ -4,29 +4,30 @@ import { useEffect, useState } from "react";
import axiosInstance from "../../api/axios";
import TeamDeleteButton from "../../components/TeamDeleteButton";
import TeamDetailModal from "./TeamDetailModal";
import { Team } from "types/model";
export default function TeamManage() {
const [teams, setTeams] = useState([]);
const [data, setData] = useState([]);
const [searchName, setSearchName] = useState();
const [editingId, setEditingId] = useState();
const [teams, setTeams] = useState<Team[]>([]);
const [data, setData] = useState<Team[]>([]);
const [searchName, setSearchName] = useState<string>();
const [editingId, setEditingId] = useState<string | null>(null);
const [editingName, setEditingName] = useState("");
const [newTeamName, setNewTeamName] = useState();
const [newTeamName, setNewTeamName] = useState("");
const [open, setOpen] = useState(false);
const [selectedTeam, setSelectedTeam] = useState(null);
const [selectedTeam, setSelectedTeam] = useState<Team | null>(null);
const fetchData = async () => {
const data = await axiosInstance.get("/teams");
const data = await axiosInstance.get<Team[], Team[]>("/teams");
setData(data);
setTeams(data);
setSearchName(null);
setSearchName("");
};
useEffect(() => {
fetchData();
}, []);
const handleSearch = (value) => {
const handleSearch = (value: string) => {
setSearchName(value);
const filtered = data.filter((item) =>
item.name.toLowerCase().includes(value.toLowerCase())
@ -34,18 +35,18 @@ export default function TeamManage() {
setTeams(filtered);
};
const handleDelete = async (record) => {
const handleDelete = async (record: Team) => {
await axiosInstance.delete(`/team/${record.id}`);
message.success("删除成功");
fetchData();
};
const handleEdit = (record) => {
const handleEdit = (record: Team) => {
setEditingId(record.id);
setEditingName(record.name);
};
const handleSave = async (teamId) => {
const handleSave = async (teamId: string) => {
if (!editingName.trim()) return message.warning("请输入新名称");
await axiosInstance.put(`/team/${teamId}`, { name: editingName });
setEditingId(null);
@ -62,7 +63,7 @@ export default function TeamManage() {
name: newTeamName,
});
message.success("添加成功");
setNewTeamName(null);
setNewTeamName("");
fetchData();
};
@ -87,7 +88,7 @@ export default function TeamManage() {
style={{ width: "300px" }}
/>
</Flex>
<Table rowKey="id" dataSource={teams}>
<Table<Team> rowKey="id" dataSource={teams}>
<Column
title="团队名"
key="name"
@ -106,7 +107,7 @@ export default function TeamManage() {
<Column title="下属人数" key="size" dataIndex="size" />
<Column
title="操作"
render={(_, record) => {
render={(_, record: Team) => {
return (
<Space>
{record.id === editingId ? (
@ -145,14 +146,16 @@ export default function TeamManage() {
}}
/>
</Table>
<TeamDetailModal
open={open}
team={selectedTeam}
onclose={() => {
setOpen(false);
setSelectedTeam(null);
}}
/>
{selectedTeam && (
<TeamDetailModal
open={open}
team={selectedTeam}
onClose={() => {
setOpen(false);
setSelectedTeam(null);
}}
/>
)}
</>
);
}

View File

@ -1,22 +1,43 @@
import { Form, Input, message, Modal, Select } from "antd";
import { useEffect, useState } from "react";
import axiosInstance from "../../api/axios";
import Password from "antd/es/input/Password";
import { UserVo } from "types/model";
interface UserDTO {
username: string;
name: string;
phone: string;
password?: string;
teamId?: string;
roleId?: string;
}
interface UserDetailModalProps {
visible: boolean;
mode: string;
user: UserVo;
roles: { label: string; value: string }[];
onClose: () => void;
onSuccess: () => void;
}
export default function UserDetailModal({
visiable,
visible,
mode = "create",
user,
roles,
onclose,
onClose,
onSuccess,
}) {
}: UserDetailModalProps) {
const [form] = Form.useForm();
const [initialValues, setInitialValues] = useState();
const [teams, setTeams] = useState([]);
const [initialValues, setInitialValues] = useState<Partial<UserDTO>>({});
const [teams, setTeams] = useState<{ label: string; value: string }[]>();
const fetchTeams = async () => {
const data = await axiosInstance.get("/team-label");
const data = await axiosInstance.get<
unknown,
{ label: string; value: string }[]
>("/team-label");
setTeams(data);
};
@ -25,9 +46,9 @@ export default function UserDetailModal({
}, []);
useEffect(() => {
if (visiable) {
if (visible) {
if (mode === "edit") {
const values = {
const values: UserDTO = {
username: user.username,
name: user.name,
phone: user.phone,
@ -38,24 +59,18 @@ export default function UserDetailModal({
setInitialValues(values);
form.setFieldsValue(values);
} else {
const values = {
username: undefined,
password: undefined,
name: undefined,
phone: undefined,
teamId: undefined,
roleId: undefined,
};
const values: Partial<UserDTO> = {};
setInitialValues(values);
form.setFieldsValue(values);
}
}
}, [visiable, mode, user, form]);
}, [visible, mode, user, form]);
const handleOk = async () => {
const values = await form.validateFields();
const data = {};
Object.keys(initialValues).forEach((key) => {
const data: Partial<UserDTO> = {};
Object.keys(initialValues).forEach((_key) => {
const key = _key as keyof UserDTO;
if (values[key] !== initialValues[key]) {
data[key] = values[key];
}
@ -71,14 +86,14 @@ export default function UserDetailModal({
}
}
onSuccess();
onclose();
onClose();
};
return (
<Modal
title={mode === "edit" ? "编辑用户" : "添加用户"}
open={visiable}
open={visible}
onCancel={() => {
onclose();
onClose();
}}
onOk={handleOk}
okText="保存"

View File

@ -2,29 +2,33 @@ import { Button, Flex, Input, Popconfirm, Space, Table } from "antd";
import Column from "antd/es/table/Column";
import { useEffect, useState } from "react";
import axiosInstance from "../../api/axios";
import DeviceDetailModal from "../deviceAdmin/DeviceDetailModal";
import UserDetailModal from "./UserDetailModal";
import { PageResult, Pagination } from "types/common";
import { UserVo } from "types/model";
export default function UserManage() {
const [users, setUsers] = useState([]);
const [teams, setTeams] = useState([]);
const [modalMode, setModalMode] = useState();
const [selectedUser, setSelectedUser] = useState();
const [users, setUsers] = useState<UserVo[]>([]);
// const [teams, setTeams] = useState([]);
const [modalMode, setModalMode] = useState<string>("create");
const [selectedUser, setSelectedUser] = useState<UserVo | null>();
const [modalOpen, setModalOpen] = useState(false);
const [roles, setRoles] = useState([]);
const [pagination, setPagination] = useState({
const [roles, setRoles] = useState<{ label: string; value: string }[]>([]);
const [pagination, setPagination] = useState<Pagination>({
current: 1,
pageSize: 10,
total: 0,
});
const fetchRoles = async () => {
const data = await axiosInstance.get("/role");
const data = await axiosInstance.get<
unknown,
{ label: string; value: string }[]
>("/role");
setRoles(data);
};
const fetchData = async (pagination, name) => {
const data = await axiosInstance.get("/user", {
const fetchData = async (pagination: Pagination, name?: string) => {
const data = await axiosInstance.get<unknown, PageResult<UserVo>>("/user", {
params: {
page: pagination.current,
size: pagination.pageSize,
@ -46,11 +50,11 @@ export default function UserManage() {
});
}, []);
const handlePageChange = async (pagination) => {
const handlePageChange = async (pagination: Pagination) => {
await fetchData(pagination);
};
const handleSearch = async (value) => {
const handleSearch = async (value: string) => {
await fetchData(
{
...pagination,
@ -60,7 +64,7 @@ export default function UserManage() {
);
};
const handleDelete = async (record) => {
const handleDelete = async (record: UserVo) => {
await axiosInstance.delete(`/user/${record.userId}`);
fetchData({
current: 1,
@ -108,7 +112,7 @@ export default function UserManage() {
/>
<Column
title="操作"
render={(_, record) => {
render={(_, record: UserVo) => {
return (
<Space>
<Button
@ -135,22 +139,24 @@ export default function UserManage() {
}}
/>
</Table>
<UserDetailModal
visiable={modalOpen}
mode={modalMode}
user={selectedUser}
roles={roles}
onclose={() => {
setModalOpen(false);
setSelectedUser(null);
}}
onSuccess={async () => {
await fetchData({
...pagination,
current: 1,
});
}}
/>
{selectedUser && (
<UserDetailModal
visible={modalOpen}
mode={modalMode}
user={selectedUser}
roles={roles}
onClose={() => {
setModalOpen(false);
setSelectedUser(null);
}}
onSuccess={async () => {
await fetchData({
...pagination,
current: 1,
});
}}
/>
)}
</>
);
}

View File

@ -15,21 +15,34 @@ import { useSelector } from "react-redux";
import axiosInstance, { baseURL } from "../../api/axios";
import { deviceStatusOptions } from "../../config/DeviceStatusConfig";
import { selectUserId } from "../../features/auth/authSlice";
import { UploadFile } from "antd/lib";
import { DeviceAdminVO } from "./DeviceManage";
import { RcFile } from "antd/es/upload";
interface DeviceDetailModalProps {
visible: boolean;
mode: string;
device: DeviceAdminVO;
onClose: () => void;
onSuccess: () => void;
}
export default function DeviceDetailModal({
visiable,
visible,
mode = "create",
device,
onclose,
onClose,
onSuccess,
}) {
}: DeviceDetailModalProps) {
const [form] = Form.useForm();
const [imageFile, setImageFile] = useState(null);
const [initialValues, setInitialValues] = useState({});
const [fileList, setFileList] = useState();
const [imageFile, setImageFile] = useState<RcFile | null>(null);
const [initialValues, setInitialValues] = useState<Partial<DeviceAdminVO>>(
{}
);
const [fileList, setFileList] = useState<UploadFile[]>();
const userId = useSelector(selectUserId);
useEffect(() => {
if (visiable) {
if (visible) {
setFileList([]);
if (mode === "edit") {
const values = {
@ -42,21 +55,22 @@ export default function DeviceDetailModal({
setInitialValues(values);
} else {
const values = {
name: undefined,
location: undefined,
usageRequirement: undefined,
status: undefined,
name: "",
location: "",
usageRequirement: "",
status: "",
};
form.setFieldsValue(values);
setInitialValues(values);
}
}
}, [visiable, mode, device, form]);
}, [visible, mode, device, form]);
const handleOk = async () => {
const values = await form.validateFields();
const data = {};
Object.keys(initialValues).forEach((key) => {
const data: Partial<DeviceAdminVO> = {};
Object.keys(initialValues).forEach((_key) => {
const key = _key as keyof DeviceAdminVO;
if (values[key] !== initialValues[key]) {
data[key] = values[key];
}
@ -83,15 +97,15 @@ export default function DeviceDetailModal({
message.success("图片上传成功");
}
onSuccess();
onclose();
onClose();
};
return (
<Modal
title={mode === "edit" ? "编辑设备" : "添加设备"}
open={visiable}
open={visible}
onCancel={() => {
onclose();
onClose();
}}
onOk={handleOk}
okText="保存"

View File

@ -9,51 +9,69 @@ import {
Tag,
} from "antd";
import Column from "antd/es/table/Column";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import axiosInstance from "../../api/axios";
import { deviceStatusOptions } from "../../config/DeviceStatusConfig";
import { selectUserId } from "../../features/auth/authSlice";
import DeviceDetailModal from "./DeviceDetailModal";
import { PageResult, Pagination } from "types/common";
export interface DeviceAdminVO {
deviceId: string;
name: string;
usageRequirement: string;
location: string;
imagePath: string;
status: string;
}
export default function DeviceManage() {
const [devices, setDevices] = useState([]);
const [selectedDevice, setSelectedDevice] = useState(null);
const [devices, setDevices] = useState<DeviceAdminVO[]>([]);
const [selectedDevice, setSelectedDevice] = useState<DeviceAdminVO | null>(
null
);
const [modalOpen, setModalOpen] = useState(false);
const [modalMode, setModalMode] = useState(null);
const [searchName, setSearchName] = useState(null);
const [pagination, setPagination] = useState({
const [modalMode, setModalMode] = useState("create");
const [searchName, setSearchName] = useState<string>("");
const [pagination, setPagination] = useState<Pagination>({
current: 1,
pageSize: 10,
total: 0,
});
const userId = useSelector(selectUserId);
const fetchData = async (pagination, name = searchName) => {
const data = await axiosInstance.get(`/device/${userId}`, {
params: {
page: pagination.current,
size: pagination.pageSize,
name,
},
});
const fetchData = useCallback(
async (pagination: Pagination, name: string = searchName) => {
const data = await axiosInstance.get<unknown, PageResult<DeviceAdminVO>>(
`/device/${userId}`,
{
params: {
page: pagination.current,
size: pagination.pageSize,
name,
},
}
);
setDevices(data.records);
setPagination({
...pagination,
total: data.total,
});
};
setDevices(data.records);
setPagination({
...pagination,
total: data.total,
});
},
[userId, searchName]
);
useEffect(() => {
fetchData(pagination);
}, []);
}, [fetchData, pagination]);
const handlePageChange = async (pagination) => {
const handlePageChange = async (pagination: Pagination) => {
await fetchData(pagination);
};
const handleDelete = async (deviceId) => {
const handleDelete = async (deviceId: string) => {
await axiosInstance.delete(`/device/${deviceId}`);
message.success("删除成功");
const newPagination = {
@ -64,7 +82,7 @@ export default function DeviceManage() {
await fetchData(newPagination);
};
const handleSearch = async (value) => {
const handleSearch = async (value: string) => {
setSearchName(value);
const newPagination = {
...pagination,
@ -128,7 +146,7 @@ export default function DeviceManage() {
/>
<Column
title="操作"
render={(_, record) => {
render={(_, record: DeviceAdminVO) => {
return (
<Space>
<Button
@ -155,21 +173,23 @@ export default function DeviceManage() {
}}
/>
</Table>
<DeviceDetailModal
visiable={modalOpen}
device={selectedDevice}
mode={modalMode}
onclose={() => {
setModalOpen(false);
setSelectedDevice(null);
}}
onSuccess={async () => {
await fetchData({
...pagination,
current: 1,
});
}}
/>
{selectedDevice && (
<DeviceDetailModal
visible={modalOpen}
device={selectedDevice}
mode={modalMode}
onClose={() => {
setModalOpen(false);
setSelectedDevice(null);
}}
onSuccess={async () => {
await fetchData({
...pagination,
current: 1,
});
}}
/>
)}
</>
);
}

View File

@ -1,14 +1,26 @@
import { Button, message, Space, Table } from "antd";
import Column from "antd/es/table/Column";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import axiosInstance from "../../api/axios";
import { selectUserId } from "../../features/auth/authSlice";
import { selectUserRole } from "../../features/auth/authSlice";
import { PageResult, Pagination } from "types/common";
interface ReservationVO {
reservationId: string;
applicantName: string;
applicantTeam: string;
applicantContact: string;
deviceId: string;
deviceName: string;
startTime: Date;
endTime: Date;
}
export default function Approval() {
const [reservations, setReservations] = useState([]);
const [pagination, setPagination] = useState({
const [reservations, setReservations] = useState<ReservationVO[]>([]);
const [pagination, setPagination] = useState<Pagination>({
current: 1,
pageSize: 10,
total: 0,
@ -21,30 +33,40 @@ export default function Approval() {
showNeedAssist = true;
}
const fetchData = async (pagination) => {
const data = await axiosInstance.get(`/reservation/approval/${userId}`, {
params: {
page: pagination.current,
size: pagination.pageSize,
},
});
const fetchData = useCallback(
async (pagination: Pagination) => {
const data = await axiosInstance.get<unknown, PageResult<ReservationVO>>(
`/reservation/approval/${userId}`,
{
params: {
page: pagination.current,
size: pagination.pageSize,
},
}
);
setReservations(data.records);
setPagination({
...pagination,
total: data.total,
});
};
setReservations(data.records);
setPagination({
...pagination,
total: data.total,
});
},
[userId]
);
useEffect(() => {
fetchData(pagination);
}, []);
}, [fetchData, pagination]);
const handlePageChange = async (pagination) => {
const handlePageChange = async (pagination: Pagination) => {
await fetchData(pagination);
};
const handleApproval = async (reservationId, isApprove, needAssist) => {
const handleApproval = async (
reservationId: string,
isApprove: boolean,
needAssist: boolean
) => {
await axiosInstance.post("/approval", {
userId,
reservationId,

View File

@ -1,48 +1,71 @@
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 { useCallback, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import axiosInstance from "../../api/axios";
import { selectUserId, selectUserRole } from "../../features/auth/authSlice";
import dayjs from "dayjs";
import dayjs, { Dayjs } from "dayjs";
import { PageResult, Pagination } from "types/common";
interface ApprovalVO {
reservationId: string;
approvalId: string;
applicantName: string;
applicantTeam: string;
applicantContact: string;
deviceName: string;
startTime: string;
endTime: string;
decision: number;
status: string;
}
export default function MyApproval() {
const [approvals, setApprovals] = useState([]);
const [approvals, setApprovals] = useState<ApprovalVO[]>([]);
const [form] = useForm();
const [pagination, setPagination] = useState({
const [pagination, setPagination] = useState<Pagination>({
current: 1,
pageSize: 10,
total: 0,
});
const [editingRow, setEditingRow] = useState(null);
const [tempEndTime, setTempEndTime] = useState(null);
const [editingRow, setEditingRow] = useState<string | null>(null);
const [tempEndTime, setTempEndTime] = useState<Dayjs | null>(null);
const userId = useSelector(selectUserId);
const userRole = useSelector(selectUserRole);
const fetchData = async (pagination, searchParam) => {
const data = await axiosInstance.get(`/approval/${userId}`, {
params: {
page: pagination.current,
size: pagination.pageSize,
applicantName: searchParam?.applicantName,
deviceName: searchParam?.deviceName,
},
});
const fetchData = useCallback(
async (
pagination: Pagination,
searchParam?: { applicantName: string; deviceName: string }
) => {
const data = await axiosInstance.get<unknown, PageResult<ApprovalVO>>(
`/approval/${userId}`,
{
params: {
page: pagination.current,
size: pagination.pageSize,
applicantName: searchParam?.applicantName,
deviceName: searchParam?.deviceName,
},
}
);
setApprovals(data.records);
setPagination({
...pagination,
total: data.total,
});
};
setApprovals(data.records);
setPagination({
...pagination,
total: data.total,
});
},
[userId]
);
useEffect(() => {
fetchData(pagination);
}, []);
}, [fetchData, pagination]);
const handlePageChange = async (pagination) => {
const handlePageChange = async (pagination: Pagination) => {
const values = await form.validateFields();
fetchData(pagination, values);
};
@ -57,7 +80,7 @@ export default function MyApproval() {
await fetchData(newPagination, values);
};
const handleSubmit = async (record) => {
const handleSubmit = async (record: ApprovalVO) => {
try {
await axiosInstance.post(`/reservation/endTime/${record.reservationId}`, {
endTime: dayjs(tempEndTime).format("YYYY-MM-DD"),
@ -66,7 +89,7 @@ export default function MyApproval() {
await fetchData(pagination, values);
setEditingRow(null);
message.success("修改成功");
} catch (error) {
} catch {
message.error("修改失败");
}
};
@ -109,7 +132,7 @@ export default function MyApproval() {
<Column
title="结束时间"
key="endTime"
render={(_, record) => {
render={(_, record: ApprovalVO) => {
const isEditable =
record.decision === 1 && userRole.includes("DEVICE_ADMIN");

View File

@ -1,50 +1,67 @@
import { Button, Col, Form, Input, message, Row } from "antd";
import { useForm } from "antd/es/form/Form";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import axiosInstance from "../../api/axios";
import { selectUserId } from "../../features/auth/authSlice";
import { UserVo } from "types/model";
interface UserForm {
name: string;
phone: string;
password: string;
confirmPassword: string;
}
export default function UserDetail() {
const [form] = Form.useForm();
const [showPassword, setShowPassword] = useState(false);
const [user, setUser] = useState({
userId: "",
username: "",
team: "",
name: "",
phone: "",
});
const [user, setUser] = useState<Partial<UserVo>>({});
const userId = useSelector(selectUserId);
const fetchUser = async (userId) => {
const user = await axiosInstance.get(`/userdetail/${userId}`);
setUser(user);
form.setFieldsValue(user);
};
const fetchUser = useCallback(
async (userId: string | null) => {
if (!userId) return;
const user = await axiosInstance.get<unknown, UserVo>(
`/userdetail/${userId}`
);
setUser(user);
form.setFieldsValue(user);
},
[form]
);
useEffect(() => {
fetchUser(userId);
}, []);
}, [fetchUser, userId]);
const handleReset = () => {
form.resetFields();
};
const handleSubmit = async (values) => {
const handleSubmit = async (values: UserForm) => {
if (values.password && values.password !== values.confirmPassword) {
message.error("两次输入的密码不一致");
return;
}
const changedFields = {};
for (const key in values) {
if (values[key] !== user[key] && values[key] !== undefined) {
type FormKey = keyof UserForm;
const changedFields: Record<string, string> = {};
for (const _key in values) {
const key = _key as FormKey;
if (!(key in user)) {
changedFields[key] = values[key];
continue;
}
const userValue = (user as Record<FormKey, unknown>)[key];
if (values[key] !== userValue && values[key] !== undefined) {
changedFields[key] = values[key];
}
}
delete changedFields.confirmPassword;
const newUser = await axiosInstance.put(`/user/${userId}`, changedFields);
const newUser = await axiosInstance.put<unknown, UserVo>(
`/user/${userId}`,
changedFields
);
setUser(newUser);
form.setFieldsValue(newUser);
message.success("修改成功");

View File

@ -10,21 +10,45 @@ import {
Select,
Space,
} from "antd";
import dayjs from "dayjs";
import dayjs, { Dayjs } from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import axiosInstance, { baseURL } from "../../api/axios";
import { selectUserId } from "../../features/auth/authSlice";
import { UserVo } from "types/model";
import { DeviceVO } from "./Reserve";
export default function DeviceDetailModal({ visiable, device, onclose }) {
const [unavailableTimes, setUnavailableTims] = useState([]);
interface FormValue {
name: string;
phone: string;
team: string;
}
interface DeviceDetailModalProps {
visible: boolean;
device: DeviceVO;
onClose: () => void;
}
export default function DeviceDetailModal({
visible,
device,
onClose,
}: DeviceDetailModalProps) {
const [unavailableTimes, setUnavailableTimes] = useState<
{ startTime: Date; endTime: Date }[]
>([]);
const [form] = Form.useForm();
const userId = useSelector((state) => state.auth.userId);
const [teams, setTeams] = useState([]);
const [initialValues, setInitialValues] = useState();
const userId = useSelector(selectUserId);
const [teams, setTeams] = useState<{ label: string; value: string }[]>([]);
const [initialValues, setInitialValues] = useState<FormValue>();
const fetchTeams = async () => {
const data = await axiosInstance.get("/team-label");
const data = await axiosInstance.get<
unknown,
{ label: string; value: string }[]
>("/team-label");
const teams = data.map((item) => ({
label: item.label,
value: item.label,
@ -32,8 +56,10 @@ export default function DeviceDetailModal({ visiable, device, onclose }) {
setTeams(teams);
};
const fetchUser = async () => {
const data = await axiosInstance.get(`/userdetail/${userId}`);
const fetchUser = useCallback(async () => {
const data = await axiosInstance.get<unknown, UserVo>(
`/userdetail/${userId}`
);
const values = {
name: data.name,
phone: data.phone,
@ -41,28 +67,34 @@ export default function DeviceDetailModal({ visiable, device, onclose }) {
};
setInitialValues(values);
form.setFieldsValue(values);
};
}, [userId, form]);
useEffect(() => {
fetchTeams();
}, []);
useEffect(() => {
const fetchUnavailableTimes = async (id) => {
const data = await axiosInstance.get(`/device/unavailable-times/${id}`);
setUnavailableTims(data);
const fetchUnavailableTimes = async (id: string) => {
const data = await axiosInstance.get<
unknown,
{ startTime: Date; endTime: Date }[]
>(`/device/unavailable-times/${id}`);
setUnavailableTimes(data);
};
if (visiable && device?.deviceId) {
if (visible && device?.deviceId) {
fetchUnavailableTimes(device.deviceId);
fetchUser();
}
}, [visiable, device?.deviceId]);
}, [visible, device?.deviceId, fetchUser]);
const { RangePicker } = DatePicker;
const disabledDate = (current, { from } = {}) => {
const disabledDate = (
current: Dayjs,
info: { from?: Dayjs } = {}
): boolean => {
if (!current) return false;
const { from } = info;
const today = dayjs().startOf("day");
const currentDay = current.startOf("day");
@ -78,7 +110,7 @@ export default function DeviceDetailModal({ visiable, device, onclose }) {
});
// 限制选择范围为 7 天内(从 from 开始算起)
const isExceedingRange = from && Math.abs(current.diff(from, "day")) >= 7;
const isExceedingRange = !!from && Math.abs(current.diff(from, "day")) >= 7;
return isPastDate || isUnavailable || isExceedingRange;
};
@ -101,15 +133,15 @@ export default function DeviceDetailModal({ visiable, device, onclose }) {
await axiosInstance.post("/reservation", payload);
message.success("预约成功");
form.resetFields();
onclose();
onClose();
};
return (
<Modal
title="预约设备"
open={visiable}
open={visible}
onCancel={() => {
onclose();
onClose();
}}
onOk={handleOK}
>

View File

@ -1,14 +1,28 @@
import { Table, Tag } from "antd";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import axiosInstance from "../../api/axios";
import { useSelector } from "react-redux";
import Column from "antd/es/table/Column";
import dayjs from "dayjs";
import { selectUserId } from "../../features/auth/authSlice";
import { PageResult, Pagination } from "types/common";
interface UserReservationVO {
reservationId: string;
deviceName: string;
startTime: Date;
endTime: Date;
statusLabel: string;
deviceLeaderName: string;
deviceLeaderContact: string;
deviceAdminName: string;
deviceAdminContact: string;
createdTime: Date;
}
export default function MyReservation() {
const [reservations, setReservations] = useState([]);
const [pagination, setPagination] = useState({
const [reservations, setReservations] = useState<UserReservationVO[]>([]);
const [pagination, setPagination] = useState<Pagination>({
current: 1,
pageSize: 10,
total: 0,
@ -16,25 +30,31 @@ export default function MyReservation() {
const userId = useSelector(selectUserId);
const fetchData = async (pagination) => {
const data = await axiosInstance.get(`/reservation/${userId}`, {
params: {
page: pagination.current,
size: pagination.pageSize,
},
});
setReservations(data.records);
setPagination({
...pagination,
total: data.total,
});
};
const fetchData = useCallback(
async (pagination: Pagination) => {
const data = await axiosInstance.get<
unknown,
PageResult<UserReservationVO>
>(`/reservation/${userId}`, {
params: {
page: pagination.current,
size: pagination.pageSize,
},
});
setReservations(data.records);
setPagination({
...pagination,
total: data.total,
});
},
[userId]
);
useEffect(() => {
fetchData(pagination);
}, []);
}, [fetchData, pagination]);
const handlePageChange = (pagination) => {
const handlePageChange = (pagination: Pagination) => {
fetchData(pagination);
};

View File

@ -1,8 +1,9 @@
import { Input, Space, Table, Tag } from "antd";
import Column from "antd/es/table/Column";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import axiosInstance from "../../api/axios";
import DeviceDetailModal from "./DeviceDetailModal";
import { PageResult, Pagination } from "types/common";
const statusColorMap = {
: "green",
@ -11,41 +12,57 @@ const statusColorMap = {
: "gray",
};
export interface DeviceVO {
deviceId: string;
name: string;
usageRequirement: string;
location: string;
imagePath: string;
state: string;
}
export default function Reserve() {
const [name, setName] = useState(null);
const [pagination, setPagination] = useState({
const [name, setName] = useState<string | null>(null);
const [pagination, setPagination] = useState<Pagination>({
current: 1,
pageSize: 10,
total: 0,
});
const [devices, setDevices] = useState([]);
const [devices, setDevices] = useState<DeviceVO[]>([]);
const fetchData = async (pagination, searchName = name) => {
const data = await axiosInstance.get("/device", {
params: {
page: pagination.current,
size: pagination.pageSize,
name: searchName,
},
});
const fetchData = useCallback(
async (pagination: Pagination, searchName = name) => {
const data = await axiosInstance.get<unknown, PageResult<DeviceVO>>(
"/device",
{
params: {
page: pagination.current,
size: pagination.pageSize,
name: searchName,
},
}
);
setDevices(data.records);
setPagination({
...pagination,
total: data.total,
});
},
[name]
);
setDevices(data.records);
setPagination({
...pagination,
total: data.total,
});
};
useEffect(() => {
fetchData(pagination);
}, []);
}, [fetchData, pagination]);
const handlePageChange = (pagination) => {
const handlePageChange = (pagination: Pagination) => {
fetchData(pagination);
};
const [selectedDevice, setSelectedDevice] = useState(null);
const [selectedDevice, setSelectedDevice] = useState<DeviceVO | null>(null);
const handleSearch = (value) => {
const handleSearch = (value: string) => {
setName(value);
const newPagination = {
...pagination,
@ -74,7 +91,7 @@ export default function Reserve() {
title="使用要求"
key="usageRequirement"
dataIndex="usageRequirement"
ellipsis="true"
ellipsis={true}
/>
<Column title="位置" key="location" dataIndex="location" />
<Column
@ -83,7 +100,10 @@ export default function Reserve() {
dataIndex="state"
render={(_, { state }) => (
<>
<Tag color={statusColorMap[state]} key="state">
<Tag
color={statusColorMap[state as keyof typeof statusColorMap]}
key="state"
>
{state}
</Tag>
</>
@ -92,18 +112,21 @@ export default function Reserve() {
<Column
title="操作"
key="action"
render={(_, record) => (
render={(_, record: DeviceVO) => (
<Space size="middle">
<a onClick={() => setSelectedDevice(record)}></a>
</Space>
)}
/>
</Table>
<DeviceDetailModal
visiable={!!selectedDevice}
device={selectedDevice}
onclose={() => setSelectedDevice(null)}
/>
{selectedDevice && (
<DeviceDetailModal
visible={!!selectedDevice}
device={selectedDevice}
onClose={() => setSelectedDevice(null)}
/>
)}
</>
);
}

View File

@ -2,7 +2,11 @@ import { useSelector } from "react-redux";
import { Navigate, Outlet } from "react-router-dom";
import { selectUserRole } from "../features/auth/authSlice";
export default function ProtectedRoute({ allowedRoles }) {
export default function ProtectedRoute({
allowedRoles,
}: {
allowedRoles: string[];
}) {
const roles = useSelector(selectUserRole);
if (roles.length === 0) return <Navigate to="/login" replace />;

View File

@ -1,5 +1,6 @@
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "../features/auth/authSlice";
import { TypedUseSelectorHook, useSelector } from "react-redux";
const userId = localStorage.getItem("userId");
const name = localStorage.getItem("name");
@ -21,3 +22,7 @@ export const store = configureStore({
},
preloadedState,
});
export type RootState = ReturnType<typeof store.getState>;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

7
src/types/axios.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import "axios";
declare module "axios" {
export interface AxiosRequestConfig {
skipInterceptor?: boolean;
}
}

12
src/types/common.ts Normal file
View File

@ -0,0 +1,12 @@
export interface PageResult<T> {
records: T[];
total: number;
size: number;
current: number;
}
export interface Pagination {
current?: number;
pageSize?: number;
total?: number;
}

15
src/types/model.ts Normal file
View File

@ -0,0 +1,15 @@
export interface Team {
id: string;
name: string;
size: number;
}
export interface UserVo {
userId: string;
username: string;
team: string;
teamId?: string;
name: string;
phone: string;
roleId?: string;
}

1
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

115
tsconfig.json Normal file
View File

@ -0,0 +1,115 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [
"DOM",
"DOM.Iterable",
"ESNext"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"jsx": "react-jsx" /* Specify what JSX code is generated. */,
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "esnext" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
"baseUrl": "src" /* Specify the base directory to resolve non-relative module names. */,
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
"resolveJsonModule": true /* Enable importing .json files. */,
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
"noEmit": true /* Disable emitting files from a compilation. */,
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@ -5,4 +5,8 @@ import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
server: {
host: "0.0.0.0",
port: 5173,
},
});