feat: 初始化前端项目基础架构

- 添加路由配置和基础页面组件
- 配置环境变量和全局类型定义
- 实现Pinia状态管理和axios封装
- 添加基础布局组件和全局头部
- 配置Vite构建工具和开发环境
This commit is contained in:
2025-11-15 19:26:28 +08:00
parent e243175146
commit a98eae385f
21 changed files with 507 additions and 19 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View 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
View 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,
};

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

View File

@@ -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
View 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
View 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},
];

View File

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

11
src/store/user.ts Normal file
View 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
View File

47
src/utils/requets.ts Normal file
View 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
View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>
About
</div>
</template>
<style scoped></style>