feat: 优化整体代码

This commit is contained in:
fonghehe 2026-04-30 19:46:33 +08:00
parent b336529d66
commit bfca8a8858
11 changed files with 200 additions and 55 deletions

View File

@ -16,6 +16,7 @@
"ShallowRef": true,
"Slot": true,
"Slots": true,
"Snackbar": true,
"VNode": true,
"WritableComputedRef": true,
"acceptHMRUpdate": true,
@ -71,6 +72,7 @@
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"showToast": true,
"storeToRefs": true,
"toRaw": true,
"toRef": true,

151
README.md
View File

@ -1,40 +1,147 @@
# vue-h5-template
<div align="center">
<h1>Vue H5 Template</h1>
<p>基于 Vue 3 + Vite 7 + TypeScript + 多 UI 组件库 + Pinia + viewport 适配方案,构建移动端快速开发脚手架</p>
基于 vue3 + vite + nutui or varlet or vant + ts + sass + viewport 适配方案 + axios 封装,构建手机端模板脚手架
<p>
<img src="https://img.shields.io/github/license/sunniejs/vue-h5-template" alt="license" />
<img src="https://img.shields.io/github/stars/sunniejs/vue-h5-template?style=social" alt="stars" />
<img src="https://img.shields.io/github/forks/sunniejs/vue-h5-template?style=social" alt="forks" />
</p>
如果你对 `ts` 没有要求,想用纯 `js` 进行开发, 可以[点这里](https://github.com/sunniejs/vue-h5-template/tree/vue-h5-template-lite)来获取vue-h5-template-lite
<p>
<a href="https://sunniejs.github.io/vue-h5-template/">在线文档</a> ·
<a href="https://github.com/sunniejs/vue-h5-template/issues">问题反馈</a>
</p>
</div>
如果你不熟悉 vue3想继续使用 vue2 开发的,可以[点这里](https://github.com/sunniejs/vue-h5-template/tree/vue2-h5-template)来获取 vue2-h5-template
---
如果你想使用 monorepo 架构进行开发,可以[点这里](https://github.com/fonghehe/vue-h5-template)来获取 monorepo 版本
## 特性
详细的说明文档请[点击](https://sunniejs.github.io/vue-h5-template/)查看
- **Vue 3.5** + **Vite 7** + **TypeScript 5.9** — 最新技术栈
- **多 UI 组件库** — 同时支持 Vant、NutUI、Varlet按需自动引入
- **Pinia 状态管理** — 配合 `pinia-plugin-persistedstate` 实现持久化
- **viewportvw适配** — 基于 `cnjm-postcss-px-to-viewport`,自动处理 UI 库 375/750 设计稿差异
- **Axios + useFetch 双请求方案** — 支持传统 Axios 和 `@vueuse/core``createFetch`
- **vue-i18n 多语言** — 按需懒加载语言包
- **文件路由** — 基于 `vite-plugin-pages` 自动生成路由
- **丰富的 Vite 插件** — Mock、Eruda 调试、PWA、QRCode、图片压缩、gzip 压缩、打包分析等
- **代码规范** — ESLint flat config + Prettier + Stylelint + Husky + lint-staged
- **Docker 部署** — 内置 Dockerfile + Nginx 配置
如果对你有帮助送我一颗珍贵的小星星(づ ̄ 3  ̄)づ ╭❤
## 环境要求
# 关于我
| 工具 | 版本 |
| ------- | ---------- |
| Node.js | >= 20.10.0 |
| pnpm | >= 9.12.0 |
扫描添加下方的微信并备注加交流群(已超过 200 人,只能邀请),交流学习,及时获取代码最新动态。
## 快速开始
<p>
<img src="https://cdn.jsdelivr.net/gh/fonghehe/picture/personal/account.jpg" width="256">
```bash
# 拉取项目
git clone https://github.com/sunniejs/vue-h5-template.git
# 进入项目目录
cd vue-h5-template
# 安装依赖
pnpm install
# 启动项目
pnpm dev
# 打包
pnpm build
# 预览打包结果
pnpm preview
```
## 项目结构
```
├── build/ # Vite 构建相关配置
│ ├── vite/plugins/ # Vite 插件配置auto-import、component、mock、eruda 等)
│ └── utils.ts # 构建工具函数
├── mock/ # Mock 数据
├── public/ # 静态资源
├── src/
│ ├── api/ # 接口管理
│ ├── assets/ # 项目资源(字体、图片等)
│ ├── layout/ # 布局组件
│ ├── locales/ # 国际化语言包
│ ├── router/ # 路由配置
│ ├── store/ # Pinia 状态管理
│ ├── styles/ # 全局样式 & SCSS 变量
│ ├── utils/ # 工具函数Axios 封装、useFetch 封装)
│ ├── views/ # 页面组件
│ ├── App.vue # 根组件
│ └── main.ts # 入口文件
├── types/ # TypeScript 类型声明
├── .env.* # 多环境变量配置
├── vite.config.mts # Vite 配置
├── postcss.config.js # PostCSS 配置viewport 适配)
├── Dockerfile # Docker 部署配置
└── nginx.conf # Nginx 配置
```
## 集成的 Vite 插件
| 插件 | 说明 |
| ------------------------------- | -------------------- |
| `unplugin-auto-import` | 按需自动引入 API |
| `unplugin-vue-components` | 按需自动引入组件 |
| `vite-plugin-pages` | 文件系统路由 |
| `vite-plugin-mock` | 本地 Mock 数据 |
| `@zhaojjiang/vite-plugin-eruda` | 移动端调试工具 |
| `vite-plugin-svg-icons` | SVG 图标 |
| `vite-plugin-compression` | Gzip 压缩 |
| `vite-plugin-imagemin` | 图片压缩 |
| `vite-plugin-pwa` | PWA 支持 |
| `vite-plugin-qrcode` | 开发时二维码 |
| `vite-plugin-restart` | 配置文件修改自动重启 |
| `vite-plugin-progress` | 构建进度条 |
| `@vitejs/plugin-basic-ssl` | 本地 HTTPS |
| `rollup-plugin-visualizer` | 打包分析 |
## 其他版本
- **vue-h5-template-lite**(纯 JS 版)— [点击查看](https://github.com/sunniejs/vue-h5-template/tree/vue-h5-template-lite)
- **vue2-h5-template**Vue 2 版)— [点击查看](https://github.com/sunniejs/vue-h5-template/tree/vue2-h5-template)
- **monorepo 版**Monorepo 架构)— [点击查看](https://github.com/fonghehe/vue-h5-template)
## 文档
详细的使用文档请查看 [在线文档](https://sunniejs.github.io/vue-h5-template/)
如果对你有帮助,欢迎 Star 支持 ⭐
## 关于我
扫描添加下方的微信并备注加交流群,交流学习,及时获取代码最新动态。
<p>
<img src="https://cdn.jsdelivr.net/gh/fonghehe/picture/personal/account.jpg" width="256" />
</p>
如果你觉得该项目有给你带来帮助,方便了你的日常开发,可以请作者喝一杯 ☕ 支持持续的迭代
如果你觉得该项目有给你带来帮助,可以请作者喝一杯 ☕ 支持持续的迭代
<table >
<table>
<tr align="center">
<td>WechatPay</td>
<td>AliPay</td>
<td>WechatPay</td>
<td>AliPay</td>
</tr>
<tr style="text-align:center">
<td> <img src="https://cdn.jsdelivr.net/gh/fonghehe/picture/contribute/wechatPay.jpeg" width="256" /></td>
<td>
<img src="https://cdn.jsdelivr.net/gh/fonghehe/picture/contribute/aliPay.jpeg" width="256" />
</td>
<tr align="center">
<td><img src="https://cdn.jsdelivr.net/gh/fonghehe/picture/contribute/wechatPay.jpeg" width="256" /></td>
<td><img src="https://cdn.jsdelivr.net/gh/fonghehe/picture/contribute/aliPay.jpeg" width="256" /></td>
</tr>
</table>
# Star History
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=sunniejs/vue-h5-template&type=Timeline)](https://star-history.com/#tobe-fe-dalao/fast-vue3&Timeline)
[![Star History Chart](https://api.star-history.com/svg?repos=sunniejs/vue-h5-template&type=Timeline)](https://star-history.com/#sunniejs/vue-h5-template&Timeline)
## License
[MIT](./License)

View File

@ -1,7 +1,11 @@
/** API request prefix — matches the proxy key in vite server config */
export const API_BASE_URL = '/api';
/** Mock API prefix — used by vite-plugin-mock */
export const MOCK_API_BASE_URL = '/mock-api';
/** Real backend URL for proxy — set to your actual backend address, e.g. 'http://localhost:8080' */
export const API_TARGET_URL = '';
/** Mock server URL for proxy — leave empty when using vite-plugin-mock locally */
export const MOCK_API_TARGET_URL = '';

View File

@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
@ -8,7 +8,10 @@
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover"
/>
<meta name="format-detection" content="telephone=no, email=no, date=no, address=no" />
<title>Vite App</title>
<meta name="theme-color" content="#ffffff" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title>Vue H5 Template</title>
</head>
<body>
<div id="app"></div>

View File

@ -15,7 +15,7 @@ export default [
return {
code: 200,
message: 'ok',
data: { name: 'Evan', age: 26 },
data: { name: 'Evan', age: 26, token: 'mock-token-123456' },
};
},
},

View File

@ -23,7 +23,6 @@
</template>
<script lang="ts" setup name="BasicLayoutPage">
import { useRouter } from 'vue-router';
import { Home, Horizontal, My, Location } from '@nutui/icons-vue';
const tabItem = [
@ -41,15 +40,18 @@
const showBorder = ref(true);
const route = useRoute();
watch(
() => router,
() => {
const judgeRoute = tabItem.some((item) => item.key === router.currentRoute.value.path.replace('/', ''));
activeTab.value = tabItem.findIndex((item) => item.key === router.currentRoute.value.path.replace('/', ''));
() => route.path,
(path) => {
const currentKey = path.replace('/', '');
const judgeRoute = tabItem.some((item) => item.key === currentKey);
activeTab.value = tabItem.findIndex((item) => item.key === currentKey);
tabbarVisible.value = judgeRoute;
showBorder.value = judgeRoute;
},
{ deep: true, immediate: true },
{ immediate: true },
);
const tabSwitch = (_item: any, index: number) => {

View File

@ -21,15 +21,12 @@ export const useUserStore = defineStore('user', {
this.info = info ?? '';
},
async login() {
try {
const res = await loginPassword(); // 调用登录接口
this.setInfo(res); // 设置用户信息
this.token = res.token; // 假设返回的 res 包含 token
return res;
} catch (error) {
console.error('Login failed', error);
throw error;
const res = await loginPassword();
this.setInfo(res);
if (res?.token) {
this.token = res.token;
}
return res;
},
},
persist: {

View File

@ -0,0 +1,16 @@
// CSS custom properties (light theme defaults)
:root {
--color-text: #213547;
--color-text-secondary: #666;
--color-background: #fff;
--color-background-soft: #f5f5f5;
}
@media (prefers-color-scheme: dark) {
:root {
--color-text: rgb(255 255 255 / 87%);
--color-text-secondary: rgb(255 255 255 / 60%);
--color-background: #1a1a1a;
--color-background-soft: #2c2c2c;
}
}

View File

@ -1,15 +1,20 @@
import axios from 'axios';
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { showToast } from 'vant';
import { useUserStore } from '@/store/modules/user';
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '',
withCredentials: false,
timeout: 10000,
timeout: 15000,
});
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const userStore = useUserStore();
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`;
}
return config;
},
(error: AxiosError) => {
@ -21,16 +26,21 @@ service.interceptors.response.use(
(response: AxiosResponse) => {
const res = response.data;
if (res.code !== 200) {
showToast(res.msg);
return Promise.reject(res.msg || 'Error');
} else {
return res.data;
showToast(res.msg || 'Error');
return Promise.reject(new Error(res.msg || 'Error'));
}
return res.data;
},
(error: AxiosError) => {
console.log('err' + error);
showToast(error.message);
return Promise.reject(error.message);
const status = error.response?.status;
if (status === 401) {
const userStore = useUserStore();
userStore.$reset();
window.location.hash = '#/login';
}
const message = (error.response?.data as any)?.msg || error.message || 'Network Error';
showToast(message);
return Promise.reject(error);
},
);

View File

@ -1,15 +1,16 @@
import { createFetch } from '@vueuse/core';
import { useCookies } from '@vueuse/integrations/useCookies';
import { showNotify } from 'vant';
import { useUserStore } from '@/store/modules/user';
const useFetchApi = createFetch({
baseUrl: '',
options: {
async beforeFetch({ options }) {
const myToken = useCookies().get((import.meta.env.VITE_TOKEN_KEY as string) || 'Authorization') || '';
const userStore = useUserStore();
const token = userStore.token || '';
options.headers = {
...options.headers,
Authorization: `Bearer ${myToken}`,
Authorization: `Bearer ${token}`,
};
return { options };
},

View File

@ -35,17 +35,20 @@
import { detailsData } from '../data';
import { Dshop, Dongdong, Cart } from '@nutui/icons-vue';
const router = useRouter();
const route = useRoute();
const page = ref(1);
const details = reactive<any>({ data: {} });
watch(
() => router,
(val) => {
details.data = detailsData.find((_item, index) => index === parseInt(val.currentRoute.value.query.id as string));
() => route.query.id,
(id) => {
const index = Number(id);
if (!Number.isNaN(index)) {
details.data = detailsData.find((_item, i) => i === index) ?? {};
}
},
{ deep: true, immediate: true },
{ immediate: true },
);
</script>