Vue3 多端统一开发框架 - 快速构建跨平台应用,省时省力"。
- 现代技术栈:Vite 5.2 + Vue 3.5 + TypeScript 5.3 + Pinia 2.1
- 多端适配:一套代码,同时支持 H5 和 PC 端
- UI 组件库:Element Plus 2.7 (PC 端) + Vant 4.9 (移动端)
- 自动导入:组件和 API 自动导入,无需手动 import
- 主题系统:支持多主题切换,内置暗黑模式
- 权限控制:完整的登录认证和权限管理
- 状态管理:基于 Pinia 的状态管理,支持持久化
- 请求封装:Axios 请求封装,支持常规请求和流式请求
- 安全防护:内置请求签名和设备指纹识别
- 图标系统:SVG 图标和 Iconfront 图标组件封装
- 自定义 Hooks:常用功能封装为 Hooks
- 构建优化:自动代码分割、压缩和优化
- Cursor 规则:提供详细的项目规范文档,支持 Cursor AI 开发助手智能理解项目架构和开发规范
src/ ├── apis/ # API 接口 │ ├── modules/ # 按模块组织的API │ │ ├── user.ts # 用户相关API │ │ └── script.ts # 脚本相关API │ ├── types/ # API类型定义 │ └── index.ts # 统一导出 ├── assets/ # 静态资源 │ └── icons/ # 图标资源 ├── components/ # 公共组件 │ ├── SvgIcon/ # SVG图标组件 │ ├── InconFent/ # Iconfront图标组件 │ └── ThemeSwitch/ # 主题切换组件 ├── hooks/ # 自定义Hooks │ ├── useNProgress.ts # 进度条Hook │ └── useThemeTransition.ts # 主题切换过渡动画Hook ├── router/ # 路由配置 │ ├── home/ # 首页路由模块 │ ├── login/ # 登录路由模块 │ └── index.ts # 路由主文件 ├── stores/ # 状态管理 │ ├── theme.ts # 主题状态 │ ├── token.ts # Token管理 │ ├── user.ts # 用户信息 │ └── login.ts # 登录状态 ├── styles/ # 全局样式 │ ├── themes/ # 主题相关 │ │ └── variables.scss # 主题变量定义 │ ├── transitions/ # 过渡动画 │ │ └── theme.scss # 主题切换动画 │ └── mixins/ # 样式混入 ├── utils/ # 工具函数 │ ├── http/ # HTTP请求封装 │ ├── device.ts # 设备信息 │ └── index.ts # 工具统一导出 └── views/ # 页面组件 ├── login/ # 登录页面 ├── home/ # 首页 ├── settings/ # 设置页面 └── error/ # 错误页面 项目内置了七大核心规范,确保开发过程中的一致性和高质量代码输出:
项目基于现代前端技术栈构建,核心技术包括:
- Vue 3.5.x、TypeScript 5.3.x、Vite 5.2.x
- 状态管理:Pinia 2.1.x (含持久化)
- UI 组件库:Element Plus 2.7.x (PC 端)、Vant 4.9.x (移动端)
- 工具库:Axios、Lodash-es、TypeScript-MD5 等
项目采用模块化、分层次的目录结构,主要包括:
apis: API 接口层,按业务模块组织components: 全局公共组件hooks: 自定义 Hooksrouter: 路由配置,模块化管理stores: Pinia 状态管理styles: 全局样式和主题系统utils: 工具函数views: 页面组件
API 请求采用统一封装,包含权限认证、请求签名、错误处理等机制:
- 请求流程:参数准备 → 权限验证 → 请求签名 → 设备指纹 → 请求发送 → 响应处理
- API 定义规范:按业务模块分组、类型严格定义、命名规范统一
- 支持常规请求和流式请求两种方式
采用 JSDoc 风格的注释规范:
- 函数和方法:包含描述、参数说明、返回值说明、示例
- 类和接口:包含描述、属性说明
- 复杂业务逻辑:分步骤说明
- 特殊处理或边界条件:详细说明原因和处理方式
项目封装了多个核心 Hooks 和工具方法:
- 用户认证与权限:
useUserStore、useTokenStore - 主题管理:
useThemeStore、useThemeTransition - HTTP 请求:标准请求和流式请求封装
- 进度条:
useNProgress - 安全工具:设备指纹、请求签名
添加新功能模块的标准流程:
- 创建 API 模块:定义接口、参数和响应类型
- 创建视图组件:页面组件及私有组件
- 创建路由配置:定义路由、权限和元信息
- 创建状态管理:定义 State、Getters 和 Actions
基于 Airbnb JavaScript 风格指南,结合 Vue 官方风格指南:
- 变量与引用:优先使用 const,需要重新赋值时使用 let
- 函数:优先使用箭头函数,使用参数默认值
- 组件:使用 PascalCase 命名,详细定义 Props
- TypeScript:严格类型定义,提高代码可读性
- 命名规范:描述性命名,遵循命名约定
pnpm install# PC端开发 pnpm dev # H5端开发 pnpm dev:h5# PC端生产环境 pnpm pro # H5端生产环境 pnpm proH5# PC端构建(开发环境) pnpm build:dev # H5端构建(开发环境) pnpm build:dev:h5 # PC端构建(生产环境) pnpm build:prod # H5端构建(生产环境) pnpm build:prod:h5项目内置了强大的主题系统,支持多主题切换和平滑过渡动画。
主题变量定义在 src/styles/themes/variables.scss 中:
// 基础变量定义 $themes: ( light: ( // 亮色主题 --primary-color: #409eff, --bg-color: #ffffff, --text-primary: #303133, // 更多变量... ), dark: ( // 暗色主题 --primary-color: #409eff, --bg-color: #141414, --text-primary: #ffffff, // 更多变量... ), // 可以添加更多自定义主题... red: ( --primary-color: #f56c6c, // 红色主题的其他变量... ), );在页面中引入 ThemeSwitch 组件:
<template> <div class="container"> <!-- 主题切换按钮 --> <ThemeSwitch /> <!-- 页面内容 --> </div> </template> <script setup lang="ts"> import ThemeSwitch from "@/components/ThemeSwitch/index.vue"; import { useThemeStore } from "@/stores/theme"; // 获取主题状态管理实例 const themeStore = useThemeStore(); // 在组件挂载时初始化主题 onMounted(() => { themeStore.initTheme(); }); </script>在父容器中应用主题变量和过渡动画:
<template> <div class="app-container"> <!-- 页面内容 --> </div> </template> <style lang="scss"> // 导入主题过渡动画样式 @import "@/styles/transitions/theme.scss"; .app-container { // 使用主题混入 @include useTheme; // 使用主题变量 background: var(--bg-color); color: var(--text-primary); } </style>通过 useThemeStore 可以手动控制主题:
import { useThemeStore } from "@/stores/theme"; const themeStore = useThemeStore(); // 切换到暗色主题 themeStore.setTheme("dark"); // 切换到亮色主题 themeStore.setTheme("light"); // 切换到自定义主题 themeStore.setTheme("red"); // 切换暗黑/亮色模式 themeStore.toggleDarkMode(); // 获取当前主题 console.log(themeStore.currentTheme); // 检查是否为暗黑模式 console.log(themeStore.isDarkMode);项目使用 View Transitions API 实现平滑的主题切换动画,通过 useThemeTransition Hook 封装:
import { useThemeTransition } from "@/hooks/useThemeTransition"; const { handleThemeChange } = useThemeTransition(); // 切换主题并应用过渡动画 // 参数1: 目标主题 // 参数2: 触发元素 (可选,用于动画起点) // 参数3: 动画选项 (可选) handleThemeChange("dark", triggerElement, { duration: 500, easing: "cubic-bezier(0.4, 0, 0.2, 1)", });要添加新主题,只需在 src/styles/themes/variables.scss 中的 $themes 变量中添加新的主题配置:
$themes: ( light: ( /* 亮色主题变量 */ ), dark: ( /* 暗色主题变量 */ ), // 添加新主题 purple: ( --primary-color: #722ed1, --bg-color: #f9f0ff, --text-primary: #333333, // 其他变量... ), );在 src/apis/modules 目录下创建模块文件:
// src/apis/modules/product.ts import axiosInstance from "@/utils/http"; import type { RequestData } from "../types/common"; interface ProductParams { id: string; name: string; } // 获取产品列表 export const getProducts = (params: ProductParams) => { const requestData: RequestData<ProductParams> = { parameter: params, }; return axiosInstance.post("/api/products", requestData); };在 src/apis/index.ts 中导出新模块:
// src/apis/index.ts export * as userApi from "./modules/user"; export * as scriptApi from "./modules/script"; export * as productApi from "./modules/product"; // 新增在组件中使用 API:
// 方式1: 直接导入 import { getProducts } from "@/apis/modules/product"; // 方式2: 通过统一导出使用 import { productApi } from "@/apis"; const fetchData = async () => { try { // 方式1 const res1 = await getProducts({ id: "1", name: "test" }); // 方式2 const res2 = await productApi.getProducts({ id: "1", name: "test" }); console.log(res1, res2); } catch (error) { console.error(error); } };在 src/stores 目录下创建新的 store:
// src/stores/product.ts import { defineStore } from "pinia"; interface Product { id: string; name: string; price: number; } interface ProductState { products: Product[]; loading: boolean; } export const useProductStore = defineStore("productStore", { state: (): ProductState => ({ products: [], loading: false, }), getters: { totalProducts: (state) => state.products.length, totalPrice: (state) => state.products.reduce((sum, product) => sum + product.price, 0), }, actions: { setProducts(products: Product[]) { this.products = products; }, addProduct(product: Product) { this.products.push(product); }, async fetchProducts() { this.loading = true; try { // 使用API获取数据 const { data } = await productApi.getProducts({}); this.setProducts(data); } catch (error) { console.error(error); } finally { this.loading = false; } }, }, // 持久化配置 persist: true, });在组件中使用 store:
import { useProductStore } from "@/stores/product"; // 在setup中使用 const productStore = useProductStore(); // 读取状态 console.log(productStore.products); console.log(productStore.totalProducts); // getter // 修改状态 productStore.setProducts([...]); productStore.addProduct({ id: "1", name: "Product 1", price: 99 }); // 调用异步action productStore.fetchProducts();在 src/views 目录下创建页面组件:
<!-- src/views/product/index.vue --> <template> <div class="product-container"> <h1>产品列表</h1> <div v-if="loading">加载中...</div> <ul v-else> <li v-for="item in products" :key="item.id"> {{ item.name }} - {{ item.price }} </li> </ul> </div> </template> <script setup lang="ts"> import { onMounted, ref } from "vue"; import { productApi } from "@/apis"; const products = ref([]); const loading = ref(false); const fetchProducts = async () => { loading.value = true; try { const { data } = await productApi.getProducts({}); products.value = data; } catch (error) { console.error(error); } finally { loading.value = false; } }; onMounted(() => { fetchProducts(); }); </script>在 src/router 目录下创建路由模块:
// src/router/product/productRouter.ts const productRouter = [ { path: "/products", name: "Products", component: () => import("@/views/product/index.vue"), meta: { requiresAuth: true, title: "产品列表", }, }, { path: "/products/:id", name: "ProductDetail", component: () => import("@/views/product/detail.vue"), meta: { requiresAuth: true, title: "产品详情", }, }, ]; export default productRouter;在主路由文件中引入并注册路由模块:
// src/router/index.ts import { createRouter, createWebHistory } from "vue-router"; import homeRouter from "./home/homeRouter"; import loginRouter from "./login/loginRouter"; import productRouter from "./product/productRouter"; // 新增 const routes = [ // 其他路由... ...homeRouter, ...loginRouter, ...productRouter, // 注册产品路由 ]; const router = createRouter({ history: createWebHistory(), routes, }); export default router;项目支持多环境配置,可以在根目录创建不同的环境变量文件:
.env- 所有环境通用的变量.env.development- 开发环境变量.env.production- 生产环境变量.env.developmentH5- H5 开发环境变量.env.productionH5- H5 生产环境变量
示例配置:
# .env.development VITE_NODE_ENV=development VITE_API_BASE_URL=/api VITE_TEST_URL=http://dev-api.example.com VITE_PRO_URL=http://api.example.com在代码中使用环境变量:
// 使用环境变量 const apiUrl = import.meta.env.VITE_API_BASE_URL; console.log(`当前环境: ${import.meta.env.VITE_NODE_ENV}`);项目的 vite.config.ts 提供了丰富的功能配置:
无需手动导入 Vue API 和 UI 组件库:
// 自动导入Vue API、Element Plus和Vant组件 AutoImport({ imports: ["vue"], // 自动导入Vue API resolvers: [ElementPlusResolver(), VantResolver()], }); // 自动导入组件 Components({ resolvers: [ElementPlusResolver(), VantResolver()], });自动为每个 SCSS 文件注入全局变量和混入:
css: { preprocessorOptions: { scss: { additionalData: ` @import "@/styles/themes/variables.scss"; @import "@/styles/mixins/theme.scss"; @import "@/styles/mixins/common.scss"; `; } } }自动加载和优化 SVG 图标:
createSvgIconsPlugin({ iconDirs: [path.join(__dirname, "src/assets/icons/svg")], symbolId: "icon-[name]", });智能代码分割和压缩:
build: { minify: "terser", rollupOptions: { output: { manualChunks(id) { // 不同依赖库提取到独立的文件 if (id.includes('node_modules')) { return id.toString().split('node_modules/')[1].split('/')[0].toString(); } } } } }简化导入路径:
resolve: { alias: { '@': path.resolve(__dirname, 'src'), '@c': path.resolve(__dirname, 'src/components'), '@u': path.resolve(__dirname, 'src/utils') } }- 组件命名: 使用 PascalCase 命名组件
- 目录结构: 复杂组件使用目录包含多个文件
- Props 类型: 始终为 props 定义类型
- 样式隔离: 使用 scoped 或 module 确保样式隔离
<!-- 推荐的组件结构 --> <template> <div class="product-card"> <!-- 组件内容 --> </div> </template> <script setup lang="ts"> // Props定义 interface Props { product: { id: string; name: string; price: number; }; showDetails?: boolean; } // 默认值 const props = withDefaults(defineProps<Props>(), { showDetails: false, }); // 事件 const emit = defineEmits<{ (e: "select", id: string): void; (e: "delete", id: string): void; }>(); </script> <style lang="scss" scoped> .product-card { @include flex(column, flex-start, stretch); // 使用主题变量 color: var(--text-primary); background-color: var(--bg-color); } </style>- 模块化: 按业务领域组织 API
- 类型安全: 为请求和响应定义类型
- 注释: 使用 JSDoc 注释 API 功能和参数
- 错误处理: 统一处理 API 错误
- Store 拆分: 按功能模块拆分 Store
- 类型定义: 为 State、Getters 和 Actions 定义类型
- 持久化: 根据需要配置持久化选项
- Action 复用: 在 Action 中复用其他 Action
- ZsTs119
- Email: zsts@foxmail.com
- GitHub: https://github.com/ZsTs119