feat: 初始化前端项目基础架构
- 添加路由配置和基础页面组件 - 配置环境变量和全局类型定义 - 实现Pinia状态管理和axios封装 - 添加基础布局组件和全局头部 - 配置Vite构建工具和开发环境
This commit is contained in:
12
src/App.vue
12
src/App.vue
@@ -1,11 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import BasicLayout from './layouts/BasicLayout.vue';
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<RouterView />
|
||||
</main>
|
||||
<div id="app">
|
||||
<BasicLayout />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
#app {}
|
||||
</style>
|
||||
|
||||
10
src/access/accessEnum.ts
Normal file
10
src/access/accessEnum.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 权限定义
|
||||
*/
|
||||
|
||||
const ACCESS_ENUM = {
|
||||
NOT_LOGIN: "notLogin",
|
||||
USER: "user",
|
||||
ADMIN: "admin",
|
||||
};
|
||||
export default ACCESS_ENUM;
|
||||
BIN
src/assets/logo.webp
Normal file
BIN
src/assets/logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
75
src/components/GlobalHeader.vue
Normal file
75
src/components/GlobalHeader.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<a-row id="globalHeader" align="center" :wrap="false">
|
||||
|
||||
<a-col flex="auto">
|
||||
<a-menu mode="horizontal" :selected-keys="selectedKeys" @menu-item-click="doMenuItemClick">
|
||||
<a-menu-item key="0" :style="{ padding: 0, marginRight: '38px' }" disabled>
|
||||
<div class="title-bar">
|
||||
<img class="logo" src="@/assets/logo.webp" />
|
||||
<div class="logo-title">AI OJ</div>
|
||||
</div>
|
||||
</a-menu-item>
|
||||
<a-menu-item v-for="item in visibleRoutes" :key="item.path">
|
||||
{{ item.name }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-col>
|
||||
<a-col flex="100px">
|
||||
<div>MeowRain</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { routes } from '../router/index.ts';
|
||||
import { useRouter } from "vue-router";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
const router = useRouter();
|
||||
// 默认主页
|
||||
const selectedKeys = ref(["/"]);
|
||||
const doMenuItemClick = (key: string) => {
|
||||
// console.info("触发菜单跳转,当前路径: ", key)
|
||||
router.push({
|
||||
path: key
|
||||
})
|
||||
}
|
||||
const visibleRoutes = computed(() => {
|
||||
return routes.filter((item, index) => {
|
||||
return true;
|
||||
})
|
||||
})
|
||||
/**
|
||||
* router.afterEach 是 Vue Router 的全局后置钩子(global after hook),会在每次路由导航完成后触发。它的典型用途是:
|
||||
*
|
||||
* 记录页面访问日志
|
||||
*
|
||||
* 改变页面标题
|
||||
*
|
||||
* 停止 loading 动画
|
||||
*
|
||||
* 做一些不影响导航结果的副作用(因为 afterEach 无法取消导航)
|
||||
*/
|
||||
router.afterEach((to, from, failure) => {
|
||||
console.log('导航已完成:', from.fullPath, '->', to.fullPath)
|
||||
selectedKeys.value = [to.path]
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
#globalHeader {}
|
||||
|
||||
.title-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
9
src/config/index.ts
Normal file
9
src/config/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// src/config/index.ts
|
||||
interface EnvironmentVariables {
|
||||
APP_ENV: string;
|
||||
API_BASE_URL: string;
|
||||
}
|
||||
export const ENV: EnvironmentVariables = {
|
||||
APP_ENV: import.meta.env.VITE_APP_ENV,
|
||||
API_BASE_URL: import.meta.env.VITE_API_URL,
|
||||
};
|
||||
46
src/layouts/BasicLayout.vue
Normal file
46
src/layouts/BasicLayout.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div id="basicLayout">
|
||||
<a-layout style="height: 400px;">
|
||||
<a-layout-header class="header">
|
||||
<GlobalHeader />
|
||||
</a-layout-header>
|
||||
<a-layout-content class="content">
|
||||
<RouterView />
|
||||
</a-layout-content>
|
||||
<a-layout-footer class="footer">
|
||||
|
||||
<a href="https://meowrain.cn">AI OJ bY MeowRain</a>
|
||||
</a-layout-footer>
|
||||
</a-layout>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import GlobalHeader from "../components/GlobalHeader.vue";
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#basicLayout {}
|
||||
|
||||
#basicLayout .header {
|
||||
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#basicLayout .content {
|
||||
background: linear-gradient(to right, #eee, #fff);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: #efefef;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -3,8 +3,12 @@ import ArcoVue from "@arco-design/web-vue";
|
||||
import "@arco-design/web-vue/dist/arco.css";
|
||||
import "./style.css";
|
||||
import App from "./App.vue";
|
||||
import router from "./routes/router";
|
||||
import router from "./router/router";
|
||||
import {createPinia} from "pinia";
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
app.use(router);
|
||||
app.use(ArcoVue);
|
||||
app.use(pinia)
|
||||
app.mount("#app");
|
||||
|
||||
51
src/plugins/axios.ts
Normal file
51
src/plugins/axios.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import axios, {
|
||||
type AxiosInstance,
|
||||
type InternalAxiosRequestConfig,
|
||||
} from "axios";
|
||||
import { ENV } from "../config";
|
||||
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: ENV.API_BASE_URL,
|
||||
timeout: 10000,
|
||||
withCredentials: false,
|
||||
});
|
||||
// =======================
|
||||
// 请求拦截器
|
||||
// =======================
|
||||
request.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig<any>) => {
|
||||
// 自动携带 token
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
config.headers = config.headers || {};
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return error;
|
||||
}
|
||||
);
|
||||
// =======================
|
||||
// 响应拦截器
|
||||
// =======================
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
console.log("响应: ", response);
|
||||
const data = response.data;
|
||||
/**TODO: 增加响应码处理 */
|
||||
return data;
|
||||
},
|
||||
(error) => {
|
||||
/**
|
||||
* Promise.reject(error) 用来 返回一个状态为 rejected 的 Promise,相当于“主动抛出错误”,
|
||||
*
|
||||
* 让调用者能够在 .catch() 或 try/catch 中捕获这个错误。
|
||||
它在 异步流程、拦截器、错误处理 中非常常见。
|
||||
*/
|
||||
console.error("❌ 网络错误:", error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default request;
|
||||
8
src/router/index.ts
Normal file
8
src/router/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type {RouteRecordRaw} from "vue-router";
|
||||
import HomeView from "../views/HomeView.vue";
|
||||
import AboutView from "../views/AboutView.vue";
|
||||
|
||||
export const routes: Array<RouteRecordRaw> = [
|
||||
{path: "/home", name: "HomeView", component: HomeView},
|
||||
{path: "/about", name: "AboutView", component: AboutView},
|
||||
];
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createWebHashHistory, createRouter } from "vue-router";
|
||||
import HomeView from "../views/HomeView.vue";
|
||||
const routes = [{ path: "/", component: HomeView }];
|
||||
import { routes } from ".";
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
0
src/store/index.ts
Normal file
0
src/store/index.ts
Normal file
11
src/store/user.ts
Normal file
11
src/store/user.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineStore } from "pinia";
|
||||
import ACCESS_ENUM from "../access/accessEnum";
|
||||
export const useUserStore = defineStore("user", {
|
||||
state: () => ({
|
||||
loginUser: {
|
||||
userName: "未登录",
|
||||
userRole: ACCESS_ENUM.NOT_LOGIN,
|
||||
},
|
||||
}),
|
||||
actions: {},
|
||||
});
|
||||
0
src/types/global.d.ts
vendored
Normal file
0
src/types/global.d.ts
vendored
Normal file
47
src/utils/requets.ts
Normal file
47
src/utils/requets.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { AxiosResponse } from "axios";
|
||||
import request from "../plugins/axios";
|
||||
// 》》》 这里 T 表示的是响应类型 D表示请求类型
|
||||
// GET请求
|
||||
export function get<T>(url: string, params?: any) {
|
||||
return request.get<T>(url, { params });
|
||||
}
|
||||
|
||||
// POST请求
|
||||
export function post<T, D>(url: string, data?: D) {
|
||||
return request.post<T>(url, data);
|
||||
}
|
||||
|
||||
// PUT请求 强制要求必须设置data参数
|
||||
export function put<T, D>(url: string, data?: D) {
|
||||
return request.put<T>(url, data);
|
||||
}
|
||||
|
||||
// DELETE 请求
|
||||
export function del<T>(url: string, params?: any) {
|
||||
return request.delete<T>(url, { params });
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
export function upload<T>(url: string, file: File) {
|
||||
const formData: FormData = new FormData();
|
||||
formData.append("file", file);
|
||||
return request.post<T>(url, formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
export async function download(url: string, params?: any) {
|
||||
const result: AxiosResponse<Blob> = await request.get<Blob>(url, {
|
||||
params,
|
||||
responseType: "blob",
|
||||
});
|
||||
const downloadUrl = window.URL.createObjectURL(result.data);
|
||||
const link = document.createElement("a");
|
||||
link.href = downloadUrl;
|
||||
link.download = "download";
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
}
|
||||
11
src/views/AboutView.vue
Normal file
11
src/views/AboutView.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
About
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user