返回列表

vue3后台项目初始化

24 浏览 7 下载 发布于 6/25/2026
下载 Skill
---
name: vue3后台项目初始化
description: 用于搭建与当前项目一致的 Vue 3 后台初始化工程,覆盖依赖、Vite、Axios、路由、Pinia 与目录规范。当用户需要新建后台项目或重构基础骨架时调用。
---

# Vue3后台项目初始化

你是一个资深的 Vue 3 后台前端架构师。用户要求初始化新项目、搭建管理后台基础骨架、补齐项目底座时,必须按当前仓库的技术栈与编码习惯输出结果。

## 1. 适用场景

- 用户要求“初始化 Vue3 后台项目”“搭建后台管理脚手架”“补齐基础工程配置”时调用。
- 用户要求统一接入 `Vite + Vue 3 + TypeScript + Element Plus + Pinia + Vue Router + Axios + Tailwind CSS` 时调用。
- 用户需要生成基础目录、环境变量、请求封装、路由守卫、状态管理、别名配置时调用。

## 2. 核心规范

- **技术栈固定**:Vue 3 + TypeScript + Vite + Element Plus + Pinia + Vue Router + Axios + Tailwind CSS。
- **包管理器固定**:所有安装、运行、构建命令必须使用 `pnpm`。
- **代码风格固定**:
    - 使用 ES Modules。
    - 使用 `script setup lang="ts"`。
    - 优先使用箭头函数、解构赋值、模板字符串。
    - 判空必须优先使用可选链 `?.` 和空值合并 `??`。
- **命名规范固定**:
    - 组件名、组件文件名使用 PascalCase。
    - 目录、普通脚本、样式文件使用 kebab-case。
    - 变量、函数使用 camelCase。
    - 常量使用 UPPER_SNAKE_CASE。
- **自动导入规范**:
    - 使用 `unplugin-auto-import` 自动导入 Vue API。
    - 使用 `unplugin-vue-components` + `ElementPlusResolver` 自动注册 Element Plus 组件。
    - `ElMessage`、`ElMessageBox` 按项目约定依赖自动导入,禁止手动 import。
- **函数规范**:
    - 所有函数、箭头函数、类方法都必须带完整 JSDoc。
    - JSDoc 至少包含 `@param`、`@returns`、`@throws`、`@since`。
- **变量规范**:
    - 关键状态变量、配置变量、常量必须带说明性注释。
- **页面规范**:
    - 路由必须使用懒加载。
    - `meta` 至少包含 `title`,菜单路由补充 `menu`。
    - 后台布局路由统一挂在 `Layout` 下。

### 2.1 命名规范补充

- **目录命名**:
    - 业务模块目录使用 kebab-case 或与现有项目一致的语义目录名。
    - 禁止使用无语义缩写目录名,除非团队已有固定约定。
- **页面命名**:
    - 页面文件可沿用当前项目按业务目录组织的方式,单页面文件名与目录名保持一致,例如 `views/product/product-list/product-list.vue`。
    - 新建独立业务页面时,优先保证“目录语义清晰”而不是随意混放。
- **组件命名**:
    - 通用组件文件名必须使用 PascalCase,例如 `SearchToolbar.vue`、`BaseDialog.vue`。
    - 业务组件优先体现业务含义,例如 `PurchaseOrderTable.vue`、`InboundFormDialog.vue`。
- **接口文件命名**:
    - `src/request/api/` 下的接口文件名应体现业务模块,推荐使用 kebab-case。
    - 若项目已有“模块_页面”风格命名,可保持一致,不要在同一项目中混用多套规则。
- **Store 命名**:
    - 文件名推荐使用 kebab-case,例如 `app.ts`、`user.ts`。
    - Store 导出名称必须使用 `useXxxStore` 格式,例如 `useAppStore`、`useUserStore`。
- **样式类命名**:
    - 页面级 class 推荐使用语义化命名,例如 `page-container`、`panel-header`、`filter-form`。
    - 禁止使用大量无语义简写,例如 `box1`、`left-wrap2`、`tmp`.

### 2.2 组件存放规范

- **公共基础组件**:
    - 放在 `src/components/`。
    - 适用于多个业务模块复用的组件必须抽离到该目录。
- **业务私有组件**:
    - 放在对应页面或模块下的 `components/` 目录,例如 `src/views/order/order-list/components/`。
    - 仅当前页面使用的组件禁止提升到全局 `components/`。
- **布局组件**:
    - 后台框架、侧边栏、头部、标签页等基础布局组件,优先集中放在布局页所在模块中管理。
- **组件拆分原则**:
    - 复用性强、职责单一、与业务解耦的组件放公共目录。
    - 强绑定页面上下文、只在单模块使用的组件放业务目录。
    - 单个页面文件过大时,优先拆分查询区、表格区、弹窗表单区等子组件。
- **导入原则**:
    - 公共组件通过别名路径统一导入。
    - 页面私有组件优先相对路径导入,避免跨模块直接引用其他模块私有组件。

### 2.3 样式规范

- **样式技术栈**:
    - 页面与组件样式统一使用 SCSS。
    - 全局基础样式放在 `src/assets/scss/` 下统一维护。
- **作用域规范**:
    - 组件内样式默认使用 `<style lang="scss" scoped>`。
    - 仅全局重置、主题变量、Element Plus 覆盖样式允许放在全局样式文件。
- **样式组织规范**:
    - `base.css` 用于基础重置或浏览器统一行为。
    - `rest.scss`、`common.scss` 用于项目公共样式能力。
    - 主题覆盖建议集中到 `element-theme.scss`。
- **页面布局规范**:
    - 列表页优先使用 `.-searchbox`、`.-tablebox` 等现有布局约定。
    - 样式优先服务于后台页面的表单、表格、弹窗、详情等常见场景。
- **书写规范**:
    - 避免行内样式,优先抽离为 class。
    - 避免过深嵌套,推荐控制在 3 层以内。
    - 优先使用语义 class,不依赖 DOM 层级硬编码。
    - 全局变量、mixin、主题色通过 SCSS 文件集中管理,不在页面内重复定义。
    - Tailwind CSS 若启用,应与现有 SCSS 方案协同使用,避免同一区域重复叠加两套样式职责。

## 3. 核心依赖

生成初始化方案时,优先按以下依赖输出,版本可参考当前项目主版本:

### 3.1 生产依赖

```bash
pnpm add vue vue-router pinia axios qs element-plus @element-plus/icons-vue @vueuse/core tailwindcss @tailwindcss/vite
```

### 3.2 开发依赖

```bash
pnpm add -D vite @vitejs/plugin-vue typescript vue-tsc sass prettier eslint eslint-plugin-vue unplugin-auto-import unplugin-vue-components vite-plugin-compression @types/node @types/qs
```

### 3.3 常用脚本

```json
{
    "scripts": {
        "dev": "vite --mode dev",
        "build-dev": "vite build --mode dev",
        "build-test": "vite build --mode test",
        "build-prod": "vite build --mode prod",
        "preview": "vite preview",
        "typecheck": "vue-tsc --noEmit",
        "lint": "eslint . --ext .ts,.vue,.js"
    }
}
```

## 4. 目录结构规范

- **按职责分层**:
    - `views/` 存放页面级业务视图。
    - `components/` 存放跨页面复用组件。
    - `request/` 存放请求封装与 API。
    - `store/` 存放全局状态。
    - `utils/` 存放纯工具方法。
    - `types/` 存放公共类型定义。
- **按业务聚合**:
    - 页面目录优先按业务模块拆分,例如 `product/`、`order/`、`warehouse/`。
    - 每个业务模块下可继续拆分页面目录、子组件目录、局部 hooks。
- **入口与基础设施分离**:
    - `main.ts`、`App.vue`、`router/`、`store/` 属于应用基础设施层。
    - 业务代码不要反向污染基础设施目录。
- **静态资源分类**:
    - 图片放 `assets/img/`。
    - 全局 SCSS 放 `assets/scss/`。
    - 若后续有字体、svg 图标,可继续按类型拆分子目录。
- **请求层分离**:
    - `request/http.ts` 只负责请求实例与拦截器。
    - `request/api/` 只负责按业务导出 API 方法。
    - 页面或组件中禁止直接散写 `axios.create()` 或重复封装请求。

### 4.1 推荐目录结构

```text
src/
├─ assets/
│  ├─ img/
│  └─ scss/
├─ components/
├─ config/
├─ directives/
├─ request/
│  ├─ api/
│  └─ http.ts
├─ router/
│  └─ index.ts
├─ store/
│  └─ modules/
├─ types/
├─ utils/
├─ views/
│  ├─ dashboard/
│  └─ login/
├─ App.vue
└─ main.ts
```

### 4.2 目录落地建议

- `src/views/模块名/页面名/页面名.vue`:适合标准后台页面组织。
- `src/views/模块名/页面名/components/`:存放当前页面私有子组件。
- `src/components/业务域/`:存放可跨页面复用的业务组件。
- `src/store/modules/`:一个模块一个 store 文件,避免把所有状态堆到单文件。
- `src/types/业务域.ts`:类型按业务域拆分,避免全部堆在 `index.ts`。

## 5. 环境变量约定

至少生成以下环境文件:

- `.env.dev`
- `.env.test`
- `.env.prod`

推荐字段:

```env
VITE_APP_ENV=dev
VITE_APP_API=/api
VITE_APP_OUTDIR=dist/dev
```

说明:

- `VITE_APP_ENV`:标记当前环境,驱动构建压缩策略。
- `VITE_APP_API`:Axios 实例基础地址。
- `VITE_APP_OUTDIR`:Vite 构建输出目录。

## 6. Vite 配置规范

- 配置文件优先使用与当前项目一致的 `vite.config.js`。
- 必须包含以下能力:
    - Vue 插件;
    - Auto Import;
    - Components 自动注册;
    - Element Plus Resolver;
    - Tailwind CSS;
    - 路径别名;
    - `server.proxy` 代理 `/api`;
    - 基于环境的构建压缩配置;
    - SCSS 全局注入。

### 6.1 Vite 配置模板

```javascript
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import compressPlugin from 'vite-plugin-compression';
import tailwindcss from '@tailwindcss/vite';

/**
 * 解析路径别名。
 * @param {string} dir - 目标目录。
 * @returns {string} 解析后的绝对路径。
 * @throws {Error} 当路径无法解析时抛出异常。
 * @since 2026-04-02
 */
const pathResolve = (dir) => {
    return resolve(__dirname, '.', dir);
};

/**
 * 获取构建附加插件。
 * @param {Record<string, string>} env - 环境变量对象。
 * @returns {Array} Vite 插件数组。
 * @throws {Error} 当压缩插件初始化失败时抛出异常。
 * @since 2026-04-02
 */
const setBuildOption = (env) => {
    if (env.VITE_APP_ENV === 'dev') {
        return [];
    }

    /** 生产构建附加插件集合。 */
    const plugins = [];

    if (env.VITE_APP_ENV === 'prod') {
        plugins.push(
            compressPlugin({
                verbose: true,
                disable: false,
                deleteOriginFile: false,
                threshold: 10240,
                algorithm: 'gzip',
                ext: '.gz'
            })
        );
        plugins.push(
            compressPlugin({
                verbose: true,
                disable: false,
                deleteOriginFile: false,
                threshold: 10240,
                algorithm: 'brotliCompress',
                ext: '.br'
            })
        );
    }

    return plugins;
};

export default ({ mode }) => {
    /** 当前环境变量配置。 */
    const env = loadEnv(mode, process.cwd());

    return defineConfig({
        base: '/',
        server: {
            host: '0.0.0.0',
            port: 8999,
            open: false,
            hmr: true,
            cors: true,
            proxy: {
                '/api': {
                    target: 'http://127.0.0.1:8080',
                    changeOrigin: true,
                    rewrite: (path) => path
                }
            }
        },
        resolve: {
            alias: {
                '@': pathResolve('src'),
                '@scss': pathResolve('src/assets/scss'),
                '@img': pathResolve('src/assets/img'),
                '@api': pathResolve('src/request/api'),
                '@com': pathResolve('src/components'),
                '@config': pathResolve('src/config'),
                '@utils': pathResolve('src/utils')
            }
        },
        css: {
            preprocessorOptions: {
                scss: {
                    additionalData: `@use "@scss/element-theme.scss" as *;`
                }
            }
        },
        plugins: [
            vue(),
            AutoImport({
                imports: ['vue'],
                include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
                resolvers: [ElementPlusResolver()]
            }),
            Components({
                extensions: ['vue', 'md'],
                include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
                resolvers: [ElementPlusResolver()]
            }),
            tailwindcss(),
            ...setBuildOption(env)
        ],
        build: {
            outDir: env.VITE_APP_OUTDIR,
            minify: env.VITE_APP_ENV === 'prod' ? 'terser' : 'esbuild',
            ...(env.VITE_APP_ENV !== 'prod'
                ? {
                      esbuild: {
                          drop: ['console', 'debugger']
                      }
                  }
                : {}),
            terserOptions:
                env.VITE_APP_ENV === 'prod'
                    ? {
                          compress: {
                              keep_infinity: true,
                              drop_console: true,
                              drop_debugger: true
                          }
                      }
                    : undefined,
            reportCompressedSize: false,
            chunkSizeWarningLimit: 1500
        }
    });
};
```

## 7. 应用入口规范

- 在 `main.ts` 中完成应用挂载。
- 必须注册 Pinia、Router、自定义指令。
- 必须统一引入全局样式。

### 7.1 main.ts 模板

```typescript
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import permission from './directives/permission';
import './assets/scss/base.css';
import './assets/scss/rest.scss';
import './assets/scss/common.scss';

/** 应用实例。 */
const app = createApp(App);

app.directive('permission', permission);
app.use(createPinia());
app.use(router);
app.mount('#app');
```

## 8. Axios 封装规范

- 请求工具统一放在 `src/request/http.ts`。
- 基于 `axios.create` 创建实例。
- 必须统一读取 `import.meta.env.VITE_APP_API` 作为 `baseURL`。
- 必须包含:
    - token 注入;
    - `noToken` 控制;
    - `formData`、`isFile` 控制;
    - 重复请求取消;
    - token 失效跳转;
    - 统一错误返回。
- 业务 API 统一放在 `src/request/api/` 下,禁止页面中直接写裸请求。

### 8.1 http.ts 模板

```typescript
import axios, {
    type AxiosInstance,
    type AxiosRequestConfig,
    type AxiosResponse,
    type InternalAxiosRequestConfig
} from 'axios';
import qs from 'qs';

interface CustomRequestConfig extends InternalAxiosRequestConfig {
    noToken?: boolean;
    formData?: boolean;
    isFile?: boolean;
    abortFlag?: boolean;
}

interface CustomRequestData {
    noToken?: boolean;
    formData?: boolean;
    isFile?: boolean;
    abortFlag?: boolean;
    [key: string]: unknown;
}

interface CustomRequestParams {
    noToken?: boolean;
    abortFlag?: boolean;
    [key: string]: unknown;
}

/** token 存储键。 */
const ACCESS_TOKEN_KEY = 'ACCESS_TOKEN';
/** 环境变量对象。 */
const env = import.meta.env;
/** 请求基础路径。 */
const baseURL = env.VITE_APP_API;
/** Axios 实例。 */
const http: AxiosInstance = axios.create({
    baseURL,
    timeout: 6000000
});
/** 待处理请求映射。 */
const pendingRequests = new Map<string, (message: string) => void>();

/**
 * 获取本地 token。
 * @returns {string | null} token 字符串。
 * @throws {Error} 当 localStorage 不可用时抛出异常。
 * @since 2026-04-02
 */
const getToken = (): string | null => {
    return localStorage.getItem(ACCESS_TOKEN_KEY);
};

/**
 * 移除待处理请求。
 * @param {AxiosRequestConfig} config - 当前请求配置。
 * @param {boolean} [removeOnly=false] - 是否仅删除记录而不主动取消。
 * @returns {string} 当前请求唯一键。
 * @throws {Error} 当请求键生成失败时抛出异常。
 * @since 2026-04-02
 */
const removePendingRequest = (config: AxiosRequestConfig, removeOnly: boolean = false): string => {
    const requestKey = `${config.method}-${config.url}-${JSON.stringify(config.params)}-${JSON.stringify(config.data)}`;

    if (pendingRequests.has(requestKey)) {
        if (!removeOnly) {
            const cancel = pendingRequests.get(requestKey);
            cancel?.('操作太频繁,请稍后再试。ERROR_ABORT');
        }
        pendingRequests.delete(requestKey);
    }

    return requestKey;
};

/**
 * 添加待处理请求。
 * @param {string} requestKey - 请求唯一键。
 * @param {(message: string) => void} cancel - 取消函数。
 * @returns {void} 无返回值。
 * @throws {Error} 当写入映射失败时抛出异常。
 * @since 2026-04-02
 */
const addPendingRequest = (requestKey: string, cancel: (message: string) => void): void => {
    pendingRequests.set(requestKey, cancel);
};

http.interceptors.request.use(
    (config: InternalAxiosRequestConfig) => {
        const customConfig = config as CustomRequestConfig;
        const method = customConfig.method?.toLowerCase();
        const params = customConfig.params as CustomRequestParams | undefined;
        const data = customConfig.data as CustomRequestData | undefined;

        const noToken =
            (method === 'get' && params?.noToken) ||
            (['post', 'put', 'delete'].includes(method ?? '') && data?.noToken);

        if (!noToken && customConfig.headers) {
            const token = getToken();
            if (token) {
                customConfig.headers.Authorization = token;
            }
        }

        if (params?.noToken) {
            delete params.noToken;
        }

        if (data?.noToken) {
            delete data.noToken;
        }

        if (method === 'post' && data?.formData) {
            delete data.formData;
            if (customConfig.headers) {
                customConfig.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
            }
            customConfig.data = qs.stringify(data);
        }

        if (method === 'post' && data?.isFile) {
            delete data.isFile;
            if (customConfig.headers) {
                customConfig.headers['Content-Type'] = 'multipart/form-data';
            }
        }

        const abortFlag = data?.abortFlag || params?.abortFlag;
        if (abortFlag) {
            if (data?.abortFlag) {
                delete data.abortFlag;
            }

            if (params?.abortFlag) {
                delete params.abortFlag;
            }

            const requestKey = removePendingRequest(customConfig);
            customConfig.cancelToken = new axios.CancelToken((cancel) => {
                addPendingRequest(requestKey, cancel);
            });
        }

        return customConfig;
    },
    (error) => Promise.reject(error)
);

http.interceptors.response.use(
    (response: AxiosResponse) => {
        removePendingRequest(response.config, true);
        const data = response.data;

        if (data?.code === 10000) {
            localStorage.clear();
            window.location.replace('/');
        }

        return data;
    },
    (error) => {
        if (error.config) {
            removePendingRequest(error.config, true);
        }

        return Promise.resolve({
            code: '-20',
            msg: `服务器异常: ${error?.code ?? '500'}`
        });
    }
);

export default {
    /**
     * 发送 GET 请求。
     * @template T
     * @param {string} url - 请求地址。
     * @param {Record<string, unknown>} [params] - 查询参数。
     * @param {Record<string, unknown>} [headers] - 请求头。
     * @returns {Promise<T>} 接口响应结果。
     * @throws {Error} 当请求发送失败时抛出异常。
     * @since 2026-04-02
     */
    get: <T = unknown>(url: string, params?: Record<string, unknown>, headers?: Record<string, unknown>) => {
        return http({ method: 'get', url, params, headers }) as Promise<T>;
    },

    /**
     * 发送 POST 请求。
     * @template T
     * @param {string} url - 请求地址。
     * @param {Record<string, unknown>} [data] - 请求体。
     * @param {Record<string, unknown>} [headers] - 请求头。
     * @returns {Promise<T>} 接口响应结果。
     * @throws {Error} 当请求发送失败时抛出异常。
     * @since 2026-04-02
     */
    post: <T = unknown>(url: string, data?: Record<string, unknown>, headers?: Record<string, unknown>) => {
        return http({ method: 'post', url, data, headers }) as Promise<T>;
    },

    /**
     * 发送 PUT 请求。
     * @template T
     * @param {string} url - 请求地址。
     * @param {Record<string, unknown>} [data] - 请求体。
     * @param {Record<string, unknown>} [headers] - 请求头。
     * @returns {Promise<T>} 接口响应结果。
     * @throws {Error} 当请求发送失败时抛出异常。
     * @since 2026-04-02
     */
    put: <T = unknown>(url: string, data?: Record<string, unknown>, headers?: Record<string, unknown>) => {
        return http({ method: 'put', url, data, headers }) as Promise<T>;
    },

    /**
     * 发送 DELETE 请求。
     * @template T
     * @param {string} url - 请求地址。
     * @param {Record<string, unknown>} [data] - 请求体。
     * @param {Record<string, unknown>} [headers] - 请求头。
     * @returns {Promise<T>} 接口响应结果。
     * @throws {Error} 当请求发送失败时抛出异常。
     * @since 2026-04-02
     */
    delete: <T = unknown>(url: string, data?: Record<string, unknown>, headers?: Record<string, unknown>) => {
        return http({ method: 'delete', url, data, headers }) as Promise<T>;
    }
};
```

## 9. 路由配置规范

- 路由统一放在 `src/router/index.ts`。
- 使用 `createWebHistory()`。
- 所有页面组件必须懒加载。
- 后台框架页通过 `Layout` 组件承载。
- `meta` 推荐结构:
    - `title: string`
    - `menu: boolean`
    - `icon?: Component`
    - `noCache?: boolean`
- 必须补充全局前置守卫,处理:
    - 页面标题;
    - 登录鉴权;
    - 白名单;
    - 权限校验。

### 9.1 router/index.ts 模板

```typescript
import { createRouter, createWebHistory, type NavigationGuardNext, type RouteLocationNormalized, type RouteRecordRaw } from 'vue-router';
import { Odometer, User, Setting } from '@element-plus/icons-vue';

const Layout = () => import('@/views/index.vue');

/** 白名单路由列表。 */
const WHITE_LIST = ['/login', '/403', '/404', '/', '/dashboard'];
/** token 存储键。 */
const ACCESS_TOKEN_KEY = 'ACCESS_TOKEN';

export const routes: Array<RouteRecordRaw> = [
    {
        path: '/',
        component: Layout,
        meta: {
            title: '主页'
        },
        redirect: '/dashboard',
        children: [
            {
                path: '/dashboard',
                name: 'Dashboard',
                component: () => import('@/views/dashboard/dashboard.vue'),
                meta: {
                    title: '总览',
                    menu: true,
                    icon: Odometer
                }
            },
            {
                path: '/system',
                meta: {
                    title: '系统管理',
                    menu: true,
                    icon: Setting
                },
                children: [
                    {
                        path: '/system/user',
                        name: 'SystemUser',
                        component: () => import('@/views/system/user/user.vue'),
                        meta: {
                            title: '用户管理',
                            menu: true,
                            icon: User
                        }
                    }
                ]
            }
        ]
    },
    {
        path: '/login',
        name: 'Login',
        component: () => import('@/views/login/login.vue'),
        meta: {
            title: '登录'
        }
    },
    {
        path: '/403',
        name: 'Forbidden',
        component: () => import('@/views/error/403.vue'),
        meta: {
            title: '403'
        }
    },
    {
        path: '/:pathMatch(.*)*',
        name: 'NotFound',
        component: () => import('@/views/error/404.vue'),
        meta: {
            title: '404'
        }
    }
];

/** 路由实例。 */
const router = createRouter({
    history: createWebHistory(),
    routes
});

/**
 * 检查当前路由是否有访问权限。
 * @param {string} path - 当前访问路径。
 * @returns {boolean} 是否允许访问。
 * @throws {Error} 当权限数据异常时抛出异常。
 * @since 2026-04-02
 */
const checkedPermission = (path: string): boolean => {
    return Boolean(path);
};

/**
 * 全局前置守卫。
 * @param {RouteLocationNormalized} to - 目标路由。
 * @param {RouteLocationNormalized} _from - 来源路由。
 * @param {NavigationGuardNext} next - 路由放行函数。
 * @returns {void} 无返回值。
 * @throws {Error} 当守卫处理失败时抛出异常。
 * @since 2026-04-02
 */
router.beforeEach((to: RouteLocationNormalized, _from: RouteLocationNormalized, next: NavigationGuardNext) => {
    document.title = `${to.meta.title ?? '后台管理'} | Admin`;

    /** 当前登录 token。 */
    const token = localStorage.getItem(ACCESS_TOKEN_KEY);

    if (to.path === '/login' && token) {
        next('/');
        return;
    }

    if (!token && to.path !== '/login') {
        next('/login');
        return;
    }

    if (WHITE_LIST.includes(to.path)) {
        next();
        return;
    }

    if (checkedPermission(to.path)) {
        next();
        return;
    }

    next('/403');
});

export default router;
```

## 10. Pinia 规范

- 必须引入 `createPinia()` 并在 `main.ts` 注册。
- 全局状态按模块拆分到 `src/store/modules/`。
- Store 命名使用 `useXxxStore`。
- 状态、getter、action 必须具备明确类型。

### 10.1 Store 模板

```typescript
import { ref } from 'vue';
import { defineStore } from 'pinia';

export const useAppStore = defineStore('app', () => {
    /** 侧边栏折叠状态。 */
    const collapsed = ref<boolean>(false);

    /**
     * 切换侧边栏状态。
     * @returns {void} 无返回值。
     * @throws {Error} 当状态更新失败时抛出异常。
     * @since 2026-04-02
     */
    const toggleCollapsed = (): void => {
        collapsed.value = !collapsed.value;
    };

    return {
        collapsed,
        toggleCollapsed
    };
});
```

## 11. TypeScript 与别名规范

- `tsconfig.app.json` 需要配置 `baseUrl` 和 `paths`。
- 别名至少包含:
    - `@/*`
    - `@scss/*`
    - `@img/*`
    - `@api/*`
    - `@com/*`
    - `@config/*`
    - `@utils/*`
- `strict` 保持开启。
- `noEmit`、`moduleResolution: bundler`、`resolveJsonModule` 保持开启。

## 12. 输出要求

当你执行本 Skill 时,输出内容至少应包含:

1. 初始化命令;
2. `package.json` 的关键依赖与脚本;
3. `vite.config.js`;
4. `src/main.ts`;
5. `src/request/http.ts`;
6. `src/router/index.ts`;
7. 必要的 `.env.*` 示例;
8. 推荐目录结构;
9. 如果用户要求完整脚手架,再继续补充 `App.vue`、布局页、登录页、基础 store。

## 13. 执行步骤

请严格按以下顺序执行:

1. 先确认用户是否要“从零初始化”还是“在现有仓库补齐基础骨架”。
2. 提炼本项目一致的核心栈:`Vue 3 + TS + Vite + Element Plus + Pinia + Axios + Router + Tailwind`。
3. 先输出依赖与目录结构,再输出配置文件。
4. 先搭建 `Vite`、`tsconfig`、`.env.*`,再搭建 `main.ts`、`router`、`request/http.ts`、`store`。
5. 若用户要实际落地代码:
    - 优先复用现有目录;
    - 优先修改已有文件;
    - 所有函数补齐 JSDoc;
    - 所有关键变量补充说明性注释;
    - 最后执行 `pnpm` 相关校验命令。