Compare commits
4 Commits
main
...
ts-migrati
| Author | SHA1 | Date | |
|---|---|---|---|
| 12db8d668e | |||
| d1a7eed16b | |||
| d56bca6692 | |||
| b3d4e14409 |
@ -1,33 +1,41 @@
|
|||||||
import js from '@eslint/js'
|
import js from "@eslint/js";
|
||||||
import globals from 'globals'
|
import globals from "globals";
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import tseslint from "typescript-eslint";
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
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 [
|
export default [
|
||||||
{ ignores: ['dist'] },
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{ ignores: ["dist"] },
|
||||||
{
|
{
|
||||||
files: ['**/*.{js,jsx}'],
|
files: ["**/*.{js,jsx,tsx}"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 'latest',
|
ecmaVersion: "latest",
|
||||||
ecmaFeatures: { jsx: true },
|
ecmaFeatures: { jsx: true },
|
||||||
sourceType: 'module',
|
sourceType: "module",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
'react-hooks': reactHooks,
|
react,
|
||||||
'react-refresh': reactRefresh,
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
prettier,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...js.configs.recommended.rules,
|
...js.configs.recommended.rules,
|
||||||
...reactHooks.configs.recommended.rules,
|
...reactHooks.configs.recommended.rules,
|
||||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
"no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
|
||||||
'react-refresh/only-export-components': [
|
"react-refresh/only-export-components": [
|
||||||
'warn',
|
"warn",
|
||||||
{ allowConstantExport: true },
|
{ 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": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.0",
|
"@eslint/js": "^9.25.0",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@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",
|
"@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-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"typescript-eslint": "^8.41.0",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,19 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Popconfirm, Button, message } from "antd";
|
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 [visiable, setVisiable] = useState(false);
|
||||||
|
|
||||||
const handleClick = (record) => {
|
const handleClick = (record: Team) => {
|
||||||
if (record.size > 0) {
|
if (record.size > 0) {
|
||||||
message.warning("该团队下还有成员,无法删除");
|
message.warning("该团队下还有成员,无法删除");
|
||||||
} else {
|
} else {
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
import quarterOfYear from "dayjs/plugin/quarterOfYear";
|
import quarterOfYear from "dayjs/plugin/quarterOfYear";
|
||||||
|
|
||||||
dayjs.extend(quarterOfYear);
|
dayjs.extend(quarterOfYear);
|
||||||
|
|
||||||
export const datePresets = [
|
export const datePresets: Array<{ label: string; value: [Dayjs, Dayjs] }> = [
|
||||||
{
|
{
|
||||||
label: "本月",
|
label: "本月",
|
||||||
value: [dayjs().startOf("month"), dayjs().endOf("month")],
|
value: [dayjs().startOf("month"), dayjs().endOf("month")],
|
||||||
@ -10,7 +10,16 @@ import {
|
|||||||
UserOutlined,
|
UserOutlined,
|
||||||
} from "@ant-design/icons";
|
} 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",
|
path: "/user/reserve",
|
||||||
label: "设备预约",
|
label: "设备预约",
|
||||||
@ -1,19 +1,29 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
import { login } from "./authThunk";
|
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({
|
const authSlice = createSlice({
|
||||||
name: "authSlice",
|
name: "authSlice",
|
||||||
initialState: {
|
initialState: initialState,
|
||||||
userId: null,
|
|
||||||
name: null,
|
|
||||||
roles: [],
|
|
||||||
token: null,
|
|
||||||
},
|
|
||||||
reducers: {
|
reducers: {
|
||||||
logout(state) {
|
logout(state) {
|
||||||
state.userId = null;
|
state.userId = "";
|
||||||
state.name = null;
|
state.name = "";
|
||||||
state.token = null;
|
state.token = "";
|
||||||
state.roles = [];
|
state.roles = [];
|
||||||
|
|
||||||
localStorage.removeItem("userId");
|
localStorage.removeItem("userId");
|
||||||
@ -30,7 +40,7 @@ const authSlice = createSlice({
|
|||||||
state.roles = payload.roles;
|
state.roles = payload.roles;
|
||||||
state.token = payload.token;
|
state.token = payload.token;
|
||||||
|
|
||||||
localStorage.setItem("userId", payload.userId);
|
localStorage.setItem("userId", payload.userId.toString());
|
||||||
localStorage.setItem("name", payload.name);
|
localStorage.setItem("name", payload.name);
|
||||||
localStorage.setItem("roles", JSON.stringify(payload.roles));
|
localStorage.setItem("roles", JSON.stringify(payload.roles));
|
||||||
localStorage.setItem("token", action.payload.token);
|
localStorage.setItem("token", action.payload.token);
|
||||||
@ -39,7 +49,7 @@ const authSlice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const { logout } = authSlice.actions;
|
export const { logout } = authSlice.actions;
|
||||||
export const selectUserRole = (state) => state.auth.roles;
|
export const selectUserRole = (state: RootState) => state.auth.roles;
|
||||||
export const selectUserName = (state) => state.auth.name;
|
export const selectUserName = (state: RootState) => state.auth.name;
|
||||||
export const selectUserId = (state) => state.auth.userId;
|
export const selectUserId = (state: RootState) => state.auth.userId;
|
||||||
export default authSlice.reducer;
|
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 React, { useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
import menuConfig from "../config/menuConfig";
|
import menuConfig, { MenuItem } from "../config/menuConfig";
|
||||||
import {
|
import {
|
||||||
logout,
|
logout,
|
||||||
selectUserName,
|
selectUserName,
|
||||||
selectUserRole,
|
selectUserRole,
|
||||||
} from "../features/auth/authSlice";
|
} from "../features/auth/authSlice";
|
||||||
|
import { ItemType, MenuItemType } from "antd/es/menu/interface";
|
||||||
|
|
||||||
export default function CommonLayout() {
|
export default function CommonLayout() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -36,7 +37,7 @@ export default function CommonLayout() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleMenuClick = ({ key }) => {
|
const handleMenuClick = ({ key }: { key: string }) => {
|
||||||
if (key === "logout") {
|
if (key === "logout") {
|
||||||
dispatch(logout());
|
dispatch(logout());
|
||||||
message.success("已退出登录");
|
message.success("已退出登录");
|
||||||
@ -45,7 +46,7 @@ export default function CommonLayout() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 递归排序函数
|
// 递归排序函数
|
||||||
const sortMenu = (menu) => {
|
const sortMenu = (menu: MenuItem[]): MenuItem[] => {
|
||||||
return [...menu]
|
return [...menu]
|
||||||
.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
|
.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
@ -54,8 +55,8 @@ export default function CommonLayout() {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildMenuItems = (menu) =>
|
const buildMenuItems = (menu: MenuItem[]): ItemType<MenuItemType>[] =>
|
||||||
sortMenu(menu).map((item) => {
|
sortMenu(menu).map((item: MenuItem): ItemType<MenuItemType> => {
|
||||||
if (item.children) {
|
if (item.children) {
|
||||||
return {
|
return {
|
||||||
key: item.label, // 可改为 item.path 但注意不冲突
|
key: item.label, // 可改为 item.path 但注意不冲突
|
||||||
@ -65,7 +66,7 @@ export default function CommonLayout() {
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
key: item.path,
|
key: item.path ?? item.label,
|
||||||
label: item.label,
|
label: item.label,
|
||||||
icon: item.icon ? React.createElement(item.icon) : null,
|
icon: item.icon ? React.createElement(item.icon) : null,
|
||||||
};
|
};
|
||||||
@ -3,7 +3,7 @@ import { createRoot } from "react-dom/client";
|
|||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { RouterProvider } from "react-router-dom";
|
import { RouterProvider } from "react-router-dom";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import router from "./router/index.jsx";
|
import router from "./router/index.js";
|
||||||
import { store } from "./store/index.js";
|
import { store } from "./store/index.js";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import zhCN from "antd/locale/zh_CN";
|
import zhCN from "antd/locale/zh_CN";
|
||||||
@ -11,7 +11,7 @@ import { ConfigProvider } from "antd";
|
|||||||
|
|
||||||
dayjs.locale("zh-cn");
|
dayjs.locale("zh-cn");
|
||||||
|
|
||||||
createRoot(document.getElementById("root")).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ConfigProvider locale={zhCN}>
|
<ConfigProvider locale={zhCN}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
@ -4,14 +4,18 @@ import { useDispatch } from "react-redux";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { login } from "../features/auth/authThunk";
|
import { login } from "../features/auth/authThunk";
|
||||||
import roleRoute from "../config/roleRouteConfig";
|
import roleRoute from "../config/roleRouteConfig";
|
||||||
|
import { store } from "store";
|
||||||
|
import { LoginForm } from "features/auth/types";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch<typeof store.dispatch>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const onFinish = async (values) => {
|
const onFinish = async (values: LoginForm) => {
|
||||||
const res = await dispatch(login(values)).unwrap();
|
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) {
|
if (path) {
|
||||||
message.success("登录成功");
|
message.success("登录成功");
|
||||||
navigate(path);
|
navigate(path);
|
||||||
@ -1,20 +1,40 @@
|
|||||||
import { message, Modal, Space, Spin, Table } from "antd";
|
import { message, Modal, Spin, Table } from "antd";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import axiosInstance from "../../api/axios";
|
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({
|
export default function DeviceDetailStatsModal({
|
||||||
visible,
|
visible,
|
||||||
record,
|
record,
|
||||||
range,
|
range,
|
||||||
onClose,
|
onClose,
|
||||||
}) {
|
}: DeviceDetailStatsModalProps) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState<DeviceDetailStats[]>([]);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await axiosInstance.get("/device/detail-stats", {
|
const res = await axiosInstance.get<
|
||||||
|
DeviceDetailStats[],
|
||||||
|
DeviceDetailStats[]
|
||||||
|
>("/device/detail-stats", {
|
||||||
params: {
|
params: {
|
||||||
deviceId: record.deviceId,
|
deviceId: record.deviceId,
|
||||||
start: range[0].format("YYYY-MM-DD"),
|
start: range[0].format("YYYY-MM-DD"),
|
||||||
@ -22,12 +42,12 @@ export default function DeviceDetailStatsModal({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
setData(res);
|
setData(res);
|
||||||
} catch (e) {
|
} catch {
|
||||||
message.error("获取数据失败");
|
message.error("获取数据失败");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [record, range]);
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -73,9 +93,9 @@ export default function DeviceDetailStatsModal({
|
|||||||
if (visible) {
|
if (visible) {
|
||||||
fetchData();
|
fetchData();
|
||||||
}
|
}
|
||||||
}, [visible, record]);
|
}, [visible, fetchData]);
|
||||||
|
|
||||||
const columns = [
|
const columns: ColumnsType<DeviceDetailStats> = [
|
||||||
{
|
{
|
||||||
title: "使用人",
|
title: "使用人",
|
||||||
dataIndex: "applicantName",
|
dataIndex: "applicantName",
|
||||||
@ -117,8 +137,8 @@ export default function DeviceDetailStatsModal({
|
|||||||
okText="导出Excel"
|
okText="导出Excel"
|
||||||
onOk={handleExport}
|
onOk={handleExport}
|
||||||
>
|
>
|
||||||
<Table
|
<Table<DeviceDetailStats>
|
||||||
rowKey={(record) => record.deviceId}
|
// rowKey={(record) => record.deviceId}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
@ -1,43 +1,55 @@
|
|||||||
import { Button, DatePicker, Input, Space, Spin, Table, message } from "antd";
|
import { Button, DatePicker, Input, message, Space, Spin, Table } from "antd";
|
||||||
import dayjs from "dayjs";
|
import { ColumnsType } from "antd/es/table";
|
||||||
import { useEffect, useState } from "react";
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import axiosInstance from "../../api/axios";
|
import axiosInstance from "../../api/axios";
|
||||||
import { datePresets } from "../../config/datePresetsConfig";
|
import { datePresets } from "../../config/datePresetsConfig";
|
||||||
import DeviceDetailStatsModal from "./DeviceDetailStatsModal";
|
import DeviceDetailStatsModal from "./DeviceDetailStatsModal";
|
||||||
|
|
||||||
|
export interface UsageStats {
|
||||||
|
deviceId: string;
|
||||||
|
deviceName: string;
|
||||||
|
usageCount: number;
|
||||||
|
totalUsageDays: number;
|
||||||
|
}
|
||||||
|
|
||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
export default function DeviceStats() {
|
export default function DeviceStats() {
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState<UsageStats[]>([]);
|
||||||
const [filteredData, setFilteredData] = useState([]);
|
const [filteredData, setFilteredData] = useState<UsageStats[]>([]);
|
||||||
const [range, setRange] = useState([
|
const [range, setRange] = useState<[Dayjs, Dayjs]>([
|
||||||
dayjs().startOf("month"),
|
dayjs().startOf("month"),
|
||||||
dayjs().endOf("month"),
|
dayjs().endOf("month"),
|
||||||
]);
|
]);
|
||||||
|
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [visiable, setVisiable] = 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);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await axiosInstance.get("/device/usage-stats", {
|
const res = await axiosInstance.get<UsageStats[], UsageStats[]>(
|
||||||
|
"/device/usage-stats",
|
||||||
|
{
|
||||||
params: {
|
params: {
|
||||||
start: range[0].format("YYYY-MM-DD"),
|
start: range[0].format("YYYY-MM-DD"),
|
||||||
end: range[1].format("YYYY-MM-DD"),
|
end: range[1].format("YYYY-MM-DD"),
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
setData(res);
|
setData(res);
|
||||||
setFilteredData(res);
|
setFilteredData(res);
|
||||||
} catch (e) {
|
} catch {
|
||||||
message.error("获取数据失败");
|
message.error("获取数据失败");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [range]);
|
||||||
|
|
||||||
const handleSearch = (value) => {
|
const handleSearch = (value: string) => {
|
||||||
setSearch(value);
|
setSearch(value);
|
||||||
const filtered = data.filter((item) =>
|
const filtered = data.filter((item) =>
|
||||||
item.deviceName.toLowerCase().includes(value.toLowerCase())
|
item.deviceName.toLowerCase().includes(value.toLowerCase())
|
||||||
@ -89,9 +101,9 @@ export default function DeviceStats() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [range]);
|
}, [fetchData]);
|
||||||
|
|
||||||
const columns = [
|
const columns: ColumnsType<UsageStats> = [
|
||||||
{
|
{
|
||||||
title: "设备名称",
|
title: "设备名称",
|
||||||
dataIndex: "deviceName",
|
dataIndex: "deviceName",
|
||||||
@ -135,7 +147,7 @@ export default function DeviceStats() {
|
|||||||
<RangePicker
|
<RangePicker
|
||||||
value={range}
|
value={range}
|
||||||
onChange={(dates) => {
|
onChange={(dates) => {
|
||||||
if (dates) setRange(dates);
|
if (dates && dates[0] && dates[1]) setRange([dates[0], dates[1]]);
|
||||||
}}
|
}}
|
||||||
presets={datePresets}
|
presets={datePresets}
|
||||||
/>
|
/>
|
||||||
@ -150,11 +162,12 @@ export default function DeviceStats() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Table
|
<Table<UsageStats>
|
||||||
rowKey={(record) => record.deviceId}
|
rowKey={(record) => record.deviceId}
|
||||||
dataSource={filteredData}
|
dataSource={filteredData}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
/>
|
/>
|
||||||
|
{selectedRecord && (
|
||||||
<DeviceDetailStatsModal
|
<DeviceDetailStatsModal
|
||||||
visible={visiable}
|
visible={visiable}
|
||||||
record={selectedRecord}
|
record={selectedRecord}
|
||||||
@ -163,6 +176,7 @@ export default function DeviceStats() {
|
|||||||
}}
|
}}
|
||||||
range={range}
|
range={range}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</Spin>
|
</Spin>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,40 +1,53 @@
|
|||||||
import { Button, DatePicker, Input, Space, Spin, Table, message } from "antd";
|
import { Button, DatePicker, Input, Space, Spin, Table, message } from "antd";
|
||||||
import dayjs from "dayjs";
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import axiosInstance from "../../api/axios";
|
import axiosInstance from "../../api/axios";
|
||||||
import { datePresets } from "../../config/datePresetsConfig";
|
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;
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
export default function ReservationStats() {
|
export default function ReservationStats() {
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState<ReservationStat[]>([]);
|
||||||
const [filteredData, setFilteredData] = useState([]);
|
const [filteredData, setFilteredData] = useState<ReservationStat[]>([]);
|
||||||
const [range, setRange] = useState([
|
const [range, setRange] = useState<[Dayjs, Dayjs]>([
|
||||||
dayjs().startOf("month"),
|
dayjs().startOf("month"),
|
||||||
dayjs().endOf("month"),
|
dayjs().endOf("month"),
|
||||||
]);
|
]);
|
||||||
|
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await axiosInstance.get("/reservation/stats", {
|
const res = await axiosInstance.get<ReservationStat[], ReservationStat[]>(
|
||||||
|
"/reservation/stats",
|
||||||
|
{
|
||||||
params: {
|
params: {
|
||||||
start: range[0].format("YYYY-MM-DD"),
|
start: range[0].format("YYYY-MM-DD"),
|
||||||
end: range[1].format("YYYY-MM-DD"),
|
end: range[1].format("YYYY-MM-DD"),
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
setData(res);
|
setData(res);
|
||||||
setFilteredData(res);
|
setFilteredData(res);
|
||||||
} catch (e) {
|
} catch {
|
||||||
message.error("获取数据失败");
|
message.error("获取数据失败");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [range]);
|
||||||
|
|
||||||
const handleSearch = (value) => {
|
const handleSearch = (value: string) => {
|
||||||
setSearch(value);
|
setSearch(value);
|
||||||
const filtered = data.filter((item) =>
|
const filtered = data.filter((item) =>
|
||||||
item.deviceName.toLowerCase().includes(value.toLowerCase())
|
item.deviceName.toLowerCase().includes(value.toLowerCase())
|
||||||
@ -86,9 +99,9 @@ export default function ReservationStats() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [range]);
|
}, [fetchData]);
|
||||||
|
|
||||||
const columns = [
|
const columns: ColumnsType<ReservationStat> = [
|
||||||
{
|
{
|
||||||
title: "设备名称",
|
title: "设备名称",
|
||||||
dataIndex: "deviceName",
|
dataIndex: "deviceName",
|
||||||
@ -121,7 +134,7 @@ export default function ReservationStats() {
|
|||||||
<RangePicker
|
<RangePicker
|
||||||
value={range}
|
value={range}
|
||||||
onChange={(dates) => {
|
onChange={(dates) => {
|
||||||
if (dates) setRange(dates);
|
if (dates && dates[0] && dates[1]) setRange([dates[0], dates[1]]);
|
||||||
}}
|
}}
|
||||||
presets={datePresets}
|
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 axiosInstance from "../../api/axios";
|
||||||
import TeamDeleteButton from "../../components/TeamDeleteButton";
|
import TeamDeleteButton from "../../components/TeamDeleteButton";
|
||||||
import TeamDetailModal from "./TeamDetailModal";
|
import TeamDetailModal from "./TeamDetailModal";
|
||||||
|
import { Team } from "types/model";
|
||||||
|
|
||||||
export default function TeamManage() {
|
export default function TeamManage() {
|
||||||
const [teams, setTeams] = useState([]);
|
const [teams, setTeams] = useState<Team[]>([]);
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState<Team[]>([]);
|
||||||
const [searchName, setSearchName] = useState();
|
const [searchName, setSearchName] = useState<string>();
|
||||||
const [editingId, setEditingId] = useState();
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [editingName, setEditingName] = useState("");
|
const [editingName, setEditingName] = useState("");
|
||||||
const [newTeamName, setNewTeamName] = useState();
|
const [newTeamName, setNewTeamName] = useState("");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [selectedTeam, setSelectedTeam] = useState(null);
|
const [selectedTeam, setSelectedTeam] = useState<Team | null>(null);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
const data = await axiosInstance.get("/teams");
|
const data = await axiosInstance.get<Team[], Team[]>("/teams");
|
||||||
setData(data);
|
setData(data);
|
||||||
setTeams(data);
|
setTeams(data);
|
||||||
setSearchName(null);
|
setSearchName("");
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSearch = (value) => {
|
const handleSearch = (value: string) => {
|
||||||
setSearchName(value);
|
setSearchName(value);
|
||||||
const filtered = data.filter((item) =>
|
const filtered = data.filter((item) =>
|
||||||
item.name.toLowerCase().includes(value.toLowerCase())
|
item.name.toLowerCase().includes(value.toLowerCase())
|
||||||
@ -34,18 +35,18 @@ export default function TeamManage() {
|
|||||||
setTeams(filtered);
|
setTeams(filtered);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (record) => {
|
const handleDelete = async (record: Team) => {
|
||||||
await axiosInstance.delete(`/team/${record.id}`);
|
await axiosInstance.delete(`/team/${record.id}`);
|
||||||
message.success("删除成功");
|
message.success("删除成功");
|
||||||
fetchData();
|
fetchData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (record) => {
|
const handleEdit = (record: Team) => {
|
||||||
setEditingId(record.id);
|
setEditingId(record.id);
|
||||||
setEditingName(record.name);
|
setEditingName(record.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async (teamId) => {
|
const handleSave = async (teamId: string) => {
|
||||||
if (!editingName.trim()) return message.warning("请输入新名称");
|
if (!editingName.trim()) return message.warning("请输入新名称");
|
||||||
await axiosInstance.put(`/team/${teamId}`, { name: editingName });
|
await axiosInstance.put(`/team/${teamId}`, { name: editingName });
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
@ -62,7 +63,7 @@ export default function TeamManage() {
|
|||||||
name: newTeamName,
|
name: newTeamName,
|
||||||
});
|
});
|
||||||
message.success("添加成功");
|
message.success("添加成功");
|
||||||
setNewTeamName(null);
|
setNewTeamName("");
|
||||||
fetchData();
|
fetchData();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -87,7 +88,7 @@ export default function TeamManage() {
|
|||||||
style={{ width: "300px" }}
|
style={{ width: "300px" }}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Table rowKey="id" dataSource={teams}>
|
<Table<Team> rowKey="id" dataSource={teams}>
|
||||||
<Column
|
<Column
|
||||||
title="团队名"
|
title="团队名"
|
||||||
key="name"
|
key="name"
|
||||||
@ -106,7 +107,7 @@ export default function TeamManage() {
|
|||||||
<Column title="下属人数" key="size" dataIndex="size" />
|
<Column title="下属人数" key="size" dataIndex="size" />
|
||||||
<Column
|
<Column
|
||||||
title="操作"
|
title="操作"
|
||||||
render={(_, record) => {
|
render={(_, record: Team) => {
|
||||||
return (
|
return (
|
||||||
<Space>
|
<Space>
|
||||||
{record.id === editingId ? (
|
{record.id === editingId ? (
|
||||||
@ -145,14 +146,16 @@ export default function TeamManage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Table>
|
</Table>
|
||||||
|
{selectedTeam && (
|
||||||
<TeamDetailModal
|
<TeamDetailModal
|
||||||
open={open}
|
open={open}
|
||||||
team={selectedTeam}
|
team={selectedTeam}
|
||||||
onclose={() => {
|
onClose={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setSelectedTeam(null);
|
setSelectedTeam(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,22 +1,43 @@
|
|||||||
import { Form, Input, message, Modal, Select } from "antd";
|
import { Form, Input, message, Modal, Select } from "antd";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import axiosInstance from "../../api/axios";
|
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({
|
export default function UserDetailModal({
|
||||||
visiable,
|
visible,
|
||||||
mode = "create",
|
mode = "create",
|
||||||
user,
|
user,
|
||||||
roles,
|
roles,
|
||||||
onclose,
|
onClose,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}) {
|
}: UserDetailModalProps) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [initialValues, setInitialValues] = useState();
|
const [initialValues, setInitialValues] = useState<Partial<UserDTO>>({});
|
||||||
const [teams, setTeams] = useState([]);
|
const [teams, setTeams] = useState<{ label: string; value: string }[]>();
|
||||||
|
|
||||||
const fetchTeams = async () => {
|
const fetchTeams = async () => {
|
||||||
const data = await axiosInstance.get("/team-label");
|
const data = await axiosInstance.get<
|
||||||
|
unknown,
|
||||||
|
{ label: string; value: string }[]
|
||||||
|
>("/team-label");
|
||||||
setTeams(data);
|
setTeams(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,9 +46,9 @@ export default function UserDetailModal({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visiable) {
|
if (visible) {
|
||||||
if (mode === "edit") {
|
if (mode === "edit") {
|
||||||
const values = {
|
const values: UserDTO = {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
phone: user.phone,
|
phone: user.phone,
|
||||||
@ -38,24 +59,18 @@ export default function UserDetailModal({
|
|||||||
setInitialValues(values);
|
setInitialValues(values);
|
||||||
form.setFieldsValue(values);
|
form.setFieldsValue(values);
|
||||||
} else {
|
} else {
|
||||||
const values = {
|
const values: Partial<UserDTO> = {};
|
||||||
username: undefined,
|
|
||||||
password: undefined,
|
|
||||||
name: undefined,
|
|
||||||
phone: undefined,
|
|
||||||
teamId: undefined,
|
|
||||||
roleId: undefined,
|
|
||||||
};
|
|
||||||
setInitialValues(values);
|
setInitialValues(values);
|
||||||
form.setFieldsValue(values);
|
form.setFieldsValue(values);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [visiable, mode, user, form]);
|
}, [visible, mode, user, form]);
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
const data = {};
|
const data: Partial<UserDTO> = {};
|
||||||
Object.keys(initialValues).forEach((key) => {
|
Object.keys(initialValues).forEach((_key) => {
|
||||||
|
const key = _key as keyof UserDTO;
|
||||||
if (values[key] !== initialValues[key]) {
|
if (values[key] !== initialValues[key]) {
|
||||||
data[key] = values[key];
|
data[key] = values[key];
|
||||||
}
|
}
|
||||||
@ -71,14 +86,14 @@ export default function UserDetailModal({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onSuccess();
|
onSuccess();
|
||||||
onclose();
|
onClose();
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={mode === "edit" ? "编辑用户" : "添加用户"}
|
title={mode === "edit" ? "编辑用户" : "添加用户"}
|
||||||
open={visiable}
|
open={visible}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
onclose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
okText="保存"
|
okText="保存"
|
||||||
@ -2,29 +2,33 @@ import { Button, Flex, Input, Popconfirm, Space, Table } from "antd";
|
|||||||
import Column from "antd/es/table/Column";
|
import Column from "antd/es/table/Column";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import axiosInstance from "../../api/axios";
|
import axiosInstance from "../../api/axios";
|
||||||
import DeviceDetailModal from "../deviceAdmin/DeviceDetailModal";
|
|
||||||
import UserDetailModal from "./UserDetailModal";
|
import UserDetailModal from "./UserDetailModal";
|
||||||
|
import { PageResult, Pagination } from "types/common";
|
||||||
|
import { UserVo } from "types/model";
|
||||||
|
|
||||||
export default function UserManage() {
|
export default function UserManage() {
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState<UserVo[]>([]);
|
||||||
const [teams, setTeams] = useState([]);
|
// const [teams, setTeams] = useState([]);
|
||||||
const [modalMode, setModalMode] = useState();
|
const [modalMode, setModalMode] = useState<string>("create");
|
||||||
const [selectedUser, setSelectedUser] = useState();
|
const [selectedUser, setSelectedUser] = useState<UserVo | null>();
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [roles, setRoles] = useState([]);
|
const [roles, setRoles] = useState<{ label: string; value: string }[]>([]);
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState<Pagination>({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchRoles = async () => {
|
const fetchRoles = async () => {
|
||||||
const data = await axiosInstance.get("/role");
|
const data = await axiosInstance.get<
|
||||||
|
unknown,
|
||||||
|
{ label: string; value: string }[]
|
||||||
|
>("/role");
|
||||||
setRoles(data);
|
setRoles(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = async (pagination, name) => {
|
const fetchData = async (pagination: Pagination, name?: string) => {
|
||||||
const data = await axiosInstance.get("/user", {
|
const data = await axiosInstance.get<unknown, PageResult<UserVo>>("/user", {
|
||||||
params: {
|
params: {
|
||||||
page: pagination.current,
|
page: pagination.current,
|
||||||
size: pagination.pageSize,
|
size: pagination.pageSize,
|
||||||
@ -46,11 +50,11 @@ export default function UserManage() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handlePageChange = async (pagination) => {
|
const handlePageChange = async (pagination: Pagination) => {
|
||||||
await fetchData(pagination);
|
await fetchData(pagination);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = async (value) => {
|
const handleSearch = async (value: string) => {
|
||||||
await fetchData(
|
await fetchData(
|
||||||
{
|
{
|
||||||
...pagination,
|
...pagination,
|
||||||
@ -60,7 +64,7 @@ export default function UserManage() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (record) => {
|
const handleDelete = async (record: UserVo) => {
|
||||||
await axiosInstance.delete(`/user/${record.userId}`);
|
await axiosInstance.delete(`/user/${record.userId}`);
|
||||||
fetchData({
|
fetchData({
|
||||||
current: 1,
|
current: 1,
|
||||||
@ -108,7 +112,7 @@ export default function UserManage() {
|
|||||||
/>
|
/>
|
||||||
<Column
|
<Column
|
||||||
title="操作"
|
title="操作"
|
||||||
render={(_, record) => {
|
render={(_, record: UserVo) => {
|
||||||
return (
|
return (
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
@ -135,12 +139,13 @@ export default function UserManage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Table>
|
</Table>
|
||||||
|
{selectedUser && (
|
||||||
<UserDetailModal
|
<UserDetailModal
|
||||||
visiable={modalOpen}
|
visible={modalOpen}
|
||||||
mode={modalMode}
|
mode={modalMode}
|
||||||
user={selectedUser}
|
user={selectedUser}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
onclose={() => {
|
onClose={() => {
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
setSelectedUser(null);
|
setSelectedUser(null);
|
||||||
}}
|
}}
|
||||||
@ -151,6 +156,7 @@ export default function UserManage() {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -15,21 +15,34 @@ import { useSelector } from "react-redux";
|
|||||||
import axiosInstance, { baseURL } from "../../api/axios";
|
import axiosInstance, { baseURL } from "../../api/axios";
|
||||||
import { deviceStatusOptions } from "../../config/DeviceStatusConfig";
|
import { deviceStatusOptions } from "../../config/DeviceStatusConfig";
|
||||||
import { selectUserId } from "../../features/auth/authSlice";
|
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({
|
export default function DeviceDetailModal({
|
||||||
visiable,
|
visible,
|
||||||
mode = "create",
|
mode = "create",
|
||||||
device,
|
device,
|
||||||
onclose,
|
onClose,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}) {
|
}: DeviceDetailModalProps) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [imageFile, setImageFile] = useState(null);
|
const [imageFile, setImageFile] = useState<RcFile | null>(null);
|
||||||
const [initialValues, setInitialValues] = useState({});
|
const [initialValues, setInitialValues] = useState<Partial<DeviceAdminVO>>(
|
||||||
const [fileList, setFileList] = useState();
|
{}
|
||||||
|
);
|
||||||
|
const [fileList, setFileList] = useState<UploadFile[]>();
|
||||||
const userId = useSelector(selectUserId);
|
const userId = useSelector(selectUserId);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visiable) {
|
if (visible) {
|
||||||
setFileList([]);
|
setFileList([]);
|
||||||
if (mode === "edit") {
|
if (mode === "edit") {
|
||||||
const values = {
|
const values = {
|
||||||
@ -42,21 +55,22 @@ export default function DeviceDetailModal({
|
|||||||
setInitialValues(values);
|
setInitialValues(values);
|
||||||
} else {
|
} else {
|
||||||
const values = {
|
const values = {
|
||||||
name: undefined,
|
name: "",
|
||||||
location: undefined,
|
location: "",
|
||||||
usageRequirement: undefined,
|
usageRequirement: "",
|
||||||
status: undefined,
|
status: "",
|
||||||
};
|
};
|
||||||
form.setFieldsValue(values);
|
form.setFieldsValue(values);
|
||||||
setInitialValues(values);
|
setInitialValues(values);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [visiable, mode, device, form]);
|
}, [visible, mode, device, form]);
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
const data = {};
|
const data: Partial<DeviceAdminVO> = {};
|
||||||
Object.keys(initialValues).forEach((key) => {
|
Object.keys(initialValues).forEach((_key) => {
|
||||||
|
const key = _key as keyof DeviceAdminVO;
|
||||||
if (values[key] !== initialValues[key]) {
|
if (values[key] !== initialValues[key]) {
|
||||||
data[key] = values[key];
|
data[key] = values[key];
|
||||||
}
|
}
|
||||||
@ -83,15 +97,15 @@ export default function DeviceDetailModal({
|
|||||||
message.success("图片上传成功");
|
message.success("图片上传成功");
|
||||||
}
|
}
|
||||||
onSuccess();
|
onSuccess();
|
||||||
onclose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={mode === "edit" ? "编辑设备" : "添加设备"}
|
title={mode === "edit" ? "编辑设备" : "添加设备"}
|
||||||
open={visiable}
|
open={visible}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
onclose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
okText="保存"
|
okText="保存"
|
||||||
@ -9,51 +9,69 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import Column from "antd/es/table/Column";
|
import Column from "antd/es/table/Column";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import axiosInstance from "../../api/axios";
|
import axiosInstance from "../../api/axios";
|
||||||
import { deviceStatusOptions } from "../../config/DeviceStatusConfig";
|
import { deviceStatusOptions } from "../../config/DeviceStatusConfig";
|
||||||
import { selectUserId } from "../../features/auth/authSlice";
|
import { selectUserId } from "../../features/auth/authSlice";
|
||||||
import DeviceDetailModal from "./DeviceDetailModal";
|
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() {
|
export default function DeviceManage() {
|
||||||
const [devices, setDevices] = useState([]);
|
const [devices, setDevices] = useState<DeviceAdminVO[]>([]);
|
||||||
const [selectedDevice, setSelectedDevice] = useState(null);
|
const [selectedDevice, setSelectedDevice] = useState<DeviceAdminVO | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [modalMode, setModalMode] = useState(null);
|
const [modalMode, setModalMode] = useState("create");
|
||||||
const [searchName, setSearchName] = useState(null);
|
const [searchName, setSearchName] = useState<string>("");
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState<Pagination>({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const userId = useSelector(selectUserId);
|
const userId = useSelector(selectUserId);
|
||||||
const fetchData = async (pagination, name = searchName) => {
|
const fetchData = useCallback(
|
||||||
const data = await axiosInstance.get(`/device/${userId}`, {
|
async (pagination: Pagination, name: string = searchName) => {
|
||||||
|
const data = await axiosInstance.get<unknown, PageResult<DeviceAdminVO>>(
|
||||||
|
`/device/${userId}`,
|
||||||
|
{
|
||||||
params: {
|
params: {
|
||||||
page: pagination.current,
|
page: pagination.current,
|
||||||
size: pagination.pageSize,
|
size: pagination.pageSize,
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
setDevices(data.records);
|
setDevices(data.records);
|
||||||
setPagination({
|
setPagination({
|
||||||
...pagination,
|
...pagination,
|
||||||
total: data.total,
|
total: data.total,
|
||||||
});
|
});
|
||||||
};
|
},
|
||||||
|
[userId, searchName]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData(pagination);
|
fetchData(pagination);
|
||||||
}, []);
|
}, [fetchData, pagination]);
|
||||||
|
|
||||||
const handlePageChange = async (pagination) => {
|
const handlePageChange = async (pagination: Pagination) => {
|
||||||
await fetchData(pagination);
|
await fetchData(pagination);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (deviceId) => {
|
const handleDelete = async (deviceId: string) => {
|
||||||
await axiosInstance.delete(`/device/${deviceId}`);
|
await axiosInstance.delete(`/device/${deviceId}`);
|
||||||
message.success("删除成功");
|
message.success("删除成功");
|
||||||
const newPagination = {
|
const newPagination = {
|
||||||
@ -64,7 +82,7 @@ export default function DeviceManage() {
|
|||||||
await fetchData(newPagination);
|
await fetchData(newPagination);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = async (value) => {
|
const handleSearch = async (value: string) => {
|
||||||
setSearchName(value);
|
setSearchName(value);
|
||||||
const newPagination = {
|
const newPagination = {
|
||||||
...pagination,
|
...pagination,
|
||||||
@ -128,7 +146,7 @@ export default function DeviceManage() {
|
|||||||
/>
|
/>
|
||||||
<Column
|
<Column
|
||||||
title="操作"
|
title="操作"
|
||||||
render={(_, record) => {
|
render={(_, record: DeviceAdminVO) => {
|
||||||
return (
|
return (
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
@ -155,11 +173,12 @@ export default function DeviceManage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Table>
|
</Table>
|
||||||
|
{selectedDevice && (
|
||||||
<DeviceDetailModal
|
<DeviceDetailModal
|
||||||
visiable={modalOpen}
|
visible={modalOpen}
|
||||||
device={selectedDevice}
|
device={selectedDevice}
|
||||||
mode={modalMode}
|
mode={modalMode}
|
||||||
onclose={() => {
|
onClose={() => {
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
setSelectedDevice(null);
|
setSelectedDevice(null);
|
||||||
}}
|
}}
|
||||||
@ -170,6 +189,7 @@ export default function DeviceManage() {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,14 +1,26 @@
|
|||||||
import { Button, message, Space, Table } from "antd";
|
import { Button, message, Space, Table } from "antd";
|
||||||
import Column from "antd/es/table/Column";
|
import Column from "antd/es/table/Column";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import axiosInstance from "../../api/axios";
|
import axiosInstance from "../../api/axios";
|
||||||
import { selectUserId } from "../../features/auth/authSlice";
|
import { selectUserId } from "../../features/auth/authSlice";
|
||||||
import { selectUserRole } 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() {
|
export default function Approval() {
|
||||||
const [reservations, setReservations] = useState([]);
|
const [reservations, setReservations] = useState<ReservationVO[]>([]);
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState<Pagination>({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
@ -21,30 +33,40 @@ export default function Approval() {
|
|||||||
showNeedAssist = true;
|
showNeedAssist = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchData = async (pagination) => {
|
const fetchData = useCallback(
|
||||||
const data = await axiosInstance.get(`/reservation/approval/${userId}`, {
|
async (pagination: Pagination) => {
|
||||||
|
const data = await axiosInstance.get<unknown, PageResult<ReservationVO>>(
|
||||||
|
`/reservation/approval/${userId}`,
|
||||||
|
{
|
||||||
params: {
|
params: {
|
||||||
page: pagination.current,
|
page: pagination.current,
|
||||||
size: pagination.pageSize,
|
size: pagination.pageSize,
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
setReservations(data.records);
|
setReservations(data.records);
|
||||||
setPagination({
|
setPagination({
|
||||||
...pagination,
|
...pagination,
|
||||||
total: data.total,
|
total: data.total,
|
||||||
});
|
});
|
||||||
};
|
},
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData(pagination);
|
fetchData(pagination);
|
||||||
}, []);
|
}, [fetchData, pagination]);
|
||||||
|
|
||||||
const handlePageChange = async (pagination) => {
|
const handlePageChange = async (pagination: Pagination) => {
|
||||||
await fetchData(pagination);
|
await fetchData(pagination);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApproval = async (reservationId, isApprove, needAssist) => {
|
const handleApproval = async (
|
||||||
|
reservationId: string,
|
||||||
|
isApprove: boolean,
|
||||||
|
needAssist: boolean
|
||||||
|
) => {
|
||||||
await axiosInstance.post("/approval", {
|
await axiosInstance.post("/approval", {
|
||||||
userId,
|
userId,
|
||||||
reservationId,
|
reservationId,
|
||||||
@ -1,48 +1,71 @@
|
|||||||
import { Button, DatePicker, Form, Input, message, Table, Tag } from "antd";
|
import { Button, DatePicker, Form, Input, message, Table, Tag } from "antd";
|
||||||
import { useForm } from "antd/es/form/Form";
|
import { useForm } from "antd/es/form/Form";
|
||||||
import Column from "antd/es/table/Column";
|
import Column from "antd/es/table/Column";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import axiosInstance from "../../api/axios";
|
import axiosInstance from "../../api/axios";
|
||||||
import { selectUserId, selectUserRole } from "../../features/auth/authSlice";
|
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() {
|
export default function MyApproval() {
|
||||||
const [approvals, setApprovals] = useState([]);
|
const [approvals, setApprovals] = useState<ApprovalVO[]>([]);
|
||||||
const [form] = useForm();
|
const [form] = useForm();
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState<Pagination>({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
const [editingRow, setEditingRow] = useState(null);
|
const [editingRow, setEditingRow] = useState<string | null>(null);
|
||||||
const [tempEndTime, setTempEndTime] = useState(null);
|
const [tempEndTime, setTempEndTime] = useState<Dayjs | null>(null);
|
||||||
|
|
||||||
const userId = useSelector(selectUserId);
|
const userId = useSelector(selectUserId);
|
||||||
const userRole = useSelector(selectUserRole);
|
const userRole = useSelector(selectUserRole);
|
||||||
|
|
||||||
const fetchData = async (pagination, searchParam) => {
|
const fetchData = useCallback(
|
||||||
const data = await axiosInstance.get(`/approval/${userId}`, {
|
async (
|
||||||
|
pagination: Pagination,
|
||||||
|
searchParam?: { applicantName: string; deviceName: string }
|
||||||
|
) => {
|
||||||
|
const data = await axiosInstance.get<unknown, PageResult<ApprovalVO>>(
|
||||||
|
`/approval/${userId}`,
|
||||||
|
{
|
||||||
params: {
|
params: {
|
||||||
page: pagination.current,
|
page: pagination.current,
|
||||||
size: pagination.pageSize,
|
size: pagination.pageSize,
|
||||||
applicantName: searchParam?.applicantName,
|
applicantName: searchParam?.applicantName,
|
||||||
deviceName: searchParam?.deviceName,
|
deviceName: searchParam?.deviceName,
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
setApprovals(data.records);
|
setApprovals(data.records);
|
||||||
setPagination({
|
setPagination({
|
||||||
...pagination,
|
...pagination,
|
||||||
total: data.total,
|
total: data.total,
|
||||||
});
|
});
|
||||||
};
|
},
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData(pagination);
|
fetchData(pagination);
|
||||||
}, []);
|
}, [fetchData, pagination]);
|
||||||
|
|
||||||
const handlePageChange = async (pagination) => {
|
const handlePageChange = async (pagination: Pagination) => {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
fetchData(pagination, values);
|
fetchData(pagination, values);
|
||||||
};
|
};
|
||||||
@ -57,7 +80,7 @@ export default function MyApproval() {
|
|||||||
await fetchData(newPagination, values);
|
await fetchData(newPagination, values);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (record) => {
|
const handleSubmit = async (record: ApprovalVO) => {
|
||||||
try {
|
try {
|
||||||
await axiosInstance.post(`/reservation/endTime/${record.reservationId}`, {
|
await axiosInstance.post(`/reservation/endTime/${record.reservationId}`, {
|
||||||
endTime: dayjs(tempEndTime).format("YYYY-MM-DD"),
|
endTime: dayjs(tempEndTime).format("YYYY-MM-DD"),
|
||||||
@ -66,7 +89,7 @@ export default function MyApproval() {
|
|||||||
await fetchData(pagination, values);
|
await fetchData(pagination, values);
|
||||||
setEditingRow(null);
|
setEditingRow(null);
|
||||||
message.success("修改成功");
|
message.success("修改成功");
|
||||||
} catch (error) {
|
} catch {
|
||||||
message.error("修改失败");
|
message.error("修改失败");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -109,7 +132,7 @@ export default function MyApproval() {
|
|||||||
<Column
|
<Column
|
||||||
title="结束时间"
|
title="结束时间"
|
||||||
key="endTime"
|
key="endTime"
|
||||||
render={(_, record) => {
|
render={(_, record: ApprovalVO) => {
|
||||||
const isEditable =
|
const isEditable =
|
||||||
record.decision === 1 && userRole.includes("DEVICE_ADMIN");
|
record.decision === 1 && userRole.includes("DEVICE_ADMIN");
|
||||||
|
|
||||||
@ -1,50 +1,67 @@
|
|||||||
import { Button, Col, Form, Input, message, Row } from "antd";
|
import { Button, Col, Form, Input, message, Row } from "antd";
|
||||||
import { useForm } from "antd/es/form/Form";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import axiosInstance from "../../api/axios";
|
import axiosInstance from "../../api/axios";
|
||||||
import { selectUserId } from "../../features/auth/authSlice";
|
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() {
|
export default function UserDetail() {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [user, setUser] = useState({
|
const [user, setUser] = useState<Partial<UserVo>>({});
|
||||||
userId: "",
|
|
||||||
username: "",
|
|
||||||
team: "",
|
|
||||||
name: "",
|
|
||||||
phone: "",
|
|
||||||
});
|
|
||||||
const userId = useSelector(selectUserId);
|
const userId = useSelector(selectUserId);
|
||||||
|
|
||||||
const fetchUser = async (userId) => {
|
const fetchUser = useCallback(
|
||||||
const user = await axiosInstance.get(`/userdetail/${userId}`);
|
async (userId: string | null) => {
|
||||||
|
if (!userId) return;
|
||||||
|
const user = await axiosInstance.get<unknown, UserVo>(
|
||||||
|
`/userdetail/${userId}`
|
||||||
|
);
|
||||||
setUser(user);
|
setUser(user);
|
||||||
form.setFieldsValue(user);
|
form.setFieldsValue(user);
|
||||||
};
|
},
|
||||||
|
[form]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUser(userId);
|
fetchUser(userId);
|
||||||
}, []);
|
}, [fetchUser, userId]);
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (values) => {
|
const handleSubmit = async (values: UserForm) => {
|
||||||
if (values.password && values.password !== values.confirmPassword) {
|
if (values.password && values.password !== values.confirmPassword) {
|
||||||
message.error("两次输入的密码不一致");
|
message.error("两次输入的密码不一致");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const changedFields = {};
|
type FormKey = keyof UserForm;
|
||||||
for (const key in values) {
|
const changedFields: Record<string, string> = {};
|
||||||
if (values[key] !== user[key] && values[key] !== undefined) {
|
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];
|
changedFields[key] = values[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete changedFields.confirmPassword;
|
delete changedFields.confirmPassword;
|
||||||
|
|
||||||
const newUser = await axiosInstance.put(`/user/${userId}`, changedFields);
|
const newUser = await axiosInstance.put<unknown, UserVo>(
|
||||||
|
`/user/${userId}`,
|
||||||
|
changedFields
|
||||||
|
);
|
||||||
setUser(newUser);
|
setUser(newUser);
|
||||||
form.setFieldsValue(newUser);
|
form.setFieldsValue(newUser);
|
||||||
message.success("修改成功");
|
message.success("修改成功");
|
||||||
@ -10,21 +10,45 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import dayjs from "dayjs";
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
import isBetween from "dayjs/plugin/isBetween";
|
import isBetween from "dayjs/plugin/isBetween";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import axiosInstance, { baseURL } from "../../api/axios";
|
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 }) {
|
interface FormValue {
|
||||||
const [unavailableTimes, setUnavailableTims] = useState([]);
|
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 [form] = Form.useForm();
|
||||||
const userId = useSelector((state) => state.auth.userId);
|
const userId = useSelector(selectUserId);
|
||||||
const [teams, setTeams] = useState([]);
|
const [teams, setTeams] = useState<{ label: string; value: string }[]>([]);
|
||||||
const [initialValues, setInitialValues] = useState();
|
const [initialValues, setInitialValues] = useState<FormValue>();
|
||||||
|
|
||||||
const fetchTeams = async () => {
|
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) => ({
|
const teams = data.map((item) => ({
|
||||||
label: item.label,
|
label: item.label,
|
||||||
value: item.label,
|
value: item.label,
|
||||||
@ -32,8 +56,10 @@ export default function DeviceDetailModal({ visiable, device, onclose }) {
|
|||||||
setTeams(teams);
|
setTeams(teams);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchUser = async () => {
|
const fetchUser = useCallback(async () => {
|
||||||
const data = await axiosInstance.get(`/userdetail/${userId}`);
|
const data = await axiosInstance.get<unknown, UserVo>(
|
||||||
|
`/userdetail/${userId}`
|
||||||
|
);
|
||||||
const values = {
|
const values = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
phone: data.phone,
|
phone: data.phone,
|
||||||
@ -41,28 +67,34 @@ export default function DeviceDetailModal({ visiable, device, onclose }) {
|
|||||||
};
|
};
|
||||||
setInitialValues(values);
|
setInitialValues(values);
|
||||||
form.setFieldsValue(values);
|
form.setFieldsValue(values);
|
||||||
};
|
}, [userId, form]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTeams();
|
fetchTeams();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUnavailableTimes = async (id) => {
|
const fetchUnavailableTimes = async (id: string) => {
|
||||||
const data = await axiosInstance.get(`/device/unavailable-times/${id}`);
|
const data = await axiosInstance.get<
|
||||||
setUnavailableTims(data);
|
unknown,
|
||||||
|
{ startTime: Date; endTime: Date }[]
|
||||||
|
>(`/device/unavailable-times/${id}`);
|
||||||
|
setUnavailableTimes(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (visiable && device?.deviceId) {
|
if (visible && device?.deviceId) {
|
||||||
fetchUnavailableTimes(device.deviceId);
|
fetchUnavailableTimes(device.deviceId);
|
||||||
fetchUser();
|
fetchUser();
|
||||||
}
|
}
|
||||||
}, [visiable, device?.deviceId]);
|
}, [visible, device?.deviceId, fetchUser]);
|
||||||
|
|
||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
const disabledDate = (current, { from } = {}) => {
|
const disabledDate = (
|
||||||
|
current: Dayjs,
|
||||||
|
info: { from?: Dayjs } = {}
|
||||||
|
): boolean => {
|
||||||
if (!current) return false;
|
if (!current) return false;
|
||||||
|
const { from } = info;
|
||||||
const today = dayjs().startOf("day");
|
const today = dayjs().startOf("day");
|
||||||
const currentDay = current.startOf("day");
|
const currentDay = current.startOf("day");
|
||||||
|
|
||||||
@ -78,7 +110,7 @@ export default function DeviceDetailModal({ visiable, device, onclose }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 限制选择范围为 7 天内(从 from 开始算起)
|
// 限制选择范围为 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;
|
return isPastDate || isUnavailable || isExceedingRange;
|
||||||
};
|
};
|
||||||
@ -101,15 +133,15 @@ export default function DeviceDetailModal({ visiable, device, onclose }) {
|
|||||||
await axiosInstance.post("/reservation", payload);
|
await axiosInstance.post("/reservation", payload);
|
||||||
message.success("预约成功");
|
message.success("预约成功");
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
onclose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="预约设备"
|
title="预约设备"
|
||||||
open={visiable}
|
open={visible}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
onclose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
onOk={handleOK}
|
onOk={handleOK}
|
||||||
>
|
>
|
||||||
@ -1,14 +1,28 @@
|
|||||||
import { Table, Tag } from "antd";
|
import { Table, Tag } from "antd";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import axiosInstance from "../../api/axios";
|
import axiosInstance from "../../api/axios";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import Column from "antd/es/table/Column";
|
import Column from "antd/es/table/Column";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { selectUserId } from "../../features/auth/authSlice";
|
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() {
|
export default function MyReservation() {
|
||||||
const [reservations, setReservations] = useState([]);
|
const [reservations, setReservations] = useState<UserReservationVO[]>([]);
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState<Pagination>({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
@ -16,8 +30,12 @@ export default function MyReservation() {
|
|||||||
|
|
||||||
const userId = useSelector(selectUserId);
|
const userId = useSelector(selectUserId);
|
||||||
|
|
||||||
const fetchData = async (pagination) => {
|
const fetchData = useCallback(
|
||||||
const data = await axiosInstance.get(`/reservation/${userId}`, {
|
async (pagination: Pagination) => {
|
||||||
|
const data = await axiosInstance.get<
|
||||||
|
unknown,
|
||||||
|
PageResult<UserReservationVO>
|
||||||
|
>(`/reservation/${userId}`, {
|
||||||
params: {
|
params: {
|
||||||
page: pagination.current,
|
page: pagination.current,
|
||||||
size: pagination.pageSize,
|
size: pagination.pageSize,
|
||||||
@ -28,13 +46,15 @@ export default function MyReservation() {
|
|||||||
...pagination,
|
...pagination,
|
||||||
total: data.total,
|
total: data.total,
|
||||||
});
|
});
|
||||||
};
|
},
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData(pagination);
|
fetchData(pagination);
|
||||||
}, []);
|
}, [fetchData, pagination]);
|
||||||
|
|
||||||
const handlePageChange = (pagination) => {
|
const handlePageChange = (pagination: Pagination) => {
|
||||||
fetchData(pagination);
|
fetchData(pagination);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import { Input, Space, Table, Tag } from "antd";
|
import { Input, Space, Table, Tag } from "antd";
|
||||||
import Column from "antd/es/table/Column";
|
import Column from "antd/es/table/Column";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import axiosInstance from "../../api/axios";
|
import axiosInstance from "../../api/axios";
|
||||||
import DeviceDetailModal from "./DeviceDetailModal";
|
import DeviceDetailModal from "./DeviceDetailModal";
|
||||||
|
import { PageResult, Pagination } from "types/common";
|
||||||
|
|
||||||
const statusColorMap = {
|
const statusColorMap = {
|
||||||
空闲: "green",
|
空闲: "green",
|
||||||
@ -11,41 +12,57 @@ const statusColorMap = {
|
|||||||
维修中: "gray",
|
维修中: "gray",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface DeviceVO {
|
||||||
|
deviceId: string;
|
||||||
|
name: string;
|
||||||
|
usageRequirement: string;
|
||||||
|
location: string;
|
||||||
|
imagePath: string;
|
||||||
|
state: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Reserve() {
|
export default function Reserve() {
|
||||||
const [name, setName] = useState(null);
|
const [name, setName] = useState<string | null>(null);
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState<Pagination>({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
const [devices, setDevices] = useState([]);
|
const [devices, setDevices] = useState<DeviceVO[]>([]);
|
||||||
|
|
||||||
const fetchData = async (pagination, searchName = name) => {
|
const fetchData = useCallback(
|
||||||
const data = await axiosInstance.get("/device", {
|
async (pagination: Pagination, searchName = name) => {
|
||||||
|
const data = await axiosInstance.get<unknown, PageResult<DeviceVO>>(
|
||||||
|
"/device",
|
||||||
|
{
|
||||||
params: {
|
params: {
|
||||||
page: pagination.current,
|
page: pagination.current,
|
||||||
size: pagination.pageSize,
|
size: pagination.pageSize,
|
||||||
name: searchName,
|
name: searchName,
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
setDevices(data.records);
|
setDevices(data.records);
|
||||||
setPagination({
|
setPagination({
|
||||||
...pagination,
|
...pagination,
|
||||||
total: data.total,
|
total: data.total,
|
||||||
});
|
});
|
||||||
};
|
},
|
||||||
|
[name]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData(pagination);
|
fetchData(pagination);
|
||||||
}, []);
|
}, [fetchData, pagination]);
|
||||||
|
|
||||||
const handlePageChange = (pagination) => {
|
const handlePageChange = (pagination: Pagination) => {
|
||||||
fetchData(pagination);
|
fetchData(pagination);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [selectedDevice, setSelectedDevice] = useState(null);
|
const [selectedDevice, setSelectedDevice] = useState<DeviceVO | null>(null);
|
||||||
|
|
||||||
const handleSearch = (value) => {
|
const handleSearch = (value: string) => {
|
||||||
setName(value);
|
setName(value);
|
||||||
const newPagination = {
|
const newPagination = {
|
||||||
...pagination,
|
...pagination,
|
||||||
@ -74,7 +91,7 @@ export default function Reserve() {
|
|||||||
title="使用要求"
|
title="使用要求"
|
||||||
key="usageRequirement"
|
key="usageRequirement"
|
||||||
dataIndex="usageRequirement"
|
dataIndex="usageRequirement"
|
||||||
ellipsis="true"
|
ellipsis={true}
|
||||||
/>
|
/>
|
||||||
<Column title="位置" key="location" dataIndex="location" />
|
<Column title="位置" key="location" dataIndex="location" />
|
||||||
<Column
|
<Column
|
||||||
@ -83,7 +100,10 @@ export default function Reserve() {
|
|||||||
dataIndex="state"
|
dataIndex="state"
|
||||||
render={(_, { state }) => (
|
render={(_, { state }) => (
|
||||||
<>
|
<>
|
||||||
<Tag color={statusColorMap[state]} key="state">
|
<Tag
|
||||||
|
color={statusColorMap[state as keyof typeof statusColorMap]}
|
||||||
|
key="state"
|
||||||
|
>
|
||||||
{state}
|
{state}
|
||||||
</Tag>
|
</Tag>
|
||||||
</>
|
</>
|
||||||
@ -92,18 +112,21 @@ export default function Reserve() {
|
|||||||
<Column
|
<Column
|
||||||
title="操作"
|
title="操作"
|
||||||
key="action"
|
key="action"
|
||||||
render={(_, record) => (
|
render={(_, record: DeviceVO) => (
|
||||||
<Space size="middle">
|
<Space size="middle">
|
||||||
<a onClick={() => setSelectedDevice(record)}>预约</a>
|
<a onClick={() => setSelectedDevice(record)}>预约</a>
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
{selectedDevice && (
|
||||||
<DeviceDetailModal
|
<DeviceDetailModal
|
||||||
visiable={!!selectedDevice}
|
visible={!!selectedDevice}
|
||||||
device={selectedDevice}
|
device={selectedDevice}
|
||||||
onclose={() => setSelectedDevice(null)}
|
onClose={() => setSelectedDevice(null)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2,7 +2,11 @@ import { useSelector } from "react-redux";
|
|||||||
import { Navigate, Outlet } from "react-router-dom";
|
import { Navigate, Outlet } from "react-router-dom";
|
||||||
import { selectUserRole } from "../features/auth/authSlice";
|
import { selectUserRole } from "../features/auth/authSlice";
|
||||||
|
|
||||||
export default function ProtectedRoute({ allowedRoles }) {
|
export default function ProtectedRoute({
|
||||||
|
allowedRoles,
|
||||||
|
}: {
|
||||||
|
allowedRoles: string[];
|
||||||
|
}) {
|
||||||
const roles = useSelector(selectUserRole);
|
const roles = useSelector(selectUserRole);
|
||||||
|
|
||||||
if (roles.length === 0) return <Navigate to="/login" replace />;
|
if (roles.length === 0) return <Navigate to="/login" replace />;
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
import authReducer from "../features/auth/authSlice";
|
import authReducer from "../features/auth/authSlice";
|
||||||
|
import { TypedUseSelectorHook, useSelector } from "react-redux";
|
||||||
|
|
||||||
const userId = localStorage.getItem("userId");
|
const userId = localStorage.getItem("userId");
|
||||||
const name = localStorage.getItem("name");
|
const name = localStorage.getItem("name");
|
||||||
@ -21,3 +22,7 @@ export const store = configureStore({
|
|||||||
},
|
},
|
||||||
preloadedState,
|
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/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
|
server: {
|
||||||
|
host: "0.0.0.0",
|
||||||
|
port: 5173,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user