Compare commits

..

No commits in common. "ts-migration" and "main" have entirely different histories.

41 changed files with 465 additions and 3105 deletions

View File

@ -1,41 +1,33 @@
import js from "@eslint/js"; import js from '@eslint/js'
import globals from "globals"; import globals from 'globals'
import tseslint from "typescript-eslint"; import reactHooks from 'eslint-plugin-react-hooks'
import react from "eslint-plugin-react"; import reactRefresh from 'eslint-plugin-react-refresh'
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import prettier from "eslint-config-prettier";
export default [ export default [
js.configs.recommended, { ignores: ['dist'] },
...tseslint.configs.recommended,
{ ignores: ["dist"] },
{ {
files: ["**/*.{js,jsx,tsx}"], files: ['**/*.{js,jsx}'],
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, 'react-hooks': reactHooks,
"react-hooks": reactHooks, 'react-refresh': reactRefresh,
"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,19 +24,13 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.25.0", "@eslint/js": "^9.25.0",
"@types/react": "^19.1.12", "@types/react": "^19.1.2",
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.1.2",
"@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.34.0", "eslint": "^9.25.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,19 +1,10 @@
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";
interface TeamDeleteButtonProps { export default function TeamDeleteButton({ record, onConfirm }) {
record: Team;
onConfirm: () => void;
}
export default function TeamDeleteButton({
record,
onConfirm,
}: TeamDeleteButtonProps) {
const [visiable, setVisiable] = useState(false); const [visiable, setVisiable] = useState(false);
const handleClick = (record: Team) => { const handleClick = (record) => {
if (record.size > 0) { if (record.size > 0) {
message.warning("该团队下还有成员,无法删除"); message.warning("该团队下还有成员,无法删除");
} else { } else {

View File

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

View File

@ -10,16 +10,7 @@ import {
UserOutlined, UserOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
export interface MenuItem { const menuConfig = [
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,29 +1,19 @@
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 = ""; state.userId = null;
state.name = ""; state.name = null;
state.token = ""; state.token = null;
state.roles = []; state.roles = [];
localStorage.removeItem("userId"); localStorage.removeItem("userId");
@ -40,7 +30,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.toString()); localStorage.setItem("userId", payload.userId);
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);
@ -49,7 +39,7 @@ const authSlice = createSlice({
}); });
export const { logout } = authSlice.actions; export const { logout } = authSlice.actions;
export const selectUserRole = (state: RootState) => state.auth.roles; export const selectUserRole = (state) => state.auth.roles;
export const selectUserName = (state: RootState) => state.auth.name; export const selectUserName = (state) => state.auth.name;
export const selectUserId = (state: RootState) => state.auth.userId; export const selectUserId = (state) => state.auth.userId;
export default authSlice.reducer; export default authSlice.reducer;

View File

@ -0,0 +1,11 @@
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

@ -1,15 +0,0 @@
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

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

View File

@ -5,13 +5,12 @@ 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, { MenuItem } from "../config/menuConfig"; import menuConfig 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();
@ -37,7 +36,7 @@ export default function CommonLayout() {
}, },
]; ];
const handleMenuClick = ({ key }: { key: string }) => { const handleMenuClick = ({ key }) => {
if (key === "logout") { if (key === "logout") {
dispatch(logout()); dispatch(logout());
message.success("已退出登录"); message.success("已退出登录");
@ -46,7 +45,7 @@ export default function CommonLayout() {
}; };
// //
const sortMenu = (menu: MenuItem[]): MenuItem[] => { const sortMenu = (menu) => {
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) => ({
@ -55,8 +54,8 @@ export default function CommonLayout() {
})); }));
}; };
const buildMenuItems = (menu: MenuItem[]): ItemType<MenuItemType>[] => const buildMenuItems = (menu) =>
sortMenu(menu).map((item: MenuItem): ItemType<MenuItemType> => { sortMenu(menu).map((item) => {
if (item.children) { if (item.children) {
return { return {
key: item.label, // item.path key: item.label, // item.path
@ -66,7 +65,7 @@ export default function CommonLayout() {
}; };
} else { } else {
return { return {
key: item.path ?? item.label, key: item.path,
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.js"; import router from "./router/index.jsx";
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,18 +4,14 @@ 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<typeof store.dispatch>(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const onFinish = async (values: LoginForm) => { const onFinish = async (values) => {
const res = await dispatch(login(values)).unwrap(); const res = await dispatch(login(values)).unwrap();
const path = res.roles const path = res.roles.map((r) => roleRoute[r]).find(Boolean);
.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,40 +1,20 @@
import { message, Modal, Spin, Table } from "antd"; import { message, Modal, Space, Spin, Table } from "antd";
import { useCallback, useEffect, useState } from "react"; import { 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<DeviceDetailStats[]>([]); const [data, setData] = useState([]);
const fetchData = useCallback(async () => { const fetchData = async () => {
setLoading(true); setLoading(true);
try { try {
const res = await axiosInstance.get< const res = await axiosInstance.get("/device/detail-stats", {
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"),
@ -42,12 +22,12 @@ export default function DeviceDetailStatsModal({
}, },
}); });
setData(res); setData(res);
} catch { } catch (e) {
message.error("获取数据失败"); message.error("获取数据失败");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [record, range]); };
const handleExport = async () => { const handleExport = async () => {
setLoading(true); setLoading(true);
@ -93,9 +73,9 @@ export default function DeviceDetailStatsModal({
if (visible) { if (visible) {
fetchData(); fetchData();
} }
}, [visible, fetchData]); }, [visible, record]);
const columns: ColumnsType<DeviceDetailStats> = [ const columns = [
{ {
title: "使用人", title: "使用人",
dataIndex: "applicantName", dataIndex: "applicantName",
@ -137,8 +117,8 @@ export default function DeviceDetailStatsModal({
okText="导出Excel" okText="导出Excel"
onOk={handleExport} onOk={handleExport}
> >
<Table<DeviceDetailStats> <Table
// 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,55 +1,43 @@
import { Button, DatePicker, Input, message, Space, Spin, Table } from "antd"; import { Button, DatePicker, Input, Space, Spin, Table, message } from "antd";
import { ColumnsType } from "antd/es/table"; 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 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<UsageStats[]>([]); const [data, setData] = useState([]);
const [filteredData, setFilteredData] = useState<UsageStats[]>([]); const [filteredData, setFilteredData] = useState([]);
const [range, setRange] = useState<[Dayjs, Dayjs]>([ const [range, setRange] = useState([
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<UsageStats | null>(null); const [selectedRecord, setSelectedRecord] = useState(null);
const fetchData = useCallback(async () => { const fetchData = async () => {
setLoading(true); setLoading(true);
try { try {
const res = await axiosInstance.get<UsageStats[], UsageStats[]>( const res = await axiosInstance.get("/device/usage-stats", {
"/device/usage-stats", params: {
{ start: range[0].format("YYYY-MM-DD"),
params: { end: range[1].format("YYYY-MM-DD"),
start: range[0].format("YYYY-MM-DD"), },
end: range[1].format("YYYY-MM-DD"), });
},
}
);
setData(res); setData(res);
setFilteredData(res); setFilteredData(res);
} catch { } catch (e) {
message.error("获取数据失败"); message.error("获取数据失败");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [range]); };
const handleSearch = (value: string) => { const handleSearch = (value) => {
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())
@ -101,9 +89,9 @@ export default function DeviceStats() {
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
}, [fetchData]); }, [range]);
const columns: ColumnsType<UsageStats> = [ const columns = [
{ {
title: "设备名称", title: "设备名称",
dataIndex: "deviceName", dataIndex: "deviceName",
@ -147,7 +135,7 @@ export default function DeviceStats() {
<RangePicker <RangePicker
value={range} value={range}
onChange={(dates) => { onChange={(dates) => {
if (dates && dates[0] && dates[1]) setRange([dates[0], dates[1]]); if (dates) setRange(dates);
}} }}
presets={datePresets} presets={datePresets}
/> />
@ -162,21 +150,19 @@ export default function DeviceStats() {
</Button> </Button>
</Space> </Space>
<Table<UsageStats> <Table
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} onClose={() => {
onClose={() => { setVisiable(false);
setVisiable(false); }}
}} range={range}
range={range} />
/>
)}
</Spin> </Spin>
); );
} }

View File

@ -1,53 +1,40 @@
import { Button, DatePicker, Input, Space, Spin, Table, message } from "antd"; import { Button, DatePicker, Input, Space, Spin, Table, message } from "antd";
import dayjs, { Dayjs } from "dayjs"; import dayjs from "dayjs";
import { useCallback, useEffect, useState } from "react"; import { 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<ReservationStat[]>([]); const [data, setData] = useState([]);
const [filteredData, setFilteredData] = useState<ReservationStat[]>([]); const [filteredData, setFilteredData] = useState([]);
const [range, setRange] = useState<[Dayjs, Dayjs]>([ const [range, setRange] = useState([
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 = useCallback(async () => { const fetchData = async () => {
setLoading(true); setLoading(true);
try { try {
const res = await axiosInstance.get<ReservationStat[], ReservationStat[]>( const res = await axiosInstance.get("/reservation/stats", {
"/reservation/stats", params: {
{ start: range[0].format("YYYY-MM-DD"),
params: { end: range[1].format("YYYY-MM-DD"),
start: range[0].format("YYYY-MM-DD"), },
end: range[1].format("YYYY-MM-DD"), });
},
}
);
setData(res); setData(res);
setFilteredData(res); setFilteredData(res);
} catch { } catch (e) {
message.error("获取数据失败"); message.error("获取数据失败");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [range]); };
const handleSearch = (value: string) => { const handleSearch = (value) => {
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())
@ -99,9 +86,9 @@ export default function ReservationStats() {
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
}, [fetchData]); }, [range]);
const columns: ColumnsType<ReservationStat> = [ const columns = [
{ {
title: "设备名称", title: "设备名称",
dataIndex: "deviceName", dataIndex: "deviceName",
@ -134,7 +121,7 @@ export default function ReservationStats() {
<RangePicker <RangePicker
value={range} value={range}
onChange={(dates) => { onChange={(dates) => {
if (dates && dates[0] && dates[1]) setRange([dates[0], dates[1]]); if (dates) setRange(dates);
}} }}
presets={datePresets} presets={datePresets}
/> />

View File

@ -0,0 +1,33 @@
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

@ -1,46 +0,0 @@
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,30 +4,29 @@ 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<Team[]>([]); const [teams, setTeams] = useState([]);
const [data, setData] = useState<Team[]>([]); const [data, setData] = useState([]);
const [searchName, setSearchName] = useState<string>(); const [searchName, setSearchName] = useState();
const [editingId, setEditingId] = useState<string | null>(null); const [editingId, setEditingId] = useState();
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<Team | null>(null); const [selectedTeam, setSelectedTeam] = useState(null);
const fetchData = async () => { const fetchData = async () => {
const data = await axiosInstance.get<Team[], Team[]>("/teams"); const data = await axiosInstance.get("/teams");
setData(data); setData(data);
setTeams(data); setTeams(data);
setSearchName(""); setSearchName(null);
}; };
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
}, []); }, []);
const handleSearch = (value: string) => { const handleSearch = (value) => {
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())
@ -35,18 +34,18 @@ export default function TeamManage() {
setTeams(filtered); setTeams(filtered);
}; };
const handleDelete = async (record: Team) => { const handleDelete = async (record) => {
await axiosInstance.delete(`/team/${record.id}`); await axiosInstance.delete(`/team/${record.id}`);
message.success("删除成功"); message.success("删除成功");
fetchData(); fetchData();
}; };
const handleEdit = (record: Team) => { const handleEdit = (record) => {
setEditingId(record.id); setEditingId(record.id);
setEditingName(record.name); setEditingName(record.name);
}; };
const handleSave = async (teamId: string) => { const handleSave = async (teamId) => {
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);
@ -63,7 +62,7 @@ export default function TeamManage() {
name: newTeamName, name: newTeamName,
}); });
message.success("添加成功"); message.success("添加成功");
setNewTeamName(""); setNewTeamName(null);
fetchData(); fetchData();
}; };
@ -88,7 +87,7 @@ export default function TeamManage() {
style={{ width: "300px" }} style={{ width: "300px" }}
/> />
</Flex> </Flex>
<Table<Team> rowKey="id" dataSource={teams}> <Table rowKey="id" dataSource={teams}>
<Column <Column
title="团队名" title="团队名"
key="name" key="name"
@ -107,7 +106,7 @@ export default function TeamManage() {
<Column title="下属人数" key="size" dataIndex="size" /> <Column title="下属人数" key="size" dataIndex="size" />
<Column <Column
title="操作" title="操作"
render={(_, record: Team) => { render={(_, record) => {
return ( return (
<Space> <Space>
{record.id === editingId ? ( {record.id === editingId ? (
@ -146,16 +145,14 @@ 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); }}
}} />
/>
)}
</> </>
); );
} }

View File

@ -1,43 +1,22 @@
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 { UserVo } from "types/model"; import Password from "antd/es/input/Password";
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({
visible, visiable,
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<Partial<UserDTO>>({}); const [initialValues, setInitialValues] = useState();
const [teams, setTeams] = useState<{ label: string; value: string }[]>(); const [teams, setTeams] = useState([]);
const fetchTeams = async () => { const fetchTeams = async () => {
const data = await axiosInstance.get< const data = await axiosInstance.get("/team-label");
unknown,
{ label: string; value: string }[]
>("/team-label");
setTeams(data); setTeams(data);
}; };
@ -46,9 +25,9 @@ export default function UserDetailModal({
}, []); }, []);
useEffect(() => { useEffect(() => {
if (visible) { if (visiable) {
if (mode === "edit") { if (mode === "edit") {
const values: UserDTO = { const values = {
username: user.username, username: user.username,
name: user.name, name: user.name,
phone: user.phone, phone: user.phone,
@ -59,18 +38,24 @@ export default function UserDetailModal({
setInitialValues(values); setInitialValues(values);
form.setFieldsValue(values); form.setFieldsValue(values);
} else { } else {
const values: Partial<UserDTO> = {}; const values = {
username: undefined,
password: undefined,
name: undefined,
phone: undefined,
teamId: undefined,
roleId: undefined,
};
setInitialValues(values); setInitialValues(values);
form.setFieldsValue(values); form.setFieldsValue(values);
} }
} }
}, [visible, mode, user, form]); }, [visiable, mode, user, form]);
const handleOk = async () => { const handleOk = async () => {
const values = await form.validateFields(); const values = await form.validateFields();
const data: Partial<UserDTO> = {}; const data = {};
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];
} }
@ -86,14 +71,14 @@ export default function UserDetailModal({
} }
} }
onSuccess(); onSuccess();
onClose(); onclose();
}; };
return ( return (
<Modal <Modal
title={mode === "edit" ? "编辑用户" : "添加用户"} title={mode === "edit" ? "编辑用户" : "添加用户"}
open={visible} open={visiable}
onCancel={() => { onCancel={() => {
onClose(); onclose();
}} }}
onOk={handleOk} onOk={handleOk}
okText="保存" okText="保存"

View File

@ -2,33 +2,29 @@ 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<UserVo[]>([]); const [users, setUsers] = useState([]);
// const [teams, setTeams] = useState([]); const [teams, setTeams] = useState([]);
const [modalMode, setModalMode] = useState<string>("create"); const [modalMode, setModalMode] = useState();
const [selectedUser, setSelectedUser] = useState<UserVo | null>(); const [selectedUser, setSelectedUser] = useState();
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [roles, setRoles] = useState<{ label: string; value: string }[]>([]); const [roles, setRoles] = useState([]);
const [pagination, setPagination] = useState<Pagination>({ const [pagination, setPagination] = useState({
current: 1, current: 1,
pageSize: 10, pageSize: 10,
total: 0, total: 0,
}); });
const fetchRoles = async () => { const fetchRoles = async () => {
const data = await axiosInstance.get< const data = await axiosInstance.get("/role");
unknown,
{ label: string; value: string }[]
>("/role");
setRoles(data); setRoles(data);
}; };
const fetchData = async (pagination: Pagination, name?: string) => { const fetchData = async (pagination, name) => {
const data = await axiosInstance.get<unknown, PageResult<UserVo>>("/user", { const data = await axiosInstance.get("/user", {
params: { params: {
page: pagination.current, page: pagination.current,
size: pagination.pageSize, size: pagination.pageSize,
@ -50,11 +46,11 @@ export default function UserManage() {
}); });
}, []); }, []);
const handlePageChange = async (pagination: Pagination) => { const handlePageChange = async (pagination) => {
await fetchData(pagination); await fetchData(pagination);
}; };
const handleSearch = async (value: string) => { const handleSearch = async (value) => {
await fetchData( await fetchData(
{ {
...pagination, ...pagination,
@ -64,7 +60,7 @@ export default function UserManage() {
); );
}; };
const handleDelete = async (record: UserVo) => { const handleDelete = async (record) => {
await axiosInstance.delete(`/user/${record.userId}`); await axiosInstance.delete(`/user/${record.userId}`);
fetchData({ fetchData({
current: 1, current: 1,
@ -112,7 +108,7 @@ export default function UserManage() {
/> />
<Column <Column
title="操作" title="操作"
render={(_, record: UserVo) => { render={(_, record) => {
return ( return (
<Space> <Space>
<Button <Button
@ -139,24 +135,22 @@ 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); }}
}} onSuccess={async () => {
onSuccess={async () => { await fetchData({
await fetchData({ ...pagination,
...pagination, current: 1,
current: 1, });
}); }}
}} />
/>
)}
</> </>
); );
} }

View File

@ -15,34 +15,21 @@ 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({
visible, visiable,
mode = "create", mode = "create",
device, device,
onClose, onclose,
onSuccess, onSuccess,
}: DeviceDetailModalProps) { }) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [imageFile, setImageFile] = useState<RcFile | null>(null); const [imageFile, setImageFile] = useState(null);
const [initialValues, setInitialValues] = useState<Partial<DeviceAdminVO>>( const [initialValues, setInitialValues] = useState({});
{} const [fileList, setFileList] = useState();
);
const [fileList, setFileList] = useState<UploadFile[]>();
const userId = useSelector(selectUserId); const userId = useSelector(selectUserId);
useEffect(() => { useEffect(() => {
if (visible) { if (visiable) {
setFileList([]); setFileList([]);
if (mode === "edit") { if (mode === "edit") {
const values = { const values = {
@ -55,22 +42,21 @@ export default function DeviceDetailModal({
setInitialValues(values); setInitialValues(values);
} else { } else {
const values = { const values = {
name: "", name: undefined,
location: "", location: undefined,
usageRequirement: "", usageRequirement: undefined,
status: "", status: undefined,
}; };
form.setFieldsValue(values); form.setFieldsValue(values);
setInitialValues(values); setInitialValues(values);
} }
} }
}, [visible, mode, device, form]); }, [visiable, mode, device, form]);
const handleOk = async () => { const handleOk = async () => {
const values = await form.validateFields(); const values = await form.validateFields();
const data: Partial<DeviceAdminVO> = {}; const data = {};
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];
} }
@ -97,15 +83,15 @@ export default function DeviceDetailModal({
message.success("图片上传成功"); message.success("图片上传成功");
} }
onSuccess(); onSuccess();
onClose(); onclose();
}; };
return ( return (
<Modal <Modal
title={mode === "edit" ? "编辑设备" : "添加设备"} title={mode === "edit" ? "编辑设备" : "添加设备"}
open={visible} open={visiable}
onCancel={() => { onCancel={() => {
onClose(); onclose();
}} }}
onOk={handleOk} onOk={handleOk}
okText="保存" okText="保存"

View File

@ -9,69 +9,51 @@ import {
Tag, Tag,
} from "antd"; } from "antd";
import Column from "antd/es/table/Column"; import Column from "antd/es/table/Column";
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 { 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<DeviceAdminVO[]>([]); const [devices, setDevices] = useState([]);
const [selectedDevice, setSelectedDevice] = useState<DeviceAdminVO | null>( const [selectedDevice, setSelectedDevice] = useState(null);
null
);
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [modalMode, setModalMode] = useState("create"); const [modalMode, setModalMode] = useState(null);
const [searchName, setSearchName] = useState<string>(""); const [searchName, setSearchName] = useState(null);
const [pagination, setPagination] = useState<Pagination>({ const [pagination, setPagination] = useState({
current: 1, current: 1,
pageSize: 10, pageSize: 10,
total: 0, total: 0,
}); });
const userId = useSelector(selectUserId); const userId = useSelector(selectUserId);
const fetchData = useCallback( const fetchData = async (pagination, name = searchName) => {
async (pagination: Pagination, name: string = searchName) => { const data = await axiosInstance.get(`/device/${userId}`, {
const data = await axiosInstance.get<unknown, PageResult<DeviceAdminVO>>( params: {
`/device/${userId}`, page: pagination.current,
{ size: pagination.pageSize,
params: { name,
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: Pagination) => { const handlePageChange = async (pagination) => {
await fetchData(pagination); await fetchData(pagination);
}; };
const handleDelete = async (deviceId: string) => { const handleDelete = async (deviceId) => {
await axiosInstance.delete(`/device/${deviceId}`); await axiosInstance.delete(`/device/${deviceId}`);
message.success("删除成功"); message.success("删除成功");
const newPagination = { const newPagination = {
@ -82,7 +64,7 @@ export default function DeviceManage() {
await fetchData(newPagination); await fetchData(newPagination);
}; };
const handleSearch = async (value: string) => { const handleSearch = async (value) => {
setSearchName(value); setSearchName(value);
const newPagination = { const newPagination = {
...pagination, ...pagination,
@ -146,7 +128,7 @@ export default function DeviceManage() {
/> />
<Column <Column
title="操作" title="操作"
render={(_, record: DeviceAdminVO) => { render={(_, record) => {
return ( return (
<Space> <Space>
<Button <Button
@ -173,23 +155,21 @@ 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); }}
}} onSuccess={async () => {
onSuccess={async () => { await fetchData({
await fetchData({ ...pagination,
...pagination, current: 1,
current: 1, });
}); }}
}} />
/>
)}
</> </>
); );
} }

View File

@ -1,26 +1,14 @@
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 { 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 { 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<ReservationVO[]>([]); const [reservations, setReservations] = useState([]);
const [pagination, setPagination] = useState<Pagination>({ const [pagination, setPagination] = useState({
current: 1, current: 1,
pageSize: 10, pageSize: 10,
total: 0, total: 0,
@ -33,40 +21,30 @@ export default function Approval() {
showNeedAssist = true; showNeedAssist = true;
} }
const fetchData = useCallback( const fetchData = async (pagination) => {
async (pagination: Pagination) => { const data = await axiosInstance.get(`/reservation/approval/${userId}`, {
const data = await axiosInstance.get<unknown, PageResult<ReservationVO>>( params: {
`/reservation/approval/${userId}`, page: pagination.current,
{ 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: Pagination) => { const handlePageChange = async (pagination) => {
await fetchData(pagination); await fetchData(pagination);
}; };
const handleApproval = async ( const handleApproval = async (reservationId, isApprove, needAssist) => {
reservationId: string,
isApprove: boolean,
needAssist: boolean
) => {
await axiosInstance.post("/approval", { await axiosInstance.post("/approval", {
userId, userId,
reservationId, reservationId,

View File

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

View File

@ -1,67 +1,50 @@
import { Button, Col, Form, Input, message, Row } from "antd"; import { Button, Col, Form, Input, message, Row } from "antd";
import { useCallback, useEffect, useState } from "react"; import { useForm } from "antd/es/form/Form";
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<Partial<UserVo>>({}); const [user, setUser] = useState({
userId: "",
username: "",
team: "",
name: "",
phone: "",
});
const userId = useSelector(selectUserId); const userId = useSelector(selectUserId);
const fetchUser = useCallback( const fetchUser = async (userId) => {
async (userId: string | null) => { const user = await axiosInstance.get(`/userdetail/${userId}`);
if (!userId) return; setUser(user);
const user = await axiosInstance.get<unknown, UserVo>( form.setFieldsValue(user);
`/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: UserForm) => { const handleSubmit = async (values) => {
if (values.password && values.password !== values.confirmPassword) { if (values.password && values.password !== values.confirmPassword) {
message.error("两次输入的密码不一致"); message.error("两次输入的密码不一致");
return; return;
} }
type FormKey = keyof UserForm; const changedFields = {};
const changedFields: Record<string, string> = {}; for (const key in values) {
for (const _key in values) { if (values[key] !== user[key] && values[key] !== undefined) {
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<unknown, UserVo>( const newUser = await axiosInstance.put(`/user/${userId}`, changedFields);
`/user/${userId}`,
changedFields
);
setUser(newUser); setUser(newUser);
form.setFieldsValue(newUser); form.setFieldsValue(newUser);
message.success("修改成功"); message.success("修改成功");

View File

@ -10,45 +10,21 @@ import {
Select, Select,
Space, Space,
} from "antd"; } from "antd";
import dayjs, { Dayjs } from "dayjs"; import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween"; import isBetween from "dayjs/plugin/isBetween";
import { useCallback, useEffect, useState } from "react"; import { 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";
interface FormValue { export default function DeviceDetailModal({ visiable, device, onclose }) {
name: string; const [unavailableTimes, setUnavailableTims] = useState([]);
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(selectUserId); const userId = useSelector((state) => state.auth.userId);
const [teams, setTeams] = useState<{ label: string; value: string }[]>([]); const [teams, setTeams] = useState([]);
const [initialValues, setInitialValues] = useState<FormValue>(); const [initialValues, setInitialValues] = useState();
const fetchTeams = async () => { const fetchTeams = async () => {
const data = await axiosInstance.get< const data = await axiosInstance.get("/team-label");
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,
@ -56,10 +32,8 @@ export default function DeviceDetailModal({
setTeams(teams); setTeams(teams);
}; };
const fetchUser = useCallback(async () => { const fetchUser = async () => {
const data = await axiosInstance.get<unknown, UserVo>( const data = await axiosInstance.get(`/userdetail/${userId}`);
`/userdetail/${userId}`
);
const values = { const values = {
name: data.name, name: data.name,
phone: data.phone, phone: data.phone,
@ -67,34 +41,28 @@ export default function DeviceDetailModal({
}; };
setInitialValues(values); setInitialValues(values);
form.setFieldsValue(values); form.setFieldsValue(values);
}, [userId, form]); };
useEffect(() => { useEffect(() => {
fetchTeams(); fetchTeams();
}, []); }, []);
useEffect(() => { useEffect(() => {
const fetchUnavailableTimes = async (id: string) => { const fetchUnavailableTimes = async (id) => {
const data = await axiosInstance.get< const data = await axiosInstance.get(`/device/unavailable-times/${id}`);
unknown, setUnavailableTims(data);
{ startTime: Date; endTime: Date }[]
>(`/device/unavailable-times/${id}`);
setUnavailableTimes(data);
}; };
if (visible && device?.deviceId) { if (visiable && device?.deviceId) {
fetchUnavailableTimes(device.deviceId); fetchUnavailableTimes(device.deviceId);
fetchUser(); fetchUser();
} }
}, [visible, device?.deviceId, fetchUser]); }, [visiable, device?.deviceId]);
const { RangePicker } = DatePicker; const { RangePicker } = DatePicker;
const disabledDate = ( const disabledDate = (current, { from } = {}) => {
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");
@ -110,7 +78,7 @@ export default function DeviceDetailModal({
}); });
// 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;
}; };
@ -133,15 +101,15 @@ export default function DeviceDetailModal({
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={visible} open={visiable}
onCancel={() => { onCancel={() => {
onClose(); onclose();
}} }}
onOk={handleOK} onOk={handleOK}
> >

View File

@ -1,28 +1,14 @@
import { Table, Tag } from "antd"; import { Table, Tag } from "antd";
import { useCallback, useEffect, useState } from "react"; import { 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<UserReservationVO[]>([]); const [reservations, setReservations] = useState([]);
const [pagination, setPagination] = useState<Pagination>({ const [pagination, setPagination] = useState({
current: 1, current: 1,
pageSize: 10, pageSize: 10,
total: 0, total: 0,
@ -30,31 +16,25 @@ export default function MyReservation() {
const userId = useSelector(selectUserId); const userId = useSelector(selectUserId);
const fetchData = useCallback( const fetchData = async (pagination) => {
async (pagination: Pagination) => { const data = await axiosInstance.get(`/reservation/${userId}`, {
const data = await axiosInstance.get< params: {
unknown, page: pagination.current,
PageResult<UserReservationVO> size: pagination.pageSize,
>(`/reservation/${userId}`, { },
params: { });
page: pagination.current, setReservations(data.records);
size: pagination.pageSize, setPagination({
}, ...pagination,
}); total: data.total,
setReservations(data.records); });
setPagination({ };
...pagination,
total: data.total,
});
},
[userId]
);
useEffect(() => { useEffect(() => {
fetchData(pagination); fetchData(pagination);
}, [fetchData, pagination]); }, []);
const handlePageChange = (pagination: Pagination) => { const handlePageChange = (pagination) => {
fetchData(pagination); fetchData(pagination);
}; };

View File

@ -1,9 +1,8 @@
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 { useCallback, useEffect, useState } from "react"; import { 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",
@ -12,57 +11,41 @@ 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<string | null>(null); const [name, setName] = useState(null);
const [pagination, setPagination] = useState<Pagination>({ const [pagination, setPagination] = useState({
current: 1, current: 1,
pageSize: 10, pageSize: 10,
total: 0, total: 0,
}); });
const [devices, setDevices] = useState<DeviceVO[]>([]); const [devices, setDevices] = useState([]);
const fetchData = useCallback( const fetchData = async (pagination, searchName = name) => {
async (pagination: Pagination, searchName = name) => { const data = await axiosInstance.get("/device", {
const data = await axiosInstance.get<unknown, PageResult<DeviceVO>>( params: {
"/device", page: pagination.current,
{ size: pagination.pageSize,
params: { name: searchName,
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: Pagination) => { const handlePageChange = (pagination) => {
fetchData(pagination); fetchData(pagination);
}; };
const [selectedDevice, setSelectedDevice] = useState<DeviceVO | null>(null); const [selectedDevice, setSelectedDevice] = useState(null);
const handleSearch = (value: string) => { const handleSearch = (value) => {
setName(value); setName(value);
const newPagination = { const newPagination = {
...pagination, ...pagination,
@ -91,7 +74,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
@ -100,10 +83,7 @@ export default function Reserve() {
dataIndex="state" dataIndex="state"
render={(_, { state }) => ( render={(_, { state }) => (
<> <>
<Tag <Tag color={statusColorMap[state]} key="state">
color={statusColorMap[state as keyof typeof statusColorMap]}
key="state"
>
{state} {state}
</Tag> </Tag>
</> </>
@ -112,21 +92,18 @@ export default function Reserve() {
<Column <Column
title="操作" title="操作"
key="action" key="action"
render={(_, record: DeviceVO) => ( render={(_, record) => (
<Space size="middle"> <Space size="middle">
<a onClick={() => setSelectedDevice(record)}>预约</a> <a onClick={() => setSelectedDevice(record)}>预约</a>
</Space> </Space>
)} )}
/> />
</Table> </Table>
<DeviceDetailModal
{selectedDevice && ( visiable={!!selectedDevice}
<DeviceDetailModal device={selectedDevice}
visible={!!selectedDevice} onclose={() => setSelectedDevice(null)}
device={selectedDevice} />
onClose={() => setSelectedDevice(null)}
/>
)}
</> </>
); );
} }

View File

@ -2,11 +2,7 @@ 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({ export default function ProtectedRoute({ allowedRoles }) {
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,6 +1,5 @@
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");
@ -22,7 +21,3 @@ export const store = configureStore({
}, },
preloadedState, preloadedState,
}); });
export type RootState = ReturnType<typeof store.getState>;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

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

View File

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

View File

@ -1,15 +0,0 @@
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
View File

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

View File

@ -1,115 +0,0 @@
{
"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,8 +5,4 @@ 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,
},
}); });