Compare commits

...

4 Commits

41 changed files with 3105 additions and 465 deletions

View File

@ -1,33 +1,41 @@
import js from '@eslint/js' import 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

File diff suppressed because it is too large Load Diff

View File

@ -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"
} }
} }

View File

@ -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 {

View File

@ -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")],

View File

@ -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: "设备预约",

View File

@ -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;

View File

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

View File

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

View File

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

View File

@ -5,12 +5,13 @@ import Sider from "antd/es/layout/Sider";
import React, { useState } from "react"; import 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,
}; };

View File

@ -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}>

View File

@ -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);

View File

@ -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"

View File

@ -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[]>(
params: { "/device/usage-stats",
start: range[0].format("YYYY-MM-DD"), {
end: range[1].format("YYYY-MM-DD"), params: {
}, start: range[0].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,19 +162,21 @@ 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}
/> />
<DeviceDetailStatsModal {selectedRecord && (
visible={visiable} <DeviceDetailStatsModal
record={selectedRecord} visible={visiable}
onClose={() => { record={selectedRecord}
setVisiable(false); onClose={() => {
}} setVisiable(false);
range={range} }}
/> range={range}
/>
)}
</Spin> </Spin>
); );
} }

View File

@ -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[]>(
params: { "/reservation/stats",
start: range[0].format("YYYY-MM-DD"), {
end: range[1].format("YYYY-MM-DD"), params: {
}, start: range[0].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}
/> />

View File

@ -1,33 +0,0 @@
import { List, Modal, Typography } from "antd";
import { useEffect, useState } from "react";
import axiosInstance from "../../api/axios";
export default function TeamDetailModal({ open, team, onclose }) {
const [data, setData] = useState([]);
const fetchData = async () => {
const data = await axiosInstance.get(`/user-team/${team.id}`);
setData(data);
};
useEffect(() => {
if (open) {
fetchData();
}
}, [open, team]);
return (
<Modal
open={open}
onCancel={() => {
onclose();
}}
>
<List
header={<div>{team?.name}</div>}
dataSource={data}
renderItem={(item) => <List.Item>{item}</List.Item>}
/>
</Modal>
);
}

View File

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

View File

@ -4,29 +4,30 @@ import { useEffect, useState } from "react";
import axiosInstance from "../../api/axios"; import 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>
<TeamDetailModal {selectedTeam && (
open={open} <TeamDetailModal
team={selectedTeam} open={open}
onclose={() => { team={selectedTeam}
setOpen(false); onClose={() => {
setSelectedTeam(null); setOpen(false);
}} setSelectedTeam(null);
/> }}
/>
)}
</> </>
); );
} }

View File

@ -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="保存"

View File

@ -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,22 +139,24 @@ export default function UserManage() {
}} }}
/> />
</Table> </Table>
<UserDetailModal {selectedUser && (
visiable={modalOpen} <UserDetailModal
mode={modalMode} visible={modalOpen}
user={selectedUser} mode={modalMode}
roles={roles} user={selectedUser}
onclose={() => { roles={roles}
setModalOpen(false); onClose={() => {
setSelectedUser(null); setModalOpen(false);
}} setSelectedUser(null);
onSuccess={async () => { }}
await fetchData({ onSuccess={async () => {
...pagination, await fetchData({
current: 1, ...pagination,
}); current: 1,
}} });
/> }}
/>
)}
</> </>
); );
} }

View File

@ -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="保存"

View File

@ -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) => {
params: { const data = await axiosInstance.get<unknown, PageResult<DeviceAdminVO>>(
page: pagination.current, `/device/${userId}`,
size: pagination.pageSize, {
name, params: {
}, page: pagination.current,
}); size: pagination.pageSize,
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,21 +173,23 @@ export default function DeviceManage() {
}} }}
/> />
</Table> </Table>
<DeviceDetailModal {selectedDevice && (
visiable={modalOpen} <DeviceDetailModal
device={selectedDevice} visible={modalOpen}
mode={modalMode} device={selectedDevice}
onclose={() => { mode={modalMode}
setModalOpen(false); onClose={() => {
setSelectedDevice(null); setModalOpen(false);
}} setSelectedDevice(null);
onSuccess={async () => { }}
await fetchData({ onSuccess={async () => {
...pagination, await fetchData({
current: 1, ...pagination,
}); current: 1,
}} });
/> }}
/>
)}
</> </>
); );
} }

View File

@ -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) => {
params: { const data = await axiosInstance.get<unknown, PageResult<ReservationVO>>(
page: pagination.current, `/reservation/approval/${userId}`,
size: pagination.pageSize, {
}, params: {
}); page: pagination.current,
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,

View File

@ -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 (
params: { pagination: Pagination,
page: pagination.current, searchParam?: { applicantName: string; deviceName: string }
size: pagination.pageSize, ) => {
applicantName: searchParam?.applicantName, const data = await axiosInstance.get<unknown, PageResult<ApprovalVO>>(
deviceName: searchParam?.deviceName, `/approval/${userId}`,
}, {
}); params: {
page: pagination.current,
size: pagination.pageSize,
applicantName: searchParam?.applicantName,
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");

View File

@ -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) => {
setUser(user); if (!userId) return;
form.setFieldsValue(user); const user = await axiosInstance.get<unknown, UserVo>(
}; `/userdetail/${userId}`
);
setUser(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("修改成功");

View File

@ -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}
> >

View File

@ -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,25 +30,31 @@ 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) => {
params: { const data = await axiosInstance.get<
page: pagination.current, unknown,
size: pagination.pageSize, PageResult<UserReservationVO>
}, >(`/reservation/${userId}`, {
}); params: {
setReservations(data.records); page: pagination.current,
setPagination({ size: pagination.pageSize,
...pagination, },
total: data.total, });
}); setReservations(data.records);
}; setPagination({
...pagination,
total: data.total,
});
},
[userId]
);
useEffect(() => { useEffect(() => {
fetchData(pagination); fetchData(pagination);
}, []); }, [fetchData, pagination]);
const handlePageChange = (pagination) => { const handlePageChange = (pagination: Pagination) => {
fetchData(pagination); fetchData(pagination);
}; };

View File

@ -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) => {
params: { const data = await axiosInstance.get<unknown, PageResult<DeviceVO>>(
page: pagination.current, "/device",
size: pagination.pageSize, {
name: searchName, params: {
}, page: pagination.current,
}); size: pagination.pageSize,
name: searchName,
},
}
);
setDevices(data.records);
setPagination({
...pagination,
total: data.total,
});
},
[name]
);
setDevices(data.records);
setPagination({
...pagination,
total: data.total,
});
};
useEffect(() => { 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>
<DeviceDetailModal
visiable={!!selectedDevice} {selectedDevice && (
device={selectedDevice} <DeviceDetailModal
onclose={() => setSelectedDevice(null)} visible={!!selectedDevice}
/> device={selectedDevice}
onClose={() => setSelectedDevice(null)}
/>
)}
</> </>
); );
} }

View File

@ -2,7 +2,11 @@ import { useSelector } from "react-redux";
import { Navigate, Outlet } from "react-router-dom"; import { 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 />;

View File

@ -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
View File

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

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

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

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

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

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

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

115
tsconfig.json Normal file
View File

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

View File

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