Compare commits
4 Commits
main
...
ts-migrati
| Author | SHA1 | Date | |
|---|---|---|---|
| 12db8d668e | |||
| d1a7eed16b | |||
| d56bca6692 | |||
| b3d4e14409 |
@ -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
2254
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
@ -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")],
|
||||
@ -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: "设备预约",
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
);
|
||||
15
src/features/auth/authThunk.ts
Normal file
15
src/features/auth/authThunk.ts
Normal 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;
|
||||
}
|
||||
);
|
||||
11
src/features/auth/types.ts
Normal file
11
src/features/auth/types.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface LoginResponse {
|
||||
userId: string;
|
||||
name: string;
|
||||
roles: string[];
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface LoginForm {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
@ -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}>
|
||||
@ -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);
|
||||
@ -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"
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
46
src/pages/admin/TeamDetailModal.tsx
Normal file
46
src/pages/admin/TeamDetailModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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="保存"
|
||||
@ -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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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="保存"
|
||||
@ -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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
@ -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");
|
||||
|
||||
@ -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("修改成功");
|
||||
@ -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}
|
||||
>
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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 />;
|
||||
@ -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
7
src/types/axios.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
import "axios";
|
||||
|
||||
declare module "axios" {
|
||||
export interface AxiosRequestConfig {
|
||||
skipInterceptor?: boolean;
|
||||
}
|
||||
}
|
||||
12
src/types/common.ts
Normal file
12
src/types/common.ts
Normal 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
15
src/types/model.ts
Normal 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
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
115
tsconfig.json
Normal file
115
tsconfig.json
Normal 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"]
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user