项目初始化

This commit is contained in:
禺狨 2023-06-25 11:25:39 +08:00
commit 87f82475aa
39 changed files with 893 additions and 0 deletions

1
.env.example Normal file
View File

@ -0,0 +1 @@
VITE_APP_URL=

31
.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
package-lock.json
yarn.lock
.env.production
.env.development
.env

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@reduxjs/toolkit": "^1.9.3",
"antd-mobile": "^5.31.1",
"axios": "^1.3.4",
"localforage": "^1.10.0",
"match-sorter": "^6.3.1",
"moment": "^2.29.4",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-ga": "^3.3.1",
"react-redux": "^8.0.5",
"react-router-dom": "^6.9.0",
"redux": "^4.2.1",
"sort-by": "^1.2.0",
"web-vitals": "^3.3.0"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react-swc": "^3.0.0",
"rollup-plugin-gzip": "^3.1.0",
"sass": "^1.59.3",
"typescript": "^4.9.3",
"vite": "^4.2.0"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

6
src/App.scss Normal file
View File

@ -0,0 +1,6 @@
#root {
width: 100%;
margin: 0 auto;
text-align: center;
background-color: #ffffff;
}

31
src/App.tsx Normal file
View File

@ -0,0 +1,31 @@
import { Suspense, useEffect } from "react";
import ReactGA from "react-ga";
import { useLocation, useRoutes } from "react-router-dom";
import routes from "./routes";
import "./App.scss";
import LoadingPage from "./pages/loading";
const G_ID = import.meta.env.VITE_G_ID || "";
if (G_ID) {
ReactGA.initialize(G_ID);
}
const App = () => {
const Views = () => useRoutes(routes);
const location = useLocation();
useEffect(() => {
if (!G_ID) {
return;
}
ReactGA.pageview(location.pathname + location.search);
}, [location]);
return (
<Suspense fallback={<LoadingPage />}>
<Views />
</Suspense>
);
};
export default App;

13
src/AutoTop.ts Normal file
View File

@ -0,0 +1,13 @@
import React from "react";
import { useLayoutEffect } from "react";
import { useLocation } from "react-router-dom";
const AutoScorllTop: React.FC<{ children: any }> = ({ children }) => {
const location = useLocation();
useLayoutEffect(() => {
document.documentElement.scrollTo(0, 0);
}, [location.pathname]);
return children;
};
export default AutoScorllTop;

33
src/api/course.ts Normal file
View File

@ -0,0 +1,33 @@
import client from "./internal/httpClient";
// 线上课详情
export function detail(id: number) {
return client.get(`/api/v1/course/${id}`, {});
}
// 线上课课时详情
export function play(courseId: number, id: number) {
return client.get(`/api/v1/course/${courseId}/hour/${id}`, {});
}
// 获取播放地址
export function playUrl(courseId: number, hourId: number) {
return client.get(`/api/v1/course/${courseId}/hour/${hourId}/play`, {});
}
// 记录学员观看时长
export function record(courseId: number, hourId: number, duration: number) {
return client.post(`/api/v1/course/${courseId}/hour/${hourId}/record`, {
duration,
});
}
//观看ping
export function playPing(courseId: number, hourId: number) {
return client.post(`/api/v1/course/${courseId}/hour/${hourId}/ping`, {});
}
//最近学习课程
export function latestLearn() {
return client.get(`/api/v1/user/latest-learn`, {});
}

4
src/api/index.ts Normal file
View File

@ -0,0 +1,4 @@
export * as login from "./login";
export * as user from "./user";
export * as course from "./course";
export * as system from "./system";

View File

@ -0,0 +1,143 @@
import axios, { Axios, AxiosResponse } from "axios";
import { Toast } from "antd-mobile";
import { getToken, clearToken } from "../../utils/index";
const GoLogin = () => {
clearToken();
window.location.href = "/login";
};
export class HttpClient {
axios: Axios;
constructor(url: string) {
this.axios = axios.create({
baseURL: url,
timeout: 15000,
withCredentials: false,
headers: {
Accept: "application/json",
},
});
//拦截器注册
this.axios.interceptors.request.use(
(config) => {
const token = getToken();
token && (config.headers.Authorization = "Bearer " + token);
return config;
},
(err) => {
return Promise.reject(err);
}
);
this.axios.interceptors.response.use(
(response: AxiosResponse) => {
let code = response.data.code; //业务返回代码
let msg = response.data.msg; //错误消息
if (code === 0) {
return Promise.resolve(response);
} else {
Toast.show({
icon: "fail",
content: msg,
});
}
return Promise.reject(response);
},
// 当http的状态码非0
(error) => {
let status = error.response.status;
if (status === 401) {
Toast.show({
icon: "fail",
content: "请重新登录",
});
GoLogin();
} else if (status === 404) {
// 跳转到404页面
} else if (status === 403) {
// 跳转到无权限页面
} else if (status === 500) {
// 跳转到500异常页面
}
return Promise.reject(error.response);
}
);
}
get(url: string, params: object) {
return new Promise((resolve, reject) => {
this.axios
.get(url, {
params: params,
})
.then((res) => {
resolve(res.data);
})
.catch((err) => {
reject(err.data);
});
});
}
destroy(url: string) {
return new Promise((resolve, reject) => {
this.axios
.delete(url)
.then((res) => {
resolve(res.data);
})
.catch((err) => {
reject(err.data);
});
});
}
post(url: string, params: object) {
return new Promise((resolve, reject) => {
this.axios
.post(url, params)
.then((res) => {
resolve(res.data);
})
.catch((err) => {
reject(err.data);
});
});
}
put(url: string, params: object) {
return new Promise((resolve, reject) => {
this.axios
.put(url, params)
.then((res) => {
resolve(res.data);
})
.catch((err) => {
reject(err.data);
});
});
}
request(config: object) {
return new Promise((resolve, reject) => {
this.axios
.request(config)
.then((res) => {
resolve(res.data);
})
.catch((err) => {
reject(err.data);
});
});
}
}
const APP_URL = import.meta.env.VITE_APP_URL || "";
const client = new HttpClient(APP_URL);
export default client;

19
src/api/login.ts Normal file
View File

@ -0,0 +1,19 @@
import client from "./internal/httpClient";
export function login(
email: string,
password: string,
captchaKey: string,
captchaVal: string
) {
return client.post("/api/v1/auth/login/password", {
email: email,
password: password,
captcha_key: captchaKey,
captcha_val: captchaVal,
});
}
export function logout() {
return client.post("/api/v1/auth/logout", {});
}

9
src/api/system.ts Normal file
View File

@ -0,0 +1,9 @@
import client from "./internal/httpClient";
export function config() {
return client.get("/api/v1/system/config", {});
}
export function imageCaptcha() {
return client.get("/api/v1/system/image-captcha", {});
}

31
src/api/user.ts Normal file
View File

@ -0,0 +1,31 @@
import client from "./internal/httpClient";
export function detail() {
return client.get("/api/v1/user/detail", {});
}
// 修改密码
export function password(oldPassword: string, newPassword: string) {
return client.put("/api/v1/user/password", {
old_password: oldPassword,
new_password: newPassword,
});
}
// 学员课程
export function coursesCategories() {
return client.get("/api/v1/category/all", {});
}
export function courses(depId: number, categoryId: number) {
return client.get("/api/v1/user/courses", {
dep_id: depId,
category_id: categoryId,
});
}
// 修改头像
export function avatar(file: any) {
return client.put("/api/v1/user/avatar", {
file: file,
});
}

View File

@ -0,0 +1,4 @@
.img-box {
width: 100%;
height: auto;
}

View File

@ -0,0 +1,12 @@
import styles from "./index.module.scss";
import React from "react";
import { Image } from "antd-mobile";
import empty from "../../assets/images/commen/empty.png";
export const Empty: React.FC = () => {
return (
<div className={styles["img-box"]}>
<Image src={empty} width={400} height={400} />
</div>
);
};

1
src/components/index.ts Normal file
View File

@ -0,0 +1 @@
export * from "./empty"

View File

@ -0,0 +1,12 @@
import React from "react";
import { getToken } from "../../utils/index";
import { Navigate } from "react-router-dom";
interface PropInterface {
Component: any;
}
const PrivateRoute: React.FC<PropInterface> = ({ Component }) => {
return getToken() ? Component : <Navigate to="/login" replace={true} />;
};
export default PrivateRoute;

17
src/index.tsx Normal file
View File

@ -0,0 +1,17 @@
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import store from "./store";
import { BrowserRouter } from "react-router-dom";
import "./index.scss";
import App from "./App";
import AutoScorllTop from "./AutoTop";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<Provider store={store}>
<BrowserRouter>
<AutoScorllTop>
<App />
</AutoScorllTop>
</BrowserRouter>
</Provider>
);

3
src/js/config.ts Normal file
View File

@ -0,0 +1,3 @@
export default {
app_url: import.meta.env.VITE_APP_URL || "",
};

30
src/main.scss Normal file
View File

@ -0,0 +1,30 @@
body {
margin: 0;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
:root {
--adm-color-primary: #FF4D4F;
--adm-color-success: #00b578;
--adm-color-warning: #ff8f1f;
--adm-color-danger: #ff3141;
--adm-color-white: #ffffff;
--adm-color-text: #333333;
--adm-color-text-secondary: #666666;
--adm-color-weak: #999999;
--adm-color-light: #cccccc;
--adm-color-border: #eeeeee;
--adm-color-box: #f5f5f5;
--adm-color-background: #ffffff;
--adm-font-size-main: var(--adm-font-size-5);
--adm-font-family: -apple-system, blinkmacsystemfont, 'Helvetica Neue',
helvetica, segoe ui, arial, roboto, 'PingFang SC', 'miui',
'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
}

17
src/main.tsx Normal file
View File

@ -0,0 +1,17 @@
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import store from "./store";
import { BrowserRouter } from "react-router-dom";
import "./main.scss";
import App from "./App";
import AutoScorllTop from "./AutoTop";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<Provider store={store}>
<BrowserRouter>
<AutoScorllTop>
<App />
</AutoScorllTop>
</BrowserRouter>
</Provider>
);

2
src/pages/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './login';
export * from './layout';

View File

28
src/pages/index/index.tsx Normal file
View File

@ -0,0 +1,28 @@
import { useEffect, useState } from "react";
import { user } from "../../api/index";
import styles from "./index.module.scss";
import { useSelector } from "react-redux";
const IndexPage = () => {
const systemConfig = useSelector((state: any) => state.systemConfig.value);
const [loading, setLoading] = useState<boolean>(false);
const [tabKey, setTabKey] = useState(0);
const departments = useSelector(
(state: any) => state.loginUser.value.departments
);
const currentDepId = useSelector(
(state: any) => state.loginUser.value.currentDepId
);
useEffect(() => {
document.title = systemConfig.systemName || "首页";
}, [systemConfig]);
return (
<div className="main-body">
<div className="content"></div>
</div>
);
};
export default IndexPage;

66
src/pages/init/index.tsx Normal file
View File

@ -0,0 +1,66 @@
import { useState, useEffect } from "react";
import { useDispatch } from "react-redux";
import { Outlet } from "react-router-dom";
import {
SystemConfigStoreInterface,
saveConfigAction,
} from "../../store/system/systemConfigSlice";
import { loginAction } from "../../store/user/loginUserSlice";
import { useParams, useLocation } from "react-router-dom";
interface Props {
loginData?: any;
configData?: any;
}
export const InitPage = (props: Props) => {
const pathname = useLocation().pathname;
const params = useParams();
const dispatch = useDispatch();
const [init, setInit] = useState<boolean>(false);
useEffect(() => {
if (props.loginData) {
dispatch(loginAction(props.loginData));
}
if (props.configData) {
let config: SystemConfigStoreInterface = {
//系统配置
systemApiUrl: props.configData["system-api-url"],
systemH5Url: props.configData["system-h5-url"],
systemLogo: props.configData["system-logo"],
systemName: props.configData["system-name"],
systemPcUrl: props.configData["system-pc-url"],
pcIndexFooterMsg: props.configData["system-pc-index-footer-msg"],
//播放器配置
playerPoster: props.configData["player-poster"],
playerIsEnabledBulletSecret:
props.configData["player-is-enabled-bullet-secret"] &&
props.configData["player-is-enabled-bullet-secret"] === "1"
? true
: false,
playerIsDisabledDrag:
props.configData["player-disabled-drag"] &&
props.configData["player-disabled-drag"] === "1"
? true
: false,
playerBulletSecretText: props.configData["player-bullet-secret-text"],
playerBulletSecretColor: props.configData["player-bullet-secret-color"],
playerBulletSecretOpacity:
props.configData["player-bullet-secret-opacity"],
};
dispatch(saveConfigAction(config));
}
setInit(true);
}, [props]);
return (
<>
{init && (
<div>
<Outlet />
</div>
)}
</>
);
};

View File

@ -0,0 +1,11 @@
import { DotLoading } from 'antd-mobile'
const LoadingPage = () => {
return (
<>
<DotLoading color='primary' />
</>
);
};
export default LoadingPage;

View File

40
src/pages/login/index.tsx Normal file
View File

@ -0,0 +1,40 @@
import { Button } from "antd-mobile";
import { useDispatch, useSelector } from "react-redux";
import { loginAction, logoutAction } from "../../store/user/loginUserSlice";
const LoginPage = () => {
const dispatch = useDispatch();
const loginState = useSelector((state: any) => {
return state.loginUser.value;
});
return (
<>
<Button
onClick={() => {
dispatch(
loginAction({
user: {
name: "霸王",
},
})
);
}}
>
</Button>
{loginState.isLogin && (
<Button
onClick={() => {
dispatch(logoutAction());
}}
>
{loginState.user.name}
</Button>
)}
</>
);
};
export default LoginPage;

15
src/reportWebVitals.ts Normal file
View File

@ -0,0 +1,15 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

49
src/routes/index.tsx Normal file
View File

@ -0,0 +1,49 @@
import { lazy } from "react";
import { RouteObject } from "react-router-dom";
import { system, user } from "../api";
import { getToken } from "../utils";
import { InitPage } from "../pages/init";
import IndexPage from "../pages/index/index";
import LoginPage from "../pages/login";
import PrivateRoute from "../components/private-route";
let RootPage: any = null;
if (getToken()) {
RootPage = lazy(async () => {
return new Promise<any>(async (resolve) => {
try {
let configRes: any = await system.config();
let userRes: any = await user.detail();
resolve({
default: (
<InitPage configData={configRes.data} loginData={userRes.data} />
),
});
} catch (e) {
console.error("系统初始化失败", e);
}
});
});
} else {
RootPage = <InitPage />;
}
const routes: RouteObject[] = [
{
path: "/",
element: RootPage,
children: [
{
path: "/",
element: <IndexPage />,
},
{
path: "/login",
element: <LoginPage />,
},
],
},
];
export default routes;

12
src/store/index.ts Normal file
View File

@ -0,0 +1,12 @@
import { configureStore } from "@reduxjs/toolkit";
import systemConfigReducer from "./system/systemConfigSlice";
import loginUserReducer from "./user/loginUserSlice";
const store = configureStore({
reducer: {
loginUser: loginUserReducer,
systemConfig: systemConfigReducer,
},
});
export default store;

View File

@ -0,0 +1,48 @@
import { createSlice } from "@reduxjs/toolkit";
type SystemConfigStoreInterface = {
systemApiUrl: string;
systemPcUrl: string;
systemH5Url: string;
systemLogo: string;
systemName: string;
pcIndexFooterMsg: string;
playerPoster: string;
playerIsEnabledBulletSecret: boolean;
playerIsDisabledDrag: boolean;
playerBulletSecretText: string;
playerBulletSecretColor: string;
playerBulletSecretOpacity: string;
};
let defaultValue: SystemConfigStoreInterface = {
systemApiUrl: "",
systemPcUrl: "",
systemH5Url: "",
systemLogo: "",
systemName: "",
pcIndexFooterMsg: "",
playerPoster: "",
playerIsEnabledBulletSecret: false,
playerIsDisabledDrag: false,
playerBulletSecretText: "",
playerBulletSecretColor: "",
playerBulletSecretOpacity: "",
};
const systemConfigSlice = createSlice({
name: "systemConfig",
initialState: {
value: defaultValue,
},
reducers: {
saveConfigAction(stage, e) {
stage.value = e.payload;
},
},
});
export default systemConfigSlice.reducer;
export const { saveConfigAction } = systemConfigSlice.actions;
export type { SystemConfigStoreInterface };

View File

@ -0,0 +1,58 @@
import { createSlice } from "@reduxjs/toolkit";
import {
getDepKey,
clearDepKey,
clearDepName,
setDepName,
clearToken,
} from "../../utils/index";
type UserStoreInterface = {
user: null;
departments: string[];
currentDepId: number;
isLogin: boolean;
};
let defaultValue: UserStoreInterface = {
user: null,
departments: [],
currentDepId: Number(getDepKey()) || 0,
isLogin: false,
};
const loginUserSlice = createSlice({
name: "loginUser",
initialState: {
value: defaultValue,
},
reducers: {
loginAction(stage, e) {
stage.value.user = e.payload.user;
stage.value.departments = e.payload.departments;
stage.value.isLogin = true;
if (e.payload.departments.length > 0 && stage.value.currentDepId === 0) {
stage.value.currentDepId = e.payload.departments[0].id;
setDepName(e.payload.departments[0].name);
}
},
logoutAction(stage) {
stage.value.user = null;
stage.value.departments = [];
stage.value.isLogin = false;
stage.value.currentDepId = 0;
clearToken();
clearDepKey();
clearDepName();
},
saveCurrentDepId(stage, e) {
stage.value.currentDepId = e.payload;
},
},
});
export default loginUserSlice.reducer;
export const { loginAction, logoutAction, saveCurrentDepId } =
loginUserSlice.actions;
export type { UserStoreInterface };

52
src/utils/index.ts Normal file
View File

@ -0,0 +1,52 @@
import moment from "moment";
export function getToken(): string {
return window.localStorage.getItem("playedu-h5-token") || "";
}
export function setToken(token: string) {
window.localStorage.setItem("playedu-h5-token", token);
}
export function clearToken() {
window.localStorage.removeItem("playedu-h5-token");
}
export function dateFormat(dateStr: string) {
return moment(dateStr).format("YYYY-MM-DD HH:mm");
}
export function getHost() {
return window.location.protocol + "//" + window.location.host + "/";
}
export function getDepKey(): string {
return window.localStorage.getItem("playedu-h5-depatmentKey") || "";
}
export function setDepKey(token: string) {
window.localStorage.setItem("playedu-h5-depatmentKey", token);
}
export function clearDepKey() {
window.localStorage.removeItem("playedu-h5-depatmentKey");
}
export function getDepName(): string {
return window.localStorage.getItem("playedu-h5-depatmentName") || "";
}
export function setDepName(token: string) {
window.localStorage.setItem("playedu-h5-depatmentName", token);
}
export function clearDepName() {
window.localStorage.removeItem("playedu-frontend-depatmentName");
}
export function changeAppUrl(str: string) {
let key = str.slice(str.length - 1);
if (key === "/") {
return str;
} else {
return str + "/";
}
}

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

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

21
tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

9
tsconfig.node.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

13
vite.config.ts Normal file
View File

@ -0,0 +1,13 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import gzipPlugin from "rollup-plugin-gzip";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
plugins: [gzipPlugin()],
},
}
});