123
AI 可读版:Vue3 + TypeScript 商业化前端项目从 0 到完整结构搭建规范
这份文档是给 AI 或开发者直接执行用的项目规范。
技术栈默认:Vue3 + TypeScript + Vite + Vue Router + Pinia + Element Plus。
目标:从 0 创建一个结构清晰、适合商业项目长期维护的前端项目。
0. AI 执行总原则
AI 在根据本文档生成项目时,必须遵守以下原则:
1. 不要把所有逻辑塞进 Pinia。
2. Pinia 只放全局状态,例如用户信息、token、主题、语言、全局布局状态。
3. 接口请求必须通过统一 request 层,不允许页面组件里直接 fetch 或 axios。
4. api 目录只负责定义业务接口函数,不处理页面逻辑。
5. 页面 pages 只负责页面组合,不承载大量业务工具函数。
6. 复用逻辑放 composables。
7. 通用工具函数放 utils。
8. 常量放 constants。
9. 类型定义放 types。
10. 国际化资源不要放 stores,应放 i18n 或 locales。
11. 表单规则、业务校验规则不要大量写在页面里,优先放 validators 或 schemas。
12. 路由守卫单独放 router/guard.ts。
13. 布局壳子放 layouts。
14. 样式要有统一入口,不要散乱写全局样式。
15. 项目必须支持开发、测试、生产环境变量。
16. 商业项目上线前要考虑错误处理、权限、登录失效、打包、部署、日志、README。
1. 项目创建
1.1 创建项目
npm create vite@latest frontend -- --template vue-ts
cd frontend
npm install
如果已经有后端目录,可以使用这种结构:
rental-account-platform/
backend/
frontend/
docker/
nginx/
docker-compose.yml
README.md
其中前端项目放在:
rental-account-platform/frontend/
2. 安装核心依赖
2.1 运行依赖
npm install vue-router pinia element-plus @element-plus/icons-vue axios
说明:
vue-router 路由
pinia 状态管理
element-plus UI 组件库
@element-plus/icons-vue Element Plus 图标
axios 请求库,也可以不用 axios,改用 fetch 自封装
尽量使用axios,如果决定不用 axios,也可以不安装 axios,但必须自己封装 request。
2.2 开发依赖
npm install -D eslint prettier eslint-plugin-vue @typescript-eslint/parser @typescript-eslint/eslint-plugin
npm install -D vite-plugin-vue-devtools
npm install -D unplugin-auto-import unplugin-vue-components
npm install -D unplugin-element-plus
npm install -D vitest jsdom @vue/test-utils
npm install -D playwright
说明:
eslint 代码检查
prettier 代码格式化
vite-plugin-vue-devtools Vue 开发工具
unplugin-auto-import 自动导入 Vue API、Element Plus API
unplugin-vue-components 自动导入组件
unplugin-element-plus Element Plus 按需样式
vitest 单元测试
playwright E2E 测试
3. 推荐完整目录结构
AI 创建项目时,优先按下面结构生成:
frontend/
public/
favicon.ico
src/
api/
order.ts
product.ts
system.ts
user.ts
request/
index.ts
error.ts
types.ts
assets/
images/
svg/
styles/
variables.css
reset.css
element.css
common.css
components/
common/
EmptyState.vue
PageLoading.vue
ConfirmButton.vue
layout/
AppHeader.vue
AppDrawer.vue
AppFooter.vue
composables/
useGo.ts
useMessage.ts
useLoading.ts
usePagination.ts
useConfirm.ts
config/
index.ts
env.ts
constants/
route.ts
storage.ts
tips.ts
status.ts
i18n/
index.ts
messages/
zh.ts
en.ts
ru.ts
layouts/
MainLayout.vue
AdminLayout.vue
BlankLayout.vue
pages/
HomePage.vue
LoginPage.vue
RegisterPage.vue
ProductPage.vue
ProductDetailPage.vue
OrdersPage.vue
OrderSuccessPage.vue
DeliveryPage.vue
admin/
AdminHomePage.vue
AdminOrdersPage.vue
AdminProductEditPage.vue
AdminCategoriesPage.vue
router/
index.ts
routes.ts
guard.ts
stores/
app.ts
auth.ts
user.ts
types/
api.ts
order.ts
product.ts
user.ts
common.ts
utils/
storage.ts
format.ts
price.ts
date.ts
validate.ts
validators/
auth.ts
order.ts
product.ts
App.vue
main.ts
style.css
tests/
unit/
utils/
price.test.ts
e2e/
login.spec.ts
order.spec.ts
.env
.env.development
.env.staging
.env.production
.editorconfig
.gitignore
.prettierignore
.prettierrc
eslint.config.js
index.html
package.json
package-lock.json
tsconfig.json
tsconfig.app.json
tsconfig.node.json
vite.config.ts
README.md
Dockerfile
4. 每个目录应该放什么
4.1 src/api
作用:放业务接口函数。
规则:
1. api 目录只写接口函数。
2. api 文件不直接处理 UI。
3. api 文件不直接弹窗。
4. api 文件不直接操作路由。
5. api 文件只负责调用 request 并返回结果。
示例:
// src/api/product.ts
import request from '@/request'
import type { Product, Category } from '@/types/product'
import type { PageResult } from '@/types/api'
export function getProducts(params?: {
page?: number
pageSize?: number
keyword?: string
}) {
return request.get<PageResult<Product>>('/accounts', { params })
}
export function getProduct(id: string) {
return request.get<Product>(`/accounts/${id}`)
}
export function getCategories() {
return request.get<Category[]>('/categories')
}
export function createProduct(payload: Partial<Product>) {
return request.post<Product>('/admin/accounts', payload)
}
export function updateProduct(id: string, payload: Partial<Product>) {
return request.patch<Product>(`/admin/accounts/${id}`, payload)
}
export function deleteProduct(id: string) {
return request.delete<void>(`/admin/accounts/${id}`)
}
4.2 src/request
作用:统一请求层。
必须负责:
1. baseURL
2. token 注入
3. 请求超时
4. 请求错误处理
5. 401 登录失效处理
6. 403 无权限处理
7. 500 服务错误处理
8. JSON 解析
9. FormData 兼容
10. 响应数据格式统一
axios 版本推荐
// src/request/index.ts
import axios, { AxiosError, type AxiosRequestConfig } from 'axios'
import { useAuthStore } from '@/stores/auth'
import { handleRequestError } from './error'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 15000,
headers: {
'Content-Type': 'application/json',
},
})
request.interceptors.request.use((config) => {
const auth = useAuthStore()
if (auth.token) {
config.headers.Authorization = `Bearer ${auth.token}`
}
return config
})
request.interceptors.response.use(
(response) => {
const data = response.data
// 后端如果统一返回 { code, message, data }
if (data && typeof data === 'object' && 'code' in data) {
if (data.code !== 0 && data.code !== 200) {
return Promise.reject(new Error(data.message || '请求失败'))
}
return data.data
}
// 后端如果直接返回业务数据
return data?.data ?? data
},
(error: AxiosError) => {
return Promise.reject(handleRequestError(error))
},
)
export default request
// src/request/error.ts
import type { AxiosError } from 'axios'
import { useAuthStore } from '@/stores/auth'
import router from '@/router'
export function handleRequestError(error: AxiosError | unknown) {
if (!isAxiosError(error)) {
return error instanceof Error ? error : new Error('未知错误')
}
const status = error.response?.status
const data = error.response?.data as any
if (status === 401) {
const auth = useAuthStore()
auth.logout()
router.replace('/login')
return new Error('登录已失效,请重新登录')
}
if (status === 403) {
return new Error('没有权限访问')
}
if (status === 404) {
return new Error('请求资源不存在')
}
if (status && status >= 500) {
return new Error('服务器异常,请稍后重试')
}
if (error.code === 'ECONNABORTED') {
return new Error('请求超时,请检查网络')
}
return new Error(data?.message || error.message || '请求失败')
}
function isAxiosError(error: unknown): error is AxiosError {
return !!error && typeof error === 'object' && 'isAxiosError' in error
}
fetch 版本推荐
如果不想使用 axios,可以使用 fetch,但必须补齐商业项目常用能力。
// src/request/index.ts
import { useAuthStore } from '@/stores/auth'
import { handleHttpError } from './error'
type RequestOptions = RequestInit & {
timeout?: number
params?: Record<string, string | number | boolean | undefined | null>
}
const BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'
function buildUrl(url: string, params?: RequestOptions['params']) {
const fullUrl = url.startsWith('http') ? url : `${BASE_URL}${url}`
if (!params) return fullUrl
const searchParams = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
searchParams.append(key, String(value))
}
})
const query = searchParams.toString()
return query ? `${fullUrl}?${query}` : fullUrl
}
async function request<T>(url: string, options: RequestOptions = {}): Promise<T> {
const auth = useAuthStore()
const controller = new AbortController()
const timeout = options.timeout ?? 15000
const timer = window.setTimeout(() => {
controller.abort()
}, timeout)
const headers = new Headers(options.headers)
if (!(options.body instanceof FormData)) {
headers.set('Content-Type', 'application/json')
}
if (auth.token) {
headers.set('Authorization', `Bearer ${auth.token}`)
}
try {
const res = await fetch(buildUrl(url, options.params), {
...options,
headers,
signal: controller.signal,
})
const contentType = res.headers.get('content-type')
const isJson = contentType?.includes('application/json')
const data = isJson ? await res.json().catch(() => null) : await res.text()
if (!res.ok) {
throw handleHttpError(res.status, data)
}
if (data && typeof data === 'object' && 'code' in data) {
if (data.code !== 0 && data.code !== 200) {
throw new Error(data.message || '请求失败')
}
return data.data as T
}
if (data && typeof data === 'object' && 'data' in data) {
return data.data as T
}
return data as T
} catch (error: any) {
if (error?.name === 'AbortError') {
throw new Error('请求超时,请稍后重试')
}
throw error instanceof Error ? error : new Error('请求失败')
} finally {
window.clearTimeout(timer)
}
}
export default {
get<T>(url: string, options?: RequestOptions) {
return request<T>(url, {
...options,
method: 'GET',
})
},
post<T>(url: string, body?: unknown, options?: RequestOptions) {
return request<T>(url, {
...options,
method: 'POST',
body: body instanceof FormData ? body : JSON.stringify(body ?? {}),
})
},
patch<T>(url: string, body?: unknown, options?: RequestOptions) {
return request<T>(url, {
...options,
method: 'PATCH',
body: body instanceof FormData ? body : JSON.stringify(body ?? {}),
})
},
put<T>(url: string, body?: unknown, options?: RequestOptions) {
return request<T>(url, {
...options,
method: 'PUT',
body: body instanceof FormData ? body : JSON.stringify(body ?? {}),
})
},
delete<T>(url: string, options?: RequestOptions) {
return request<T>(url, {
...options,
method: 'DELETE',
})
},
}
// src/request/error.ts
import router from '@/router'
import { useAuthStore } from '@/stores/auth'
export function handleHttpError(status: number, data: any) {
if (status === 401) {
const auth = useAuthStore()
auth.logout()
router.replace('/login')
return new Error('登录已失效,请重新登录')
}
if (status === 403) {
return new Error('没有权限访问')
}
if (status === 404) {
return new Error('请求资源不存在')
}
if (status >= 500) {
return new Error('服务器异常,请稍后重试')
}
return new Error(data?.message || data?.error || `请求失败:${status}`)
}
4.3 src/stores
作用:全局状态管理。
可以放:
1. token
2. 用户信息
3. 当前语言
4. 主题模式
5. 全局布局状态
6. 登录状态
7. 权限信息
不应该放:
1. 接口请求封装
2. 静态字典
3. 大量 i18n 文本
4. 页面内部状态
5. 临时弹窗状态
6. 表单数据
7. 纯工具函数
推荐:
src/stores/
app.ts 全局 UI 状态,例如 dark、locale、drawer
auth.ts token、登录、退出
user.ts 当前用户资料、角色、权限
示例:
// src/stores/auth.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { TOKEN_KEY } from '@/constants/storage'
export const useAuthStore = defineStore('auth', () => {
const token = ref(localStorage.getItem(TOKEN_KEY) || '')
function setToken(value: string) {
token.value = value
localStorage.setItem(TOKEN_KEY, value)
}
function logout() {
token.value = ''
localStorage.removeItem(TOKEN_KEY)
}
return {
token,
setToken,
logout,
}
})
4.4 src/router
作用:路由配置和路由守卫。
推荐结构:
src/router/
index.ts
routes.ts
guard.ts
// src/router/routes.ts
import type { RouteRecordRaw } from 'vue-router'
export const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('@/layouts/MainLayout.vue'),
children: [
{
path: '',
name: 'home',
component: () => import('@/pages/HomePage.vue'),
meta: {
title: '首页',
public: true,
},
},
{
path: 'products',
name: 'products',
component: () => import('@/pages/ProductPage.vue'),
meta: {
title: '产品',
public: true,
},
},
{
path: 'orders',
name: 'orders',
component: () => import('@/pages/OrdersPage.vue'),
meta: {
title: '订单',
requiresAuth: true,
},
},
],
},
{
path: '/login',
component: () => import('@/layouts/BlankLayout.vue'),
children: [
{
path: '',
name: 'login',
component: () => import('@/pages/LoginPage.vue'),
meta: {
title: '登录',
public: true,
},
},
],
},
{
path: '/admin',
component: () => import('@/layouts/AdminLayout.vue'),
meta: {
requiresAuth: true,
roles: ['admin'],
},
children: [
{
path: '',
name: 'admin-home',
component: () => import('@/pages/admin/AdminHomePage.vue'),
meta: {
title: '管理后台',
},
},
],
},
]
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from './routes'
import { setupRouterGuard } from './guard'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
})
setupRouterGuard(router)
export default router
// src/router/guard.ts
import type { Router } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
export function setupRouterGuard(router: Router) {
router.beforeEach((to) => {
const auth = useAuthStore()
if (typeof to.meta.title === 'string') {
document.title = to.meta.title
}
if (to.meta.requiresAuth && !auth.token) {
return {
path: '/login',
query: {
redirect: to.fullPath,
},
}
}
if (to.path === '/login' && auth.token) {
return '/'
}
return true
})
}
4.5 src/layouts
作用:页面布局壳子。
推荐:
MainLayout.vue 普通前台页面布局,例如 Header + Drawer + 内容区
AdminLayout.vue 管理后台布局,例如侧边栏 + 顶栏 + 内容区
BlankLayout.vue 空白布局,例如登录页、注册页
示例:
<!-- src/layouts/MainLayout.vue -->
<template>
<div class="main-layout">
<AppHeader />
<AppDrawer />
<main class="main-layout__content">
<RouterView />
</main>
</div>
</template>
<script setup lang="ts">
import AppHeader from '@/components/layout/AppHeader.vue'
import AppDrawer from '@/components/layout/AppDrawer.vue'
</script>
4.6 src/i18n
作用:国际化文本和语言切换。
不要把语言包放在 stores 里。
推荐:
src/i18n/
index.ts
messages/
zh.ts
en.ts
ru.ts
示例:
// src/i18n/messages/zh.ts
export default {
common: {
confirm: '确认',
cancel: '取消',
save: '保存',
delete: '删除',
},
page: {
home: '首页',
product: '产品',
order: '订单',
},
}
// src/i18n/index.ts
import zh from './messages/zh'
import en from './messages/en'
import ru from './messages/ru'
export const messages = {
zh,
en,
ru,
}
export type Locale = keyof typeof messages
export function t(locale: Locale, path: string): string {
const keys = path.split('.')
let current: any = messages[locale]
for (const key of keys) {
current = current?.[key]
}
return typeof current === 'string' ? current : path
}
Pinia 只保存当前语言:
// src/stores/app.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { Locale } from '@/i18n'
export const useAppStore = defineStore('app', () => {
const locale = ref<Locale>((localStorage.getItem('locale') as Locale) || 'zh')
const dark = ref(localStorage.getItem('theme') === 'dark')
const drawer = ref(false)
function setLocale(value: Locale) {
locale.value = value
localStorage.setItem('locale', value)
}
function setDark(value: boolean) {
dark.value = value
localStorage.setItem('theme', value ? 'dark' : 'light')
}
function toggleDrawer() {
drawer.value = !drawer.value
}
return {
locale,
dark,
drawer,
setLocale,
setDark,
toggleDrawer,
}
})
4.7 src/types
作用:集中管理类型。
推荐:
types/api.ts 接口公共类型
types/user.ts 用户类型
types/product.ts 产品类型
types/order.ts 订单类型
types/common.ts 通用类型
示例:
// src/types/api.ts
export interface ApiResponse<T> {
code: number
message: string
data: T
}
export interface PageResult<T> {
list: T[]
total: number
page: number
pageSize: number
}
// src/types/product.ts
export interface Product {
id: string
name: string
price: number
status: 'available' | 'rented' | 'disabled'
categoryId: number
description?: string
image?: string
}
export interface Category {
id: number
name: string
}
// src/types/order.ts
export interface Order {
orderNo: string
productId: string
productName: string
status: 'pending' | 'paid' | 'delivering' | 'completed' | 'cancelled'
createdAt: string
}
4.8 src/composables
作用:复用组合式逻辑。
适合放:
1. usePagination
2. useLoading
3. useConfirm
4. useMessage
5. useGo
6. useTable
7. useUpload
示例:
// src/composables/useLoading.ts
import { ref } from 'vue'
export function useLoading(initialValue = false) {
const loading = ref(initialValue)
async function withLoading<T>(task: () => Promise<T>) {
loading.value = true
try {
return await task()
} finally {
loading.value = false
}
}
return {
loading,
withLoading,
}
}
// src/composables/useMessage.ts
import { ElMessage, ElMessageBox } from 'element-plus'
export function useMessage() {
function success(message: string) {
ElMessage.success(message)
}
function error(message: string) {
ElMessage.error(message)
}
function warning(message: string) {
ElMessage.warning(message)
}
async function confirm(message: string, title = '提示') {
await ElMessageBox.confirm(message, title, {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消',
})
}
return {
success,
error,
warning,
confirm,
}
}
4.9 src/utils
作用:纯工具函数。
适合放:
1. 日期格式化
2. 金额格式化
3. 字符串处理
4. localStorage/sessionStorage 包装
5. 参数处理
6. 数据转换
7. 正则校验
规则:
1. utils 不依赖页面。
2. utils 不依赖组件。
3. utils 不直接操作 UI。
4. utils 尽量写成纯函数。
示例:
// src/utils/price.ts
export function formatPrice(value: number) {
return `¥${value.toFixed(2)}`
}
// src/utils/storage.ts
export function getStorage<T>(key: string, fallback: T): T {
const value = localStorage.getItem(key)
if (!value) return fallback
try {
return JSON.parse(value) as T
} catch {
return fallback
}
}
export function setStorage<T>(key: string, value: T) {
localStorage.setItem(key, JSON.stringify(value))
}
export function removeStorage(key: string) {
localStorage.removeItem(key)
}
4.10 src/constants
作用:常量集中管理。
推荐:
constants/storage.ts localStorage key
constants/route.ts 路由名称
constants/status.ts 状态枚举
constants/tips.ts 固定提示语
示例:
// src/constants/storage.ts
export const TOKEN_KEY = 'APP_TOKEN'
export const USER_INFO_KEY = 'APP_USER_INFO'
export const LOCALE_KEY = 'APP_LOCALE'
export const THEME_KEY = 'APP_THEME'
// src/constants/status.ts
export const ORDER_STATUS = {
pending: '待处理',
paid: '已支付',
delivering: '配送中',
completed: '已完成',
cancelled: '已取消',
} as const
4.11 src/validators
作用:表单校验和业务校验规则。
推荐:
validators/auth.ts
validators/order.ts
validators/product.ts
示例:
// src/validators/auth.ts
import type { FormRules } from 'element-plus'
export const loginRules: FormRules = {
username: [
{
required: true,
message: '请输入账号',
trigger: 'blur',
},
],
password: [
{
required: true,
message: '请输入密码',
trigger: 'blur',
},
{
min: 6,
message: '密码至少 6 位',
trigger: 'blur',
},
],
}
4.12 src/components
作用:可复用组件。
推荐分层:
components/common/ 完全通用组件
components/layout/ 布局相关组件
components/business/ 业务组件,可选
规则:
1. 通用组件不要依赖具体业务接口。
2. 业务组件可以依赖业务类型,但尽量不要直接请求接口。
3. 大组件要拆分,不要一个 Vue 文件几千行。
4.13 src/pages
作用:页面级组件。
规则:
1. pages 负责页面组织。
2. pages 可以调用 api。
3. pages 可以使用 composables。
4. pages 不应该放大量工具函数。
5. pages 不应该定义大量全局类型。
6. pages 不应该直接操作 localStorage,应该通过 utils 或 store。
页面推荐结构:
<template>
<section class="page">
页面内容
</section>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getProducts } from '@/api/product'
import type { Product } from '@/types/product'
import { useLoading } from '@/composables/useLoading'
import { useMessage } from '@/composables/useMessage'
const products = ref<Product[]>([])
const { loading, withLoading } = useLoading()
const message = useMessage()
async function loadProducts() {
await withLoading(async () => {
try {
const result = await getProducts()
products.value = result.list
} catch (error: any) {
message.error(error.message || '加载失败')
}
})
}
onMounted(() => {
loadProducts()
})
</script>
<style scoped>
.page {
padding: 16px;
}
</style>
5. 环境变量规范
必须创建:
.env
.env.development
.env.staging
.env.production
示例:
# .env
VITE_APP_NAME=Rental Account Platform
# .env.development
VITE_API_BASE_URL=/api
VITE_APP_ENV=development
# .env.staging
VITE_API_BASE_URL=https://staging-api.example.com
VITE_APP_ENV=staging
# .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_APP_ENV=production
读取方式:
const baseURL = import.meta.env.VITE_API_BASE_URL
注意:
1. Vite 前端环境变量必须以 VITE_ 开头。
2. 不要把密钥、数据库密码、服务端私钥放前端 .env。
3. 前端 .env 打包后是可见的,不能存真正的秘密。
6. main.ts 初始化规范
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import './assets/styles/reset.css'
import './assets/styles/variables.css'
import './assets/styles/common.css'
import './style.css'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')
7. App.vue 规范
<template>
<RouterView />
</template>
<script setup lang="ts"></script>
不要把所有 Header、Drawer、Footer 都直接塞进 App.vue。
应该交给 layouts 控制。
8. vite.config.ts 推荐配置
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueDevTools from 'vite-plugin-vue-devtools'
export default defineConfig({
plugins: [vue(), VueDevTools()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
})
如果后端接口本身就是 /api 开头,rewrite 根据实际情况调整。
9. tsconfig 规范
确保支持 @ 路径别名。
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
10. ESLint / Prettier 规范
10.1 .prettierrc
{
"semi": false,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "all"
}
10.2 .editorconfig
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
10.3 package.json scripts
{
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint .",
"format": "prettier --write .",
"test:unit": "vitest",
"test:e2e": "playwright test"
}
}
11. Pinia 使用边界
11.1 应该放 Pinia 的内容
token
用户信息
用户权限
语言
主题
全局菜单状态
全局布局状态
购物车这类跨页面长期状态
11.2 不应该放 Pinia 的内容
接口请求函数
纯工具函数
静态文本
静态配置
只在一个页面用的表单数据
只在一个组件用的 loading
临时弹窗开关
11.3 判断标准
如果刷新页面后不需要恢复,且只有当前页面使用,不要放 Pinia。
如果多个页面都要用,或者需要长期保存,可以考虑 Pinia。
如果是静态内容,不要放 Pinia。
如果是工具能力,不要放 Pinia。
12. 接口层规范
12.1 正确调用链
页面组件
↓
api 业务接口函数
↓
request 统一请求层
↓
后端接口
12.2 禁止调用链
页面组件直接 fetch
页面组件直接 axios
页面组件直接拼 Authorization
页面组件重复写错误处理
页面组件重复写 baseURL
12.3 推荐写法
// 页面
const products = await getProducts({ page: 1, pageSize: 20 })
// api/product.ts
export function getProducts(params: ProductQuery) {
return request.get<PageResult<Product>>('/products', { params })
}
13. 错误处理规范
商业项目至少要处理:
400 参数错误
401 登录失效
403 没有权限
404 资源不存在
408 请求超时
500 服务器错误
网络断开
JSON 解析失败
接口返回结构不符合预期
原则:
1. request 层处理通用错误。
2. 页面层处理业务错误。
3. 401 在 request 层统一退出登录。
4. 表单校验错误在页面层或 validators 处理。
5. 不要每个页面重复写同样的 401 判断。
14. 权限设计规范
14.1 路由 meta
meta: {
title: '订单管理',
requiresAuth: true,
roles: ['admin'],
permissions: ['order:list']
}
14.2 权限判断位置
router/guard.ts 页面级权限
components 按钮级权限
stores/user.ts 保存角色和权限
14.3 按钮权限示例
// src/composables/usePermission.ts
import { useUserStore } from '@/stores/user'
export function usePermission() {
const userStore = useUserStore()
function hasPermission(permission: string) {
return userStore.permissions.includes(permission)
}
function hasRole(role: string) {
return userStore.roles.includes(role)
}
return {
hasPermission,
hasRole,
}
}
15. 表单规范
表单页面应包含:
1. 表单数据
2. 表单校验规则
3. submit loading
4. 防重复提交
5. 错误提示
6. 成功后跳转或刷新
表单校验规则优先放:
src/validators/
不要所有规则都散落在页面里。
16. 列表页规范
列表页通常包括:
1. keyword 搜索
2. page
3. pageSize
4. total
5. loading
6. table data
7. refresh
8. delete confirm
9. error message
可以抽出:
usePagination
useTable
useConfirm
17. 样式规范
推荐:
src/assets/styles/
reset.css
variables.css
element.css
common.css
src/style.css
17.1 variables.css
:root {
--app-primary-color: #409eff;
--app-bg-color: #f5f7fa;
--app-text-color: #303133;
--app-border-color: #dcdfe6;
--app-header-height: 56px;
--app-sidebar-width: 220px;
}
17.2 组件样式规则
1. 页面组件可以使用 scoped。
2. 全局样式放 assets/styles。
3. 不要在多个页面重复定义相同按钮、卡片、间距样式。
4. 不建议滥用 !important。
5. 样式命名保持清晰。
18. 图片和静态资源规范
src/assets/images/ 参与打包的图片
src/assets/svg/ SVG 图标
public/ 不经过打包处理的静态资源
使用规则:
1. 需要 import 的图片放 src/assets。
2. 需要通过固定 URL 访问的文件放 public。
3. 大图要压缩。
4. 不要把无用截图、临时图片放进项目。
19. 测试规范
商业项目不是必须一开始大量写测试,但建议至少覆盖:
1. utils 纯函数
2. 登录流程
3. 下单流程
4. 订单删除、编辑这类高风险操作
5. 权限跳转
推荐:
Vitest 单元测试
Playwright E2E 测试
示例:
// tests/unit/utils/price.test.ts
import { describe, expect, it } from 'vitest'
import { formatPrice } from '@/utils/price'
describe('formatPrice', () => {
it('formats price correctly', () => {
expect(formatPrice(12)).toBe('¥12.00')
})
})
20. 监控和日志规范
商业项目上线后建议接入:
1. JS 错误监控
2. 接口错误监控
3. 白屏监控
4. 性能监控
5. 用户操作关键路径日志
可以预留目录:
src/monitor/
error.ts
performance.ts
基础错误监听:
// src/monitor/error.ts
export function setupErrorMonitor() {
window.addEventListener('error', (event) => {
console.error('Global error:', event.error)
})
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason)
})
}
21. Docker 部署规范
前端 Dockerfile 示例:
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf 示例:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
22. README.md 必须包含什么
商业项目 README 至少包含:
1. 项目简介
2. 技术栈
3. 目录结构
4. 环境要求
5. 安装依赖
6. 本地启动
7. 环境变量说明
8. 打包构建
9. 部署方式
10. 接口代理说明
11. 代码规范
12. 常见问题
README 模板:
# Project Name
## 技术栈
- Vue3
- TypeScript
- Vite
- Vue Router
- Pinia
- Element Plus
## 安装
```bash
npm install
开发
npm run dev
构建
npm run build
环境变量
| 变量 | 说明 |
|---|---|
| VITE_API_BASE_URL | API 地址 |
| VITE_APP_ENV | 当前环境 |
目录结构
见项目结构说明。
---
# 23. Git 规范
## 23.1 .gitignore
```gitignore
node_modules
dist
.env.local
.DS_Store
*.log
*.bak不要提交:
node_modules
dist
日志文件
临时备份文件
本地环境文件
23.2 分支建议
main 生产分支
develop 开发分支
feature/* 功能分支
fix/* 修复分支
release/* 发布分支
23.3 提交信息建议
feat: 新功能
fix: 修复问题
docs: 文档
style: 样式调整
refactor: 重构
test: 测试
chore: 工程配置
示例:
git commit -m "feat: add order management page"
24. 当前项目常见问题修正建议
如果已有项目结构类似:
src/
api/
stores/
api.ts
i18n-en.ts
i18n-zh.ts
建议改成:
src/
api/
request/
index.ts
error.ts
stores/
app.ts
auth.ts
i18n/
index.ts
messages/
en.ts
zh.ts
原因:
stores/api.ts 不是状态管理,应该移动到 request/index.ts。
i18n-en.ts 不是状态管理,应该移动到 i18n/messages/en.ts。
stores 只保留真正的全局状态。
25. fetch 和 axios 怎么选
25.1 继续用 fetch 的条件
1. 项目不复杂
2. 接口数量不多
3. 愿意自己封装 timeout、错误处理、JSON 解析、FormData 兼容
4. 不需要 axios 拦截器生态
25.2 推荐 axios 的条件
1. 商业项目长期维护
2. 接口很多
3. 多人协作
4. 需要统一请求/响应拦截器
5. 需要更成熟的错误处理
6. 需要上传、下载、取消请求等能力
25.3 关键结论
商业项目不是必须 axios。
但商业项目必须有统一 request 层。
fetch 可以用,但不能散着写。
axios 可以用,但也不能直接在页面里乱写。
26. AI 生成项目时的执行步骤
AI 应按这个顺序创建:
第 1 步:创建 Vite Vue TS 项目
第 2 步:安装 router、pinia、element-plus、axios
第 3 步:创建完整 src 目录结构
第 4 步:配置 @ 路径别名
第 5 步:配置 env 文件
第 6 步:创建 request 统一请求层
第 7 步:创建 api 业务接口层
第 8 步:创建 types 类型层
第 9 步:创建 stores 状态层
第 10 步:创建 router/routes/guard
第 11 步:创建 layouts
第 12 步:创建 pages
第 13 步:创建 components
第 14 步:创建 composables
第 15 步:创建 utils
第 16 步:创建 validators
第 17 步:创建 i18n
第 18 步:配置 main.ts
第 19 步:配置 App.vue
第 20 步:配置 ESLint、Prettier、EditorConfig
第 21 步:配置测试目录
第 22 步:配置 Dockerfile
第 23 步:完善 README
第 24 步:运行 npm run build 检查类型和打包
27. AI 生成代码时的禁止事项
1. 禁止在页面里直接 fetch。
2. 禁止在页面里直接 axios。
3. 禁止把 request 放 stores。
4. 禁止把 i18n 字典放 stores。
5. 禁止把所有类型都写在页面里。
6. 禁止把所有接口写在一个巨大 api.ts 里。
7. 禁止所有页面共用一个巨大 style.css。
8. 禁止把 token 判断写在每个页面里。
9. 禁止把登录失效处理写在每个接口里。
10. 禁止把大量重复的 Element Plus message 逻辑散落在页面中。
11. 禁止提交 .bak 文件。
12. 禁止提交 dist、node_modules、日志文件。
28. 最小可用商业项目结构
如果项目比较小,可以先使用精简版:
src/
api/
order.ts
product.ts
system.ts
request/
index.ts
error.ts
assets/
styles/
components/
composables/
useMessage.ts
useLoading.ts
constants/
storage.ts
i18n/
index.ts
messages/
layouts/
MainLayout.vue
AdminLayout.vue
BlankLayout.vue
pages/
router/
index.ts
routes.ts
guard.ts
stores/
app.ts
auth.ts
types/
api.ts
order.ts
product.ts
user.ts
utils/
storage.ts
format.ts
validators/
auth.ts
App.vue
main.ts
style.css
这个版本已经足够支撑大多数中小商业项目。
29. 推荐最终结论
一个合格的 Vue3 商业项目前端,不是看用了多少技术,而是看是否具备这些基础能力:
1. 请求统一
2. 状态边界清晰
3. 路由权限清晰
4. 类型定义清晰
5. 目录职责清晰
6. 环境变量清晰
7. 错误处理清晰
8. 表单校验清晰
9. 样式组织清晰
10. 构建部署清晰
11. README 清晰
12. 后期扩展不混乱
优先补齐:
request
env
router guard
types
i18n
layouts
validators
eslint/prettier
README
不要一开始追求复杂架构。
先做到边界清楚,再逐步增加测试、监控、CI/CD。