# vue-element-admin Administrator & Developer Guide

vue-element-admin is a production-ready front-end solution for building admin interfaces, created by PanJiaChen. It is built on **Vue 2.6.10**, **Element UI 2.13.2**, **Vuex 3.1.0**, and **vue-router 3.0.2**. The project provides a comprehensive set of 30+ reusable components, a dual-layer permission system, a mock API server, SCSS theming infrastructure, SVG icon management, and code generation scaffolding. Version **4.4.0**, MIT licensed.

This guide covers every architectural decision, configuration surface, deployment procedure, security consideration, and internal convention a developer or administrator needs to understand, extend, operate, and maintain vue-element-admin without consulting external sources.

---

## 1. Architecture Overview

### 1.1 High-Level Structure

vue-element-admin is a single-page application (SPA) built with Vue CLI 4.4.4. There is no server-side code — the entire application runs in the browser. During development, a mock server integrated into webpack-dev-server simulates backend APIs. In production, the app compiles to static assets (`dist/`) served by any HTTP server (Nginx, Apache, CDN).

```
┌──────────────────────────────────────────────────────────┐
│                        Browser                           │
│                                                          │
│  ┌─────────────────────────────────────────────────────┐ │
│  │ Vue Instance (main.js)                              │ │
│  │  ├── App.vue (root component, <router-view>)        │ │
│  │  ├── Router (vue-router)                            │ │
│  │  │    ├── constantRoutes (public)                   │ │
│  │  │    └── asyncRoutes (role-gated, dynamic)         │ │
│  │  ├── Store (Vuex)                                   │ │
│  │  │    ├── app (sidebar, device, size)               │ │
│  │  │    ├── user (auth, token, roles)                 │ │
│  │  │    ├── permission (route filtering)              │ │
│  │  │    ├── settings (theme, UI toggles)              │ │
│  │  │    ├── tagsView (visited/cached views)           │ │
│  │  │    └── errorLog (client error tracking)          │ │
│  │  ├── Layout                                         │ │
│  │  │    ├── Sidebar (routes → menu)                   │ │
│  │  │    ├── Navbar (breadcrumb, search, user)         │ │
│  │  │    ├── TagsView (tab navigation)                 │ │
│  │  │    ├── AppMain (keep-alive router-view)          │ │
│  │  │    └── Settings (right panel)                    │ │
│  │  ├── API Layer (axios + interceptors)               │ │
│  │  └── Mock Server (dev) / Real API (prod)            │ │
│  └─────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
```

### 1.2 Source Directory Layout

```
src/
├── main.js              # Vue instance bootstrap, Element UI registration, filters, mock init
├── App.vue              # Root component — single <router-view> with id="app"
├── permission.js        # Global beforeEach navigation guard (token + role checks)
├── settings.js          # App-level defaults (title, tagsView, fixedHeader, sidebarLogo)
├── api/                 # Axios request wrappers grouped by domain
│   ├── article.js       # fetchList, fetchArticle, fetchPv, createArticle, updateArticle
│   ├── qiniu.js         # getToken (Qiniu cloud storage)
│   ├── remote-search.js # searchUser
│   ├── role.js          # getRoutes, getRoles, addRole, deleteRole, updateRole
│   └── user.js          # login, getInfo, logout
├── components/          # 30+ reusable components (see Section 8.6)
├── directive/           # Custom Vue directives (permission, clipboard, waves, sticky, etc.)
├── filters/             # Global Vue filters (parseTime, numberFormatter, etc.)
├── icons/               # SVG icon system (svg-sprite-loader auto-registration)
├── layout/              # App shell — Sidebar, Navbar, AppMain, TagsView, Settings
├── router/              # Route definitions (constantRoutes + asyncRoutes)
├── store/               # Vuex store modules (namespaced)
├── styles/              # SCSS architecture (variables, mixins, overrides)
├── utils/               # Utility functions (request.js, auth.js, validate.js, etc.)
└── views/               # Page components organized by feature domain
mock/                    # Mock.js API server with hot reload via chokidar
plop-templates/          # Code generators (view, component, store module)
tests/                   # Jest unit tests
```

### 1.3 Application Bootstrap Sequence

The application initializes in this exact order, defined in `src/main.js`:

1. **Import core dependencies:** Vue, App.vue, store, router
2. **Import global styles:** `@/styles/index.scss`
3. **Import and configure Element UI:** `Vue.use(ElementUI, { size: ..., zIndex: ... })`
   - Size read from Vuex store (`store.getters.size`) or defaults to `'medium'`
   - Locale set to `enLang` (English) — uncomment Chinese import to switch
4. **Register global filters:** All exports from `@/filters` attached to `Vue.filter()`
5. **Register SVG icons:** `@/icons` triggers `require.context()` for automatic SVG registration
6. **Register permission guard:** `@/permission` installs the global `router.beforeEach` hook
7. **Conditionally initialize mock:** If `process.env.NODE_ENV === 'production'`, import `../mock` which calls `mockXHR()` to intercept XMLHttpRequest
8. **Set production tip:** `Vue.config.productionTip = false`
9. **Create Vue instance:** `new Vue({ el: '#app', router, store, render: h => h(App) })`

> [!WARNING]
> Step 7 means the mock server is active in production builds by default. This **must** be removed before deploying against a real API. See Section 7.4 for details.

### 1.4 Data Flow Architecture

```
User Action
    │
    ▼
Vue Component (view or child component)
    │
    ├── Dispatches Vuex Action ──────────────────┐
    │                                             │
    ├── Calls API function (src/api/*.js)         │
    │       │                                     │
    │       ▼                                     │
    │   Axios Instance (src/utils/request.js)     │
    │       │                                     │
    │       ├── Request Interceptor               │
    │       │   └── Attach X-Token from cookie    │
    │       │                                     │
    │       ▼                                     │
    │   HTTP Request ───► Mock/Real API           │
    │       │                                     │
    │       ▼                                     │
    │   Response Interceptor                      │
    │       ├── code === 20000 → resolve(data)    │
    │       ├── code === 50008/50012/50014         │
    │       │   → re-login dialog → resetToken    │
    │       └── other → reject + error toast      │
    │                                             │
    ▼                                             ▼
Component updates via      Vuex Mutation commits
reactive data binding      → State change → Getters update
                           → All subscribed components re-render
```

### 1.5 Layout System

The layout is assembled in `src/layout/index.vue` and wraps every authenticated route:

```
┌────────────────────────────────────────────────────┐
│ Layout (src/layout/index.vue)                      │
│                                                    │
│  ┌──────────┐ ┌──────────────────────────────────┐ │
│  │          │ │  Navbar                          │ │
│  │          │ │  ├── Hamburger (toggle sidebar)  │ │
│  │          │ │  ├── Breadcrumb                  │ │
│  │ Sidebar  │ │  ├── HeaderSearch                │ │
│  │          │ │  ├── ErrorLog                    │ │
│  │ (routes  │ │  ├── Screenfull                  │ │
│  │  → menu) │ │  ├── SizeSelect                 │ │
│  │          │ │  └── User dropdown               │ │
│  │          │ ├──────────────────────────────────┤ │
│  │          │ │  TagsView (tab bar)              │ │
│  │          │ ├──────────────────────────────────┤ │
│  │          │ │  AppMain                         │ │
│  │          │ │  └── <keep-alive :include=       │ │
│  │          │ │        "cachedViews">            │ │
│  │          │ │       <router-view :key="key"/>  │ │
│  │          │ │     </keep-alive>                │ │
│  └──────────┘ └──────────────────────────────────┘ │
│                                                    │
│  ┌──────────────────────────────────────────────┐  │
│  │  RightPanel (settings drawer, slides in)     │  │
│  │  └── Settings (theme, toggles)               │  │
│  └──────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────┘
```

**ResizeMixin** (`src/layout/mixin/ResizeHandler.js`):
- Watches `$route` — on route change, closes sidebar on mobile
- Mounted: checks `document.body.getBoundingClientRect().width` — if < 992px, dispatches `app/toggleDevice` to `'mobile'` and closes sidebar
- Listens to `window.resize` event

**AppMain** (`src/layout/components/AppMain.vue`):
- Uses `<keep-alive :include="cachedViews">` where `cachedViews` comes from `store.getters.cachedViews`
- The `:key` is set to `$route.path` to force re-creation on path change while allowing caching by component name
- Only components whose `name` matches an entry in `cachedViews` are cached

> [!IMPORTANT]
> For `keep-alive` caching to work, each view component **must** have a `name` property that matches the route's component name. If `meta.noCache` is not set, the component name is added to `cachedViews` automatically.

---

## 2. System Requirements

### 2.1 Runtime Requirements

| Requirement | Minimum | Recommended |
|---|---|---|
| Node.js | >= 8.9 | 12.x or 14.x LTS |
| npm | >= 3.0.0 | 6.x+ |
| Git | Any recent | 2.20+ |
| Browser | IE10+ (with polyfills) | Chrome 60+, Firefox 60+, Safari 12+ |
| OS | Any (Windows, macOS, Linux) | — |

> [!NOTE]
> The project uses `@vue/cli-service 4.4.4` which bundles webpack 4. Node.js versions above 16 may encounter OpenSSL-related build failures. If using Node 17+, set `NODE_OPTIONS=--openssl-legacy-provider` before building.

### 2.2 Disk and Memory

- Repository clone: ~50 MB
- `node_modules`: ~400-600 MB
- Production build (`dist/`): ~5-10 MB (depends on code splitting)
- Build RAM: >=2 GB available (webpack can be memory-intensive)

### 2.3 Network Requirements

During development, the mock server runs locally — no network needed for API calls. For production, outbound HTTPS to your real API backend is required.

External CDN dependencies loaded at runtime:
- **Tinymce**: loaded from `https://cdn.tinymce.com/4/tinymce.min.js` via script-loader in `src/components/Tinymce/`

### 2.4 Development Tool Versions

| Tool | Version | Purpose |
|---|---|---|
| @vue/cli-service | 4.4.4 | Build toolchain |
| babel-eslint | 10.1.0 | ES6+ parsing for ESLint |
| eslint | 6.7.2 | Linting |
| sass | 1.26.2 | SCSS compilation |
| sass-loader | 8.0.2 | Webpack SCSS integration |
| svg-sprite-loader | 4.1.3 | SVG icon spriting |
| svgo | 1.2.0 | SVG optimization |
| husky | 1.3.1 | Git hooks |
| lint-staged | 8.1.5 | Pre-commit lint |
| plop | 2.3.0 | Code generation |
| jest | (via @vue/cli-plugin-unit-jest 4.4.4) | Unit testing |

---

## 3. Configuration Reference

### 3.1 Environment Variables

Environment variables are defined in `.env.*` files at the project root. Vue CLI injects variables prefixed with `VUE_APP_` into the client bundle via `process.env`.

| File | Used When | Typical Content |
|---|---|---|
| `.env.development` | `npm run dev` | `VUE_APP_BASE_API = '/dev-api'` |
| `.env.staging` | `npm run build:stage` | `VUE_APP_BASE_API = '/stage-api'` |
| `.env.production` | `npm run build:prod` | `VUE_APP_BASE_API = '/prod-api'` |

**Variables in use:**

| Variable | Default | Purpose | Used In |
|---|---|---|---|
| `VUE_APP_BASE_API` | `'/dev-api'` (dev) | Base URL prepended to all Axios requests | `src/utils/request.js` |
| `NODE_ENV` | Set by Vue CLI | `'development'` or `'production'` | Mock init, ESLint toggle, build optimizations |
| `port` | `9527` | Dev server port override via CLI env | `vue.config.js` |
| `npm_config_port` | — | Dev server port override via npm config | `vue.config.js` |

Port resolution in `vue.config.js`:

```javascript
const port = process.env.port || process.env.npm_config_port || 9527
```

### 3.2 Application Settings (`src/settings.js`)

This module exports a plain object consumed by the Vuex `settings` store module. Changes here set application defaults.

```javascript
module.exports = {
  title: 'vue-element-admin',          // Page title (appears in document.title)
  showSettings: true,                   // Show settings panel gear icon
  tagsView: true,                       // Enable tag-based navigation bar
  fixedHeader: false,                   // Fix navbar to top on scroll
  sidebarLogo: false,                   // Show logo above sidebar
}
```

These values are loaded into Vuex at store initialization and can be toggled at runtime via the Settings panel (persisted only for the session — no localStorage persistence by default).

### 3.3 Build Configuration (`vue.config.js`)

The build configuration is the most complex configuration surface. Here is a complete reference:

#### 3.3.1 Top-Level Options

| Option | Value | Effect |
|---|---|---|
| `publicPath` | `'/'` | Base URL for all assets. Change to `'./'` for relative paths or `/subpath/` for subdirectory deployment |
| `outputDir` | `'dist'` | Build output directory |
| `assetsDir` | `'static'` | Subdirectory for generated static assets (JS, CSS, images) |
| `lintOnSave` | `process.env.NODE_ENV === 'development'` | ESLint errors shown as warnings in dev, disabled in prod build |
| `productionSourceMap` | `false` | Source maps disabled in production (reduces bundle size, prevents source exposure) |
| `devServer.port` | `port` (default 9527) | Development server port |
| `devServer.open` | `true` | Auto-open browser on `npm run dev` |
| `devServer.overlay` | `{ warnings: false, errors: true }` | Show full-screen overlay for errors only |
| `devServer.proxy` | Not configured | Add proxy rules here for real API during development |

#### 3.3.2 Webpack Chain Modifications

**Preload/Prefetch:**

```javascript
config.plugin('preload').tap(() => [
  { rel: 'preload', fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/], include: 'initial' }
])
config.plugins.delete('prefetch') // Prefetch disabled — routes loaded on demand
```

**SVG Icon Loader:**

The default SVG rule is modified to exclude `src/icons/`, which gets its own rule:

```javascript
// Exclude icons directory from default svg loader
config.module.rule('svg').exclude.add(resolve('src/icons')).end()

// Add svg-sprite-loader for icons
config.module.rule('icons')
  .test(/\.svg$/)
  .include.add(resolve('src/icons')).end()
  .use('svg-sprite-loader')
  .loader('svg-sprite-loader')
  .options({ symbolId: 'icon-[name]' })
  .end()
```

**Environment-Specific:**

Development:
```javascript
config.plugin('ScriptExtHtmlWebpackPlugin').after('html').use('script-ext-html-webpack-plugin', [{
  inline: /runtime\..*\.js$/
}])
```

Production:
```javascript
// Split chunks configuration
config.optimization.splitChunks({
  chunks: 'all',
  cacheGroups: {
    libs: {
      name: 'chunk-libs',
      test: /[\\/]node_modules[\\/]/,
      priority: 10,
      chunks: 'initial'   // Only package third-party deps used in initial load
    },
    elementUI: {
      name: 'chunk-elementUI',
      priority: 20,        // Higher priority — split Element UI separately
      test: /[\\/]node_modules[\\/]_?element-ui(.*)/
    },
    commons: {
      name: 'chunk-commons',
      test: resolve('src/components'),
      minChunks: 3,        // Components used 3+ times get their own chunk
      priority: 5,
      reuseExistingChunk: true
    }
  }
})
config.optimization.runtimeChunk('single')  // Single runtime chunk
```

#### 3.3.3 Development-Only Babel Plugin

In `babel.config.js`, when `NODE_ENV` is not `production`:

```javascript
plugins.push(['dynamic-import-node'])
```

This converts `import()` to synchronous `require()` calls, making hot module replacement (HMR) significantly faster at the cost of losing true code splitting in development.

### 3.4 ESLint Configuration (`.eslintrc.js`)

```javascript
module.exports = {
  root: true,
  parserOptions: { parser: 'babel-eslint', sourceType: 'module' },
  env: { browser: true, node: true, es6: true },
  extends: ['plugin:vue/recommended', 'eslint:recommended'],
  rules: {
    // Style
    semi: [2, 'never'],              // No semicolons
    quotes: [1, 'single'],            // Single quotes (warning)
    indent: [2, 2],                    // 2-space indentation
    
    // Vue-specific
    'vue/name-property-casing': ['error', 'PascalCase'],
    'vue/max-attributes-per-line': [2, {
      singleline: 10,
      multiline: { max: 1, allowFirstLine: false }
    }],
    'vue/singleline-html-element-content-newline': 'off',
    'vue/multiline-html-element-content-newline': 'off',
    
    // Relaxed
    'no-console': 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'accessor-pairs': 2,
    'arrow-spacing': [2, { before: true, after: true }],
    'eqeqeq': [2, 'allow-null'],
    'no-caller': 2,
    'no-eval': 2,
    'no-implied-eval': 2,
    'no-extend-native': 2,
    'no-multi-spaces': 2,
    'no-trailing-spaces': 2,
    'space-before-function-paren': [2, 'never'],
    // ... (many more — see .eslintrc.js for the complete set)
  }
}
```

**Pre-commit enforcement:**

`package.json` configures husky + lint-staged:

```json
{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "src/**/*.{js,vue}": ["eslint --fix", "git add"]
  }
}
```

Every commit automatically runs ESLint with `--fix` on staged `.js` and `.vue` files.

### 3.5 Jest Configuration

Testing is configured via `jest.config.js` (generated by `@vue/cli-plugin-unit-jest`):

```javascript
module.exports = {
  moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
  transform: {
    '^.+\\.vue$': 'vue-jest',
    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
    '^.+\\.jsx?$': 'babel-jest'
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  snapshotSerializers: ['jest-serializer-vue'],
  testMatch: ['**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'],
  testURL: 'http://localhost/',
  collectCoverageFrom: [
    'src/utils/**/*.{js,vue}',
    '!src/utils/auth.js',
    '!src/utils/request.js',
    'src/components/**/*.{js,vue}'
  ]
}
```

Coverage excludes `auth.js` and `request.js` (cookie and HTTP dependencies make them harder to unit test without mocking).

---

## 4. Deployment & Scaling

### 4.1 Build Commands

| Command | Script | Purpose |
|---|---|---|
| `npm run dev` | `vue-cli-service serve` | Start dev server with HMR and mock API |
| `npm run build:prod` | `vue-cli-service build` | Production build |
| `npm run build:stage` | `vue-cli-service build --mode staging` | Staging build |
| `npm run preview` | `node build/index.js --preview` | Preview production build locally |
| `npm run lint` | `eslint --ext .js,.vue src` | Run ESLint |
| `npm run test:unit` | `jest --clearCache && vue-cli-service test:unit` | Run unit tests |
| `npm run test:ci` | `npm run lint && npm run test:unit` | CI pipeline (lint + test) |
| `npm run new` | `plop` | Run code generator |
| `npm run svgo` | `svgo -f src/icons/svg --config=src/icons/svgo.yml` | Optimize SVG icons |

### 4.2 Production Build

```bash
# Install dependencies
npm install

# Build for production
npm run build:prod
```

Output lands in `dist/`:

```
dist/
├── index.html
├── favicon.ico
└── static/
    ├── css/
    │   ├── app.[hash].css
    │   ├── chunk-commons.[hash].css
    │   ├── chunk-elementUI.[hash].css
    │   └── chunk-libs.[hash].css
    ├── js/
    │   ├── app.[hash].js
    │   ├── chunk-commons.[hash].js
    │   ├── chunk-elementUI.[hash].js
    │   ├── chunk-libs.[hash].js
    │   ├── runtime.[hash].js
    │   └── [async-chunk].[hash].js  (one per lazy-loaded route)
    ├── fonts/
    └── img/
```

### 4.3 Nginx Deployment

Since this is a SPA with client-side routing (history mode), the server must return `index.html` for all routes:

```nginx
server {
    listen 80;
    server_name admin.example.com;
    root /var/www/vue-element-admin/dist;
    index index.html;

    # Gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript
               text/xml application/xml application/xml+rss text/javascript
               image/svg+xml;
    gzip_min_length 1024;
    gzip_comp_level 6;

    # Cache static assets aggressively (hashed filenames = cache-bust on change)
    location /static {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # SPA fallback — all routes to index.html
    location / {
        try_files $uri $uri/ /index.html;
    }

    # API proxy to backend
    location /prod-api/ {
        proxy_pass http://backend-api:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
```

> [!TIP]
> The `VUE_APP_BASE_API` value in `.env.production` must match the Nginx proxy location. If set to `'/prod-api'`, configure `location /prod-api/` to proxy to your real backend.

### 4.4 Docker Deployment

Example `Dockerfile`:

```dockerfile
# Build stage
FROM node:14-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build:prod

# Production stage
FROM nginx:1.21-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;"]
```

### 4.5 Subdirectory Deployment

To serve the app under a subpath (e.g., `https://example.com/admin/`):

1. Set `publicPath` in `vue.config.js`:
   ```javascript
   publicPath: '/admin/'
   ```

2. Set the router base:
   ```javascript
   // src/router/index.js
   const createRouter = () => new Router({
     base: '/admin/',
     // ...
   })
   ```

3. Update Nginx:
   ```nginx
   location /admin {
       alias /var/www/vue-element-admin/dist;
       try_files $uri $uri/ /admin/index.html;
   }
   ```

### 4.6 CDN Deployment

For high-traffic deployments, externalize large dependencies via CDN. In `vue.config.js`:

```javascript
const externals = {
  vue: 'Vue',
  'element-ui': 'ELEMENT',
  'vue-router': 'VueRouter',
  vuex: 'Vuex',
  axios: 'axios'
}

// In configureWebpack:
config.externals = externals

// Add CDN links to index.html via html-webpack-plugin
```

This reduces the bundle size significantly — Element UI alone is ~700KB gzipped.

### 4.7 Scaling Considerations

vue-element-admin is a static SPA — scaling is a matter of serving static files efficiently:

| Strategy | Implementation |
|---|---|
| **CDN** | Upload `dist/` to CloudFront, Cloudflare, or similar |
| **HTTP/2** | Enable in Nginx/server for multiplexed asset loading |
| **Brotli** | Pre-compress with `compression-webpack-plugin` for 15-20% smaller than gzip |
| **Service Worker** | Add `@vue/cli-plugin-pwa` for offline caching |
| **Lazy Loading** | Already implemented — async routes load on demand |

---

## 5. Operations

### 5.1 Development Workflow

```bash
# Clone and install
git clone https://github.com/PanJiaChen/vue-element-admin.git
cd vue-element-admin
npm install

# Start dev server (port 9527 by default)
npm run dev

# Override port
port=8080 npm run dev
```

The dev server starts with:
- Hot Module Replacement (HMR)
- Mock API server (registered via `devServer.before`)
- ESLint overlay for errors
- Auto-open browser

### 5.2 Mock Server Hot Reload

The mock system in `mock/mock-server.js` uses chokidar to watch the `mock/` directory:

```javascript
const mockDir = path.join(process.cwd(), 'mock')
chokidar.watch(mockDir, { ignored: /mock-server/, ... }).on('all', (event, path) => {
  // Clear require cache for changed files
  Object.keys(require.cache).forEach(i => {
    if (i.includes(mockDir)) { delete require.cache[require.resolve(i)] }
  })
  // Re-register mock routes
  registerRoutes()
})
```

When you edit any file in `mock/` (except `mock-server.js` itself), routes are automatically re-registered without restarting the dev server.

### 5.3 Adding New Mock Endpoints

1. Create or edit a file in `mock/` (e.g., `mock/orders.js`):

```javascript
module.exports = [
  {
    url: '/vue-element-admin/order/list',
    type: 'get',
    response: config => {
      const { page = 1, limit = 20 } = config.query
      return {
        code: 20000,
        data: {
          total: 100,
          items: generateOrderList(page, limit)
        }
      }
    }
  }
]
```

2. Add the file to the mock table in `mock/index.js`:

```javascript
const order = require('./order')
const mocks = [...user, ...role, ...article, ...search, ...order]
```

3. The dev server picks up changes automatically via hot reload.

**Mock response format convention:**

| Field | Value | Meaning |
|---|---|---|
| `code` | `20000` | Success |
| `code` | `50008` | Illegal token |
| `code` | `50012` | Other client logged in |
| `code` | `50014` | Token expired |
| `code` | `60204` | Account/password error |
| `message` | string | Human-readable message |
| `data` | any | Response payload |

### 5.4 npm Scripts Reference

| Script | Command | Notes |
|---|---|---|
| `dev` | `vue-cli-service serve` | Dev server + mock |
| `build:prod` | `vue-cli-service build` | `.env.production` |
| `build:stage` | `vue-cli-service build --mode staging` | `.env.staging` |
| `preview` | `node build/index.js --preview` | Serves built `dist/` |
| `lint` | `eslint --ext .js,.vue src` | Check only, no fix |
| `test:unit` | `jest --clearCache && vue-cli-service test:unit` | Clears cache first |
| `test:ci` | `npm run lint && npm run test:unit` | Sequential lint + test |
| `new` | `plop` | Interactive code scaffolding |
| `svgo` | `svgo -f src/icons/svg --config=src/icons/svgo.yml` | Optimize SVGs |

### 5.5 Code Generation with Plop

The `plopfile.js` registers three generators:

#### View Generator

```bash
npm run new
? [PLOP] Please choose a generator: view - generate a view component
? view name: my-feature
? Do you want to generate a component? Y/n
? Blocks: <template>, <script>, <style>
```

Creates: `src/views/my-feature/index.vue`

Template (`plop-templates/view/index.hbs`):
```handlebars
{{#if template}}
<template>
  <div />
</template>
{{/if}}

{{#if script}}
<script>
export default {
  name: '{{ properCase name }}',
  // ...
}
</script>
{{/if}}

{{#if style}}
<style scoped>
</style>
{{/if}}
```

#### Component Generator

Creates: `src/components/{Name}/index.vue` with the same block selection.

#### Store Module Generator

Creates: `src/store/modules/{name}.js`:

```javascript
const state = {}
const mutations = {}
const actions = {}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}
```

### 5.6 SVG Icon Management

#### Adding Icons

1. Place SVG files in `src/icons/svg/`
2. Run `npm run svgo` to optimize (removes fills, strokes, unnecessary attributes)
3. Icons are auto-registered via `require.context` in `src/icons/index.js`

#### SVGO Configuration (`src/icons/svgo.yml`)

```yaml
plugins:
  - removeTitle: true
  - removeStyleElement: true
  - removeAttrs:
      attrs: ['fill', 'fill-rule']
  - removeDoctype: true
  - removeComments: true
  - removeMetadata: true
  - removeDesc: true
  - removeUselessDefs: true
  # ... (various optimization plugins)
```

#### Using Icons

```html
<!-- By icon name (filename without .svg extension) -->
<svg-icon icon-class="dashboard" />

<!-- With custom class -->
<svg-icon icon-class="edit" class-name="custom-class" />

<!-- Element UI icon (prefix with el-icon-) -->
<i class="el-icon-edit" />
```

Built-in icons (40+): `404`, `bug`, `chart`, `clipboard`, `component`, `dashboard`, `documentation`, `drag`, `edit`, `education`, `email`, `example`, `excel`, `exit-fullscreen`, `eye`, `eye-open`, `form`, `fullscreen`, `guide`, `hamburger`, `icon`, `international`, `language`, `link`, `list`, `lock`, `message`, `money`, `nested`, `password`, `pdf`, `people`, `peoples`, `qq`, `search`, `shopping`, `size`, `skill`, `star`, `tab`, `table`, `theme`, `tree`, `tree-table`, `user`, `wechat`, `zip`.

---

## 6. Performance

### 6.1 Bundle Optimization

The production build applies several optimizations:

| Optimization | Mechanism | Impact |
|---|---|---|
| **Code splitting** | `splitChunks` in vue.config.js | Element UI, vendor libs, and commons separated |
| **Lazy loading** | `() => import()` in router | Each route loads on demand |
| **Tree shaking** | webpack 4 production mode | Dead code eliminated |
| **No source maps** | `productionSourceMap: false` | ~30% smaller JS output |
| **No prefetch** | `config.plugins.delete('prefetch')` | Prevents eager loading of async chunks |
| **CSS extraction** | `mini-css-extract-plugin` (built-in) | CSS in separate cacheable files |
| **Minification** | `terser-webpack-plugin` (built-in) | JS minified + compressed |

### 6.2 Chunk Analysis

The split chunks configuration produces these bundles:

| Chunk | Contents | Approx. Size (gzipped) |
|---|---|---|
| `chunk-libs` | All `node_modules` except Element UI | ~200 KB |
| `chunk-elementUI` | Element UI library | ~170 KB |
| `chunk-commons` | Shared `src/components/` (used 3+ times) | ~20-50 KB |
| `runtime` | Webpack runtime | ~5 KB |
| `app` | Application code | ~80-120 KB |
| Async chunks | One per lazy-loaded route group | ~2-20 KB each |

### 6.3 View Caching (`keep-alive`)

The `<keep-alive>` wrapper in AppMain caches component instances to avoid re-creation on tab switches. This is managed through the `tagsView` Vuex module:

- **Cached views**: Components whose `name` is in `store.state.tagsView.cachedViews`
- **Opt out**: Set `meta.noCache: true` on a route to prevent caching
- **Cache invalidation**: Closing a tag removes the component name from `cachedViews`, destroying the instance

> [!TIP]
> For views that need partial refresh on re-entry (e.g., list with filters), use `activated()` lifecycle hook instead of `mounted()` — it fires each time a cached component is re-displayed.

### 6.4 Development Performance

The `dynamic-import-node` babel plugin converts `import()` to synchronous `require()` during development. This:
- Eliminates async chunk creation in dev mode
- Makes HMR significantly faster (fewer files to rebuild)
- Does **not** affect production builds (plugin is development-only)

### 6.5 Large Table Performance

For views displaying large datasets (1000+ rows):

- Use Element UI's `lazy` loading on `el-tree` components
- Implement virtual scrolling (not built in — requires external library)
- The `v-el-table` adaptive directive auto-sizes table height but does not virtualize

---

## 7. Security

### 7.1 Authentication Architecture

#### Token Flow

```
1. Login
   POST /vue-element-admin/user/login { username, password }
   ← { code: 20000, data: { token: "admin-token" } }

2. Token Storage
   Cookie: "Admin-Token" = token (via js-cookie)
   Vuex: store.state.user.token = token

3. Authenticated Requests
   Axios request interceptor reads token from cookie
   Sets header: X-Token: <token>

4. Token Validation
   GET /vue-element-admin/user/info?token=xxx
   ← { code: 20000, data: { roles, name, avatar, introduction } }

5. Session Termination
   POST /vue-element-admin/user/logout
   → Clears cookie, resets Vuex state, resets router
```

#### Token Storage Analysis

| Storage | Used | Security Implication |
|---|---|---|
| Cookie (`Admin-Token`) | Yes | Accessible to JavaScript — no `HttpOnly` flag (js-cookie). Vulnerable to XSS token theft. |
| Vuex state | Yes | In-memory only, lost on refresh (cookie is source of truth) |
| localStorage | No | — |

> [!CAUTION]
> The token is stored in a JavaScript-accessible cookie without `HttpOnly`, `Secure`, or `SameSite` attributes. In a production deployment with a real authentication backend, configure the server to set the token as an `HttpOnly; Secure; SameSite=Strict` cookie instead of relying on client-side cookie management. This prevents XSS-based token theft.

### 7.2 Permission System

The permission system operates at two levels:

#### Route-Level Access Control (`src/permission.js`)

```javascript
router.beforeEach(async(to, from, next) => {
  NProgress.start()
  document.title = getPageTitle(to.meta.title)
  const hasToken = getToken()

  if (hasToken) {
    if (to.path === '/login') {
      next({ path: '/' })           // Already logged in → redirect to home
    } else {
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) {
        next()                       // Roles loaded → proceed
      } else {
        try {
          const { roles } = await store.dispatch('user/getInfo')
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          router.addRoutes(accessRoutes)
          next({ ...to, replace: true })  // Retry navigation with new routes
        } catch (error) {
          await store.dispatch('user/resetToken')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
        }
      }
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) {
      next()                         // Whitelist: /login, /auth-redirect
    } else {
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})
```

**Route filtering logic** (`src/store/modules/permission.js`):

```javascript
function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    return true    // No roles restriction → accessible to all authenticated users
  }
}

function filterAsyncRoutes(routes, roles) {
  const res = []
  routes.forEach(route => {
    const tmp = { ...route }
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })
  return res
}
```

Special case: if `roles.includes('admin')`, **all** asyncRoutes are granted without filtering.

#### Element-Level Access Control

**Directive approach** (`v-permission`):

```html
<el-button v-permission="['admin','editor']">Admin/Editor Only</el-button>
```

Implementation in `src/directive/permission/permission.js`:

```javascript
inserted(el, binding, vnode) {
  const { value } = binding
  const roles = store.getters && store.getters.roles
  if (value && value instanceof Array && value.length > 0) {
    const hasPermission = roles.some(role => value.includes(role))
    if (!hasPermission) {
      el.parentNode && el.parentNode.removeChild(el)
    }
  } else {
    throw new Error(`need roles! Like v-permission="['admin','editor']"`)
  }
}
```

**Programmatic approach** (`src/utils/permission.js`):

```javascript
import store from '@/store'

export default function checkPermission(value) {
  if (value && value instanceof Array && value.length > 0) {
    const roles = store.getters && store.getters.roles
    return roles.some(role => value.includes(role))
  } else {
    console.error(`need roles! Like checkPermission(['admin','editor'])`)
    return false
  }
}
```

Usage:
```html
<el-button v-if="checkPermission(['admin'])">Admin Only</el-button>
```

> [!NOTE]
> The `v-permission` directive uses DOM removal (`el.parentNode.removeChild(el)`) which means the element is permanently removed. Use `v-if` with `checkPermission()` if you need reactive show/hide behavior when roles change during a session (e.g., via `changeRoles` action).

### 7.3 Response Interceptor Error Codes

The Axios response interceptor in `src/utils/request.js` handles specific error codes:

```javascript
if (res.code !== 20000) {
  Message({ message: res.message || 'Error', type: 'error', duration: 5 * 1000 })

  // Codes 50008, 50012, 50014 all trigger re-login
  if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
    MessageBox.confirm(
      'You have been logged out, you can cancel to stay on this page, or log in again',
      'Confirm logout',
      { confirmButtonText: 'Re-Login', cancelButtonText: 'Cancel', type: 'warning' }
    ).then(() => {
      store.dispatch('user/resetToken').then(() => {
        location.reload()   // Full page reload to reset all state
      })
    })
  }
  return Promise.reject(new Error(res.message || 'Error'))
} else {
  return res   // Return full response (component accesses .data)
}
```

### 7.4 Mock Server in Production

> [!CAUTION]
> The `src/main.js` file contains:
> ```javascript
> if (process.env.NODE_ENV === 'production') {
>   const { mockXHR } = require('../mock')
>   mockXHR()
> }
> ```
> This activates Mock.js in production, intercepting **all** XHR requests matching mock URLs. Before deploying with a real backend, **remove or comment out this block entirely**. Leaving it in will cause:
> - Real API calls silently intercepted by mock handlers
> - Stale/fake data served to production users
> - Security risk — mock login accepts any credentials

### 7.5 XSS Considerations

| Vector | Status | Notes |
|---|---|---|
| Vue template interpolation | Safe | Vue escapes `{{ }}` by default |
| `v-html` usage | Risk | Used in Tinymce output, clipboard demo — sanitize server-side |
| Markdown editor | Risk | tui-editor renders user markdown — sanitize before display |
| JSON editor | Risk | CodeMirror displays arbitrary JSON — display-only, low risk |
| SVG icons | Safe | Pre-bundled via svg-sprite-loader, not user-supplied |
| URL parameters | Moderate | `redirect` param in login route — validate before navigation |

### 7.6 Dependency Vulnerabilities

The project uses several dependencies with known older versions:

| Package | Version | Concern |
|---|---|---|
| `axios` | 0.18.1 | Multiple CVEs in 0.x — upgrade to 0.21.4+ or 1.x |
| `xlsx` | 0.14.1 | Prototype pollution — upgrade to 0.18+ |
| `codemirror` | 5.45.0 | XSS in certain modes — upgrade to latest 5.x |
| `tui-editor` | 1.3.3 | Deprecated, renamed to `@toast-ui/editor` — XSS fixes in newer versions |
| `dropzone` | 5.5.1 | Known issues — upgrade to 5.9+ |

### 7.7 CORS and API Security

The Axios instance sets `baseURL` from `VUE_APP_BASE_API`. In development, the mock server runs on the same origin (webpack-dev-server), so no CORS issues arise. In production:

- If the API is on a different domain, the backend must send proper `Access-Control-Allow-*` headers
- The `X-Token` header is a custom header, requiring `Access-Control-Allow-Headers: X-Token` on the backend
- Consider switching to cookie-based auth (`withCredentials: true`) for same-site deployments

---

## 8. Developer Internals

### 8.1 Router System

#### 8.1.1 Route Definitions

Routes are split into two arrays in `src/router/index.js`:

**`constantRoutes`** — always available, no auth required:

| Path | Component | Notes |
|---|---|---|
| `/redirect/:path(.*)` | `views/redirect` | Internal redirect handler |
| `/login` | `views/login` | Login page |
| `/auth-redirect` | `views/login/auth-redirect` | OAuth callback handler |
| `/404` | `views/error-page/404` | Not found |
| `/401` | `views/error-page/401` | Unauthorized |
| `/` | Layout → `views/dashboard` | Dashboard (redirects from `/`) |
| `/documentation` | Layout → `views/documentation` | External link page |
| `/guide` | Layout → `views/guide` | Feature guide (driver.js) |
| `/profile` | Layout → `views/profile` | User profile |

**`asyncRoutes`** — dynamically added based on roles:

| Path | Component | Roles | Notes |
|---|---|---|---|
| `/permission` | Layout → views/permission/* | admin, editor | Page/directive/role permission demos |
| `/icon` | Layout → views/icons | — | Icon browser |
| `/components` | Layout → views/components-demo/* | — | 8 component demos |
| `/charts` | Layout → views/charts/* | — | Chart demos |
| `/nested` | Layout → views/nested/* | — | Nested route demo |
| `/example` | Layout → views/example/* | — | CRUD example |
| `/tab` | Layout → views/tab | — | Tab demo |
| `/error` | Layout → views/error-page/* | admin | Error pages |
| `/error-log` | Layout → views/error-log | — | Error log viewer |
| `/excel` | Layout → views/excel/* | — | Excel export/import |
| `/zip` | Layout → views/zip | — | ZIP download |
| `/pdf` | Layout → views/pdf/* | — | PDF generation |
| `/theme` | Layout → views/theme | — | Theme picker |
| `/clipboard` | Layout → views/clipboard | — | Clipboard demo |
| `/i18n` | Layout → views/i18n-demo | — | Internationalization |
| `*` | redirect → `/404` | — | Catch-all (must be last) |

#### 8.1.2 Route Meta Properties Reference

| Property | Type | Default | Effect |
|---|---|---|---|
| `roles` | `string[]` | `undefined` (all roles) | Restrict route to listed roles |
| `title` | `string` | — | Displayed in sidebar, breadcrumb, and tagsView |
| `icon` | `string` | — | SVG icon name for sidebar (e.g., `'dashboard'`) |
| `hidden` | `boolean` | `false` | Hide from sidebar menu |
| `alwaysShow` | `boolean` | `false` | Always show root menu even with single child |
| `noCache` | `boolean` | `false` | Exclude from `keep-alive` caching |
| `affix` | `boolean` | `false` | Pin tag in tagsView (cannot be closed) |
| `breadcrumb` | `boolean` | `true` | Include in breadcrumb trail |
| `activeMenu` | `string` | — | Highlight this sidebar path instead of current route |
| `redirect` | `'noRedirect'` | — | Make breadcrumb item non-clickable |

#### 8.1.3 Router Reset Pattern

The router instance cannot remove dynamically added routes. The project uses a factory pattern to reset:

```javascript
const createRouter = () => new Router({
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes
})

const router = createRouter()

export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher  // Replace matcher to "remove" dynamic routes
}
```

This is called during logout and role changes to clear stale routes.

### 8.2 Vuex Store Modules

All modules are **namespaced**. Access via `store.dispatch('module/action')` or `store.getters['module/getter']`.

#### 8.2.1 `app` Module (`src/store/modules/app.js`)

**State:**

| Key | Type | Initial | Persistence |
|---|---|---|---|
| `sidebar.opened` | `boolean` | `Cookies.get('sidebarStatus') !== '0'` | Cookie `sidebarStatus` |
| `sidebar.withoutAnimation` | `boolean` | `false` | None |
| `device` | `string` | `'desktop'` | None |
| `size` | `string` | `Cookies.get('size') \|\| 'medium'` | Cookie `size` |

**Mutations:**

| Mutation | Payload | Effect |
|---|---|---|
| `TOGGLE_SIDEBAR` | — | Flips `sidebar.opened`, updates cookie |
| `CLOSE_SIDEBAR` | `{ withoutAnimation }` | Sets `sidebar.opened = false` |
| `TOGGLE_DEVICE` | `device` | Sets `device` to `'desktop'` or `'mobile'` |
| `SET_SIZE` | `size` | Sets Element UI size, updates cookie |

**Actions:**

| Action | Commits |
|---|---|
| `toggleSideBar` | `TOGGLE_SIDEBAR` |
| `closeSideBar` | `CLOSE_SIDEBAR` |
| `toggleDevice` | `TOGGLE_DEVICE` |
| `setSize` | `SET_SIZE` |

#### 8.2.2 `user` Module (`src/store/modules/user.js`)

**State:**

| Key | Type | Initial | Persistence |
|---|---|---|---|
| `token` | `string` | `getToken()` (from cookie) | Cookie `Admin-Token` |
| `name` | `string` | `''` | None |
| `avatar` | `string` | `''` | None |
| `introduction` | `string` | `''` | None |
| `roles` | `string[]` | `[]` | None |

**Mutations:**

| Mutation | Payload | Effect |
|---|---|---|
| `SET_TOKEN` | `token` | Sets `state.token` |
| `SET_INTRODUCTION` | `introduction` | Sets `state.introduction` |
| `SET_NAME` | `name` | Sets `state.name` |
| `SET_AVATAR` | `avatar` | Sets `state.avatar` |
| `SET_ROLES` | `roles` | Sets `state.roles` |

**Actions:**

| Action | Input | Behavior |
|---|---|---|
| `login` | `{ username, password }` | Calls `loginAPI()`, stores token in cookie + state |
| `getInfo` | — | Calls `getInfoAPI(token)`, validates roles array non-empty, commits name/avatar/roles/introduction |
| `logout` | — | Calls `logoutAPI()`, clears token + roles + cookie, resets router + tagsView |
| `resetToken` | — | Clears token + roles + cookie (no API call) |
| `changeRoles` | `role` | Sets token to role, calls `getInfo`, regenerates routes, clears tagsView, adds new routes |

#### 8.2.3 `permission` Module (`src/store/modules/permission.js`)

**State:**

| Key | Type | Initial |
|---|---|---|
| `routes` | `Route[]` | `[]` |
| `addRoutes` | `Route[]` | `[]` |

**Getters:**

| Getter | Returns |
|---|---|
| `permission_routes` | `state.routes` |

**Actions:**

| Action | Input | Behavior |
|---|---|---|
| `generateRoutes` | `roles` | If roles includes `'admin'` → all asyncRoutes. Otherwise → `filterAsyncRoutes()`. Commits concatenation of constant + filtered async routes. |

#### 8.2.4 `settings` Module (`src/store/modules/settings.js`)

**State** (initialized from `src/settings.js`):

| Key | Type | Default |
|---|---|---|
| `theme` | `string` | `#1890ff` |
| `showSettings` | `boolean` | `true` |
| `tagsView` | `boolean` | `true` |
| `fixedHeader` | `boolean` | `false` |
| `sidebarLogo` | `boolean` | `false` |

**Mutations:**

| Mutation | Payload | Effect |
|---|---|---|
| `CHANGE_SETTING` | `{ key, value }` | Sets `state[key] = value` (validates key exists) |

**Actions:**

| Action | Input | Behavior |
|---|---|---|
| `changeSetting` | `{ key, value }` | Commits `CHANGE_SETTING` |

#### 8.2.5 `tagsView` Module (`src/store/modules/tagsView.js`)

**State:**

| Key | Type | Purpose |
|---|---|---|
| `visitedViews` | `Route[]` | Tabs shown in tag bar |
| `cachedViews` | `string[]` | Component names in `keep-alive` include list |

**Actions:**

| Action | Behavior |
|---|---|
| `addView` | Adds to both visitedViews and cachedViews |
| `addVisitedView` | Adds route to visitedViews (deduped by path) |
| `addCachedView` | Adds component name to cachedViews if not `noCache` |
| `delView` | Removes from both arrays |
| `delCachedView` | Removes component name from cachedViews |
| `delOthersViews` | Removes all except the given view |
| `delAllViews` | Removes all except affixed views |
| `updateVisitedView` | Updates route metadata for existing view |

> [!IMPORTANT]
> Views with `meta.affix: true` (like Dashboard) are never removed from visitedViews, even on "close all". They are added on component mount in the TagsView component and preserved through all clear operations.

#### 8.2.6 `errorLog` Module (`src/store/modules/errorLog.js`)

**State:**

| Key | Type | Purpose |
|---|---|---|
| `logs` | `Error[]` | Array of error log entries |

**Actions:**

| Action | Behavior |
|---|---|
| `addErrorLog` | Pushes error to logs array |
| `clearErrorLog` | Empties logs array |

Error logs are populated by the global `Vue.config.errorHandler` (can be set up in `main.js`) and displayed via the ErrorLog component in the navbar.

### 8.3 Request Layer (`src/utils/request.js`)

The centralized Axios instance:

```javascript
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 5000
})
```

**Request interceptor:**

```javascript
service.interceptors.request.use(config => {
  if (store.getters.token) {
    config.headers['X-Token'] = getToken()
  }
  return config
})
```

**Response interceptor:**

```javascript
service.interceptors.response.use(
  response => {
    const res = response.data
    if (res.code !== 20000) {
      // Error handling (see Section 7.3)
      Message({ message: res.message || 'Error', type: 'error', duration: 5000 })
      if ([50008, 50012, 50014].includes(res.code)) {
        // Re-login dialog
      }
      return Promise.reject(new Error(res.message || 'Error'))
    }
    return res
  },
  error => {
    Message({ message: error.message, type: 'error', duration: 5000 })
    return Promise.reject(error)
  }
)
```

**API module pattern** (e.g., `src/api/article.js`):

```javascript
import request from '@/utils/request'

export function fetchList(query) {
  return request({
    url: '/vue-element-admin/article/list',
    method: 'get',
    params: query
  })
}

export function createArticle(data) {
  return request({
    url: '/vue-element-admin/article/create',
    method: 'post',
    data
  })
}
```

### 8.4 Custom Directives

#### 8.4.1 `v-permission`

**Location:** `src/directive/permission/`

**Purpose:** Remove DOM elements if the current user lacks required roles.

**Registration:**

```javascript
// Global (in main.js or wherever needed)
import permission from '@/directive/permission'
Vue.directive('permission', permission)

// Or per-component
import permission from '@/directive/permission'
export default {
  directives: { permission }
}
```

**Usage:**

```html
<el-button v-permission="['admin']">Delete All</el-button>
<el-tag v-permission="['admin','editor']">Edit</el-tag>
```

**Behavior:**
- `inserted` hook: checks `store.getters.roles` against bound array
- If no role matches: `el.parentNode.removeChild(el)` — permanently removes element
- If value is not an array or is empty: throws `"need roles! Like v-permission=['admin','editor']"`

#### 8.4.2 `v-clipboard`

**Location:** `src/directive/clipboard/`

**Dependency:** `clipboard` (npm package, v2.0.4)

**Usage:**

```html
<!-- Copy text -->
<el-button v-clipboard:copy="inputData" v-clipboard:success="onCopy" v-clipboard:error="onError">
  Copy
</el-button>
```

**Modifiers:**
- `:copy` — sets the text to copy
- `:cut` — sets the text to cut
- `:success` — callback function on successful copy
- `:error` — callback function on failed copy

**Implementation notes:**
- Creates a `Clipboard` instance on `bind`, stores it on `el._v_clipboard`
- Updates on `update` hook, destroys on `unbind`
- Throws `"you should npm install clipboard"` if the clipboard package is not installed

#### 8.4.3 `v-el-drag-dialog`

**Location:** `src/directive/el-drag-dialog/`

**Purpose:** Makes Element UI `el-dialog` header draggable.

**Usage:**

```html
<el-dialog v-el-drag-dialog title="Draggable Dialog" :visible.sync="visible">
  Dialog content
</el-dialog>
```

**Implementation:**
- Queries `.el-dialog__header` and `.el-dialog` within the directive element
- Sets `cursor: move` on header
- On `mousedown` → tracks `mousemove` → calculates delta → applies CSS `margin-left` and `margin-top`
- Cleans up on `mouseup`

#### 8.4.4 `v-el-table` (Adaptive)

**Location:** `src/directive/el-table/`

**Purpose:** Auto-adjusts `el-table` height to fill remaining viewport space.

**Usage:**

```html
<el-table v-el-height-adaptive-table="{bottomOffset: 30}" height="100px" :data="data">
  ...
</el-table>
```

**Parameters:**
- `bottomOffset`: pixels to reserve at the bottom (default varies)

**Requirement:** The `el-table` element **must** have a `height` attribute set. Without it, the directive throws: `"el-$table must set the height"`

**Implementation:**
- On `bind` and `window.resize`: calculates `window.innerHeight - el.getBoundingClientRect().top - bottomOffset`
- Sets the calculated value as `el.style.height`

#### 8.4.5 `v-waves`

**Location:** `src/directive/waves/`

**Purpose:** Material Design ripple effect on click.

**Usage:**

```html
<el-button v-waves>Click Me</el-button>
<el-button v-waves.light>Light Ripple</el-button>
```

**Modifiers:**
- `.light` — white ripple (for dark backgrounds)
- No modifier — uses element color or default

#### 8.4.6 `v-sticky`

**Location:** `src/directive/sticky.js`

**Purpose:** Sticky positioning with scroll listener (polyfill for `position: sticky`).

**Usage:**

```html
<div v-sticky="{ stickyTop: 0, zIndex: 1000 }">
  Sticks to top of viewport on scroll
</div>
```

**Parameters:**
- `stickyTop`: pixels from top when stuck (default: `0`)
- `zIndex`: z-index when stuck (default: `1000`)

### 8.5 Utility Functions (`src/utils/`)

#### 8.5.1 `auth.js` — Token Management

```javascript
import Cookies from 'js-cookie'
const TokenKey = 'Admin-Token'

export function getToken() { return Cookies.get(TokenKey) }
export function setToken(token) { return Cookies.set(TokenKey, token) }
export function removeToken() { return Cookies.remove(TokenKey) }
```

#### 8.5.2 `validate.js` — Input Validation

| Function | Signature | Validates |
|---|---|---|
| `validUsername` | `(str) → boolean` | Checks against hardcoded whitelist: `['admin', 'editor']` |
| `validURL` | `(url) → boolean` | Regex: `http(s)://` + valid URL format |
| `validLowerCase` | `(str) → boolean` | Only lowercase letters |
| `validUpperCase` | `(str) → boolean` | Only uppercase letters |
| `validAlphabets` | `(str) → boolean` | Only letters (upper or lower) |
| `validEmail` | `(str) → boolean` | RFC-ish email regex |
| `isString` | `(str) → boolean` | `typeof === 'string' \|\| instanceof String` |
| `isArray` | `(arg) → boolean` | `Array.isArray` with fallback |

> [!NOTE]
> `validUsername` is a mock-only convenience — it hardcodes `['admin', 'editor']`. Replace with real validation in production.

#### 8.5.3 `index.js` — General Utilities

| Function | Signature | Purpose |
|---|---|---|
| `parseTime` | `(time, cFormat)` | Formats Date/timestamp/string to `{y}-{m}-{d} {h}:{i}:{s}` (customizable format string) |
| `formatTime` | `(time)` | Relative time in Chinese (刚刚, N分钟前, N小时前, N天前) |
| `getQueryObject` | `(url)` | Parses URL query string to object |
| `getByteLen` | `(val)` | String length counting CJK chars as 2 |
| `cleanArray` | `(actual)` | Filters falsy values |
| `param` | `(json)` | Converts object to URL query string |
| `param2Obj` | `(url)` | Parses URL query params to object (handles encoding) |
| `html2Text` | `(val)` | Strips HTML tags via DOM |
| `objectMerge` | `(target, source)` | Deep merge objects |
| `toggleClass` | `(element, className)` | Toggle CSS class on DOM element |
| `getTime` | `(type)` | Returns timestamp for start-of-today or start-of-month |
| `debounce` | `(func, wait, immediate)` | Standard debounce implementation |
| `deepClone` | `(source)` | Deep clone via JSON parse/stringify (throws `"error arguments"` if source is falsy) |
| `uniqueArr` | `(arr)` | Deduplicates array via `Array.from(new Set())` |
| `createUniqueString` | `()` | Generates unique string from timestamp + random |
| `hasClass` | `(ele, cls)` | Check if element has CSS class |
| `addClass` | `(ele, cls)` | Add CSS class to element |
| `removeClass` | `(ele, cls)` | Remove CSS class from element |

#### 8.5.4 `permission.js` — Programmatic Permission Check

```javascript
export default function checkPermission(value) {
  if (value && value instanceof Array && value.length > 0) {
    const roles = store.getters && store.getters.roles
    const permissionRoles = value
    return roles.some(role => permissionRoles.includes(role))
  } else {
    console.error(`need roles! Like checkPermission(['admin','editor'])`)
    return false
  }
}
```

#### 8.5.5 `scroll-to.js` — Smooth Scroll

```javascript
export function scrollTo(to, duration, callback)
```

Eases scroll position using `requestAnimationFrame`. Used by BackToTop component.

#### 8.5.6 `get-page-title.js` — Document Title

```javascript
import defaultSettings from '@/settings'
const title = defaultSettings.title || 'vue-element-admin'

export default function getPageTitle(pageTitle) {
  if (pageTitle) {
    return `${pageTitle} - ${title}`
  }
  return `${title}`
}
```

#### 8.5.7 `open-window.js` — Window Opener

```javascript
export default function openWindow(url, title, w, h)
```

Opens a centered popup window with specified dimensions.

#### 8.5.8 `clipboard.js` — Copy to Clipboard

```javascript
export default function handleClipboard(text, event)
```

Wraps `clipboard.js` for one-shot copy operations with Element UI success/error messages.

### 8.6 Component Reference

#### 8.6.1 BackToTop

**Path:** `src/components/BackToTop/index.vue`

**Props:**

| Prop | Type | Default | Description |
|---|---|---|---|
| `visibilityHeight` | `Number` | `400` | Scroll distance (px) before showing |
| `backPosition` | `Number` | `0` | Scroll-to target position |
| `customStyle` | `Object` | `{ right: '50px', bottom: '50px', width: '40px', height: '40px', ... }` | Button styling |
| `transitionName` | `String` | `'fade'` | CSS transition name |

Uses `scroll-to.js` for smooth scrolling animation.

#### 8.6.2 Breadcrumb

**Path:** `src/components/Breadcrumb/index.vue`

**Dependencies:** `path-to-regexp`

Watches `$route`, computes breadcrumb items from `$route.matched`. Filters items that have `meta.title` and are not set to `meta.breadcrumb === false`. Routes with `redirect: 'noRedirect'` render as plain text (not clickable).

#### 8.6.3 DndList (Drag-and-Drop List)

**Path:** `src/components/DndList/index.vue`

**Dependencies:** `vuedraggable`

**Props:**

| Prop | Type | Default |
|---|---|---|
| `list1` | `Array` | `[]` |
| `list2` | `Array` | `[]` |
| `list1Title` | `String` | `'list1'` |
| `list2Title` | `String` | `'list2'` |
| `width1` | `String` | `'48%'` |
| `width2` | `String` | `'48%'` |

Two side-by-side draggable lists for reordering and transfer between lists.

#### 8.6.4 DragSelect

**Path:** `src/components/DragSelect/index.vue`

**Dependencies:** `sortablejs`

Wraps `el-select` with drag-and-drop reordering of selected tags. Initializes `Sortable` on the `.el-select__tags > span` container.

#### 8.6.5 Dropzone

**Path:** `src/components/Dropzone/index.vue`

**Dependencies:** `dropzone`

**Props:**

| Prop | Type | Default |
|---|---|---|
| `id` | `String` | `required` |
| `url` | `String` | `required` |
| `clickable` | `Boolean` | `true` |
| `defaultMsg` | `String` | `'Drop files here to upload'` |
| `acceptedFiles` | `String` | `''` |
| `thumbnailHeight` | `Number` | `200` |
| `thumbnailWidth` | `Number` | `200` |
| `showRemoveLink` | `Boolean` | `true` |
| `maxFilesize` | `Number` | `2` (MB) |
| `maxFiles` | `Number` | `3` |
| `autoProcessQueue` | `Boolean` | `true` |
| `useCustomDropzoneOptions` | `Boolean` | `false` |
| `dropzoneOptions` | `Object` | `{}` |
| `coerce` | `Function` | identity |

**Events:** `dropzone-fileAdded`, `dropzone-success`, `dropzone-complete`, `dropzone-error`, `dropzone-removedFile`, `dropzone-maxFilesExceeded`, etc.

#### 8.6.6 ErrorLog

**Path:** `src/components/ErrorLog/index.vue`

Displays a badge with error count. Clicking opens a dialog showing all logged errors with URL, message, and info (stringified). Reads from Vuex `errorLog` module. Only visible when there are errors.

#### 8.6.7 GithubCorner

**Path:** `src/components/GithubCorner/index.vue`

Animated SVG GitHub link in the top-right corner of the page. Accepts no props — URL is hardcoded.

#### 8.6.8 Hamburger

**Path:** `src/components/Hamburger/index.vue`

**Props:**

| Prop | Type | Default |
|---|---|---|
| `isActive` | `Boolean` | `false` |

**Events:** `toggleClick`

SVG-based hamburger icon with CSS transition. Rotates 180 degrees when `isActive` is true.

#### 8.6.9 HeaderSearch

**Path:** `src/components/HeaderSearch/index.vue`

**Dependencies:** `fuse.js`, `path-to-regexp`

- Collects all routes with `meta.title` from `permission_routes` getter
- Builds a Fuse.js search index on component mount
- Shows `el-select` with remote search on click
- Navigates to selected route on selection
- Closes on `click` outside (document event listener)
- Fuse options: `shouldSort: true`, `threshold: 0.4`, `maxPatternLength: 36`

#### 8.6.10 ImageCropper

**Path:** `src/components/ImageCropper/index.vue`

Custom canvas-based image cropping component supporting:
- File selection via input or drag-and-drop
- Touch/mouse crop area positioning and resizing
- Rotation
- Zoom
- Upload to configurable URL with custom headers/data
- Preview in real-time

**Props:**

| Prop | Type | Default |
|---|---|---|
| `field` | `String` | `'avatar'` |
| `value` | `Boolean` | `true` |
| `url` | `String` | `''` |
| `method` | `String` | `'POST'` |
| `params` | `Object` | `null` |
| `headers` | `Object` | `null` |
| `langType` | `String` | `'zh'` |
| `width` | `Number` | `200` |
| `height` | `Number` | `200` |
| `noCircle` | `Boolean` | `false` |
| `noSquare` | `Boolean` | `false` |
| `noRotate` | `Boolean` | `true` |
| `withCredentials` | `Boolean` | `false` |
| `imgFormat` | `String` | `'png'` |

#### 8.6.11 JsonEditor

**Path:** `src/components/JsonEditor/index.vue`

**Dependencies:** `codemirror`, `jsonlint`

**Props:**

| Prop | Type |
|---|---|
| `value` | `String` |

**Events:** `changed` (emits parsed JSON), `json-error` (emits validation error)

Wraps CodeMirror with JSON mode, lint addon (using `jsonlint`), autoCloseBrackets, matchBrackets, and custom `gutters` styling.

#### 8.6.12 Kanban

**Path:** `src/components/Kanban/Board.vue`

**Dependencies:** `vuedraggable`

**Props:**

| Prop | Type | Default |
|---|---|---|
| `data` | `Array` | `required` |
| `group` | `String` | `'kanban'` |

Kanban board with draggable cards between columns.

#### 8.6.13 MarkdownEditor

**Path:** `src/components/MarkdownEditor/index.vue`

**Dependencies:** `tui-editor`

**Props:**

| Prop | Type | Default |
|---|---|---|
| `value` | `String` | `''` |
| `id` | `String` | auto-generated |
| `options` | `Object` | `{}` |
| `mode` | `String` | `'markdown'` |
| `height` | `String` | `'300px'` |
| `language` | `String` | `'en_US'` |

**Events:** `input` (v-model compatible, emits markdown string)

Wraps Toast UI Editor (`tui-editor` v1). Options merged with defaults: `{ minHeight: '200px', previewStyle: 'vertical', useCommandShortcut: true, useDefaultHTMLSanitizer: true, usageStatistics: false }`.

#### 8.6.14 MDinput (Material Design Input)

**Path:** `src/components/MDinput/index.vue`

**Props:**

| Prop | Type | Default |
|---|---|---|
| `name` | `String` | — |
| `type` | `String` | `'text'` |
| `value` | varies | — |
| `placeholder` | `String` | `''` |
| `readonly` | `Boolean` | `false` |
| `disabled` | `Boolean` | `false` |
| `min` | `String` | `''` |
| `max` | `String` | `''` |
| `step` | `String` | `''` |
| `minlength` | `Number` | `0` |
| `maxlength` | `Number` | `524288` |
| `required` | `Boolean` | `true` |
| `autoComplete` | `String` | `'off'` |
| `icon` | `String` | — |

Floating-label Material Design styled input field.

#### 8.6.15 Pagination

**Path:** `src/components/Pagination/index.vue`

**Props:**

| Prop | Type | Default |
|---|---|---|
| `total` | `Number` | `required` |
| `page` | `Number` | `1` |
| `limit` | `Number` | `20` |
| `pageSizes` | `Array` | `[10, 20, 30, 50]` |
| `layout` | `String` | `'total, sizes, prev, pager, next, jumper'` |
| `background` | `Boolean` | `true` |
| `autoScroll` | `Boolean` | `true` |
| `hidden` | `Boolean` | `false` |

**Events:** `update:page` (`.sync`), `update:limit` (`.sync`), `pagination`

Wraps `el-pagination` with scroll-to-top on page change (via `scroll-to.js`).

#### 8.6.16 PanThumb

**Path:** `src/components/PanThumb/index.vue`

**Props:**

| Prop | Type | Default |
|---|---|---|
| `image` | `String` | `required` |
| `zIndex` | `Number` | `1` |
| `width` | `String` | `'150px'` |
| `height` | `String` | `'150px'` |

Hover-animated thumbnail with CSS 3D transform and text overlay.

#### 8.6.17 RightPanel

**Path:** `src/components/RightPanel/index.vue`

**Props:**

| Prop | Type | Default |
|---|---|---|
| `clickNotClose` | `Boolean` | `false` |
| `buttonTop` | `Number` | `250` |

Slides in from the right edge. Contains a handle button (gear icon) and slot for content. Click outside closes the panel (unless `clickNotClose` is true). Adds `.showRightPanel` class to `body`.

#### 8.6.18 Screenfull

**Path:** `src/components/Screenfull/index.vue`

**Dependencies:** `screenfull`

Toggle fullscreen mode. Shows warning if fullscreen API is not supported. Listens to screenfull `change` event.

#### 8.6.19 SizeSelect

**Path:** `src/components/SizeSelect/index.vue`

Dropdown to change Element UI component size globally. Options: `default`, `medium`, `small`, `mini`. Dispatches `app/setSize` and refreshes the page via `location.reload()`.

#### 8.6.20 Sticky

**Path:** `src/components/Sticky/index.vue`

**Props:**

| Prop | Type | Default |
|---|---|---|
| `stickyTop` | `Number` | `0` |
| `zIndex` | `Number` | `1` |
| `className` | `String` | `''` |

Sticky positioning component. Creates a placeholder element to prevent layout shift. Listens to scroll/resize events. Activates when element reaches `stickyTop` distance from viewport top.

#### 8.6.21 SvgIcon

**Path:** `src/components/SvgIcon/index.vue`

**Props:**

| Prop | Type | Default |
|---|---|---|
| `iconClass` | `String` | `required` |
| `className` | `String` | `''` |

Renders `<svg><use :xlink:href="#icon-{iconClass}"></use></svg>`. Supports both internal icons (SVG sprite) and external icons (full URL — rendered as `<div>` with `mask` CSS).

#### 8.6.22 TextHoverEffect

**Path:** `src/components/TextHoverEffect/Mallki.vue`

**Props:**

| Prop | Type | Default |
|---|---|---|
| `text` | `String` | `required` |
| `className` | `String` | `''` |

CSS-only text hover animation using data attributes and pseudo-elements.

#### 8.6.23 ThemePicker

**Path:** `src/components/ThemePicker/index.vue`

**Dependencies:** Element UI theme generation

Uses `el-color-picker` to select a theme color. On change:
1. Fetches Element UI CSS from CDN (`unpkg.com/element-ui@{version}/lib/theme-chalk/index.css`)
2. Replaces all color values using regex
3. Generates tint colors (for hover states, etc.)
4. Appends modified CSS as a `<style>` tag in `<head>`
5. Commits new theme to Vuex settings store

> [!TIP]
> The ThemePicker downloads and reprocesses the full Element UI CSS on every color change. For production, consider pre-generating theme files using Element's theme tool (`element-theme`) and switching between them.

#### 8.6.24 Tinymce (Rich Text Editor)

**Path:** `src/components/Tinymce/index.vue`

**Props:**

| Prop | Type | Default |
|---|---|---|
| `id` | `String` | auto-generated |
| `value` | `String` | `''` |
| `toolbar` | `Array` | Full toolbar config |
| `menubar` | `String` | `'file edit insert view format table'` |
| `height` | `Number\|String` | `360` |
| `width` | `Number\|String` | `'auto'` |

**External dependency:** Loads TinyMCE 4 from CDN (`https://cdn.tinymce.com/4/tinymce.min.js`) via `script-loader`. The `dynamicLoadScript` function in `src/components/Tinymce/dynamicLoadScript.js` checks if the script is already loaded before injecting.

**Events:** `input` (v-model compatible)

Includes custom `EditorImage` component for image upload within the editor (uses upload dialog, not TinyMCE's built-in image plugin).

#### 8.6.25 UploadExcel

**Path:** `src/components/UploadExcel/index.vue`

**Dependencies:** `xlsx`

**Props:**

| Prop | Type | Default |
|---|---|---|
| `beforeUpload` | `Function` | — |
| `onSuccess` | `Function` | — |

Accepts `.xlsx`, `.xls`, `.csv` files via click or drag-drop. Reads file with `FileReader`, parses with `XLSX.read()`, and emits `{ header, results }` to `onSuccess` callback.

### 8.7 Global Filters (`src/filters/index.js`)

All filters are registered globally in `main.js`:

| Filter | Input | Output | Example |
|---|---|---|---|
| `parseTime` | `time, format` | Formatted date string | `{{ timestamp \| parseTime('{y}-{m}-{d}') }}` → `2024-10-24` |
| `formatTime` | `timestamp` | Relative time (Chinese) | `{{ timestamp \| formatTime }}` → `3分钟前` |
| `timeAgo` | `time` | Relative time (English) | `{{ timestamp \| timeAgo }}` → `3 minutes ago` |
| `numberFormatter` | `num, digits` | Short number | `{{ 12500 \| numberFormatter(1) }}` → `12.5k` |
| `toThousandFilter` | `num` | Comma-separated | `{{ 1234567 \| toThousandFilter }}` → `1,234,567` |
| `uppercaseFirst` | `string` | Capitalized | `{{ 'hello' \| uppercaseFirst }}` → `Hello` |

### 8.8 CSS Architecture

#### 8.8.1 SCSS File Structure

| File | Purpose |
|---|---|
| `src/styles/index.scss` | Entry point — imports all others, sets global styles |
| `src/styles/variables.scss` | SCSS variables (colors, sidebar dimensions, header height) |
| `src/styles/mixin.scss` | SCSS mixins (`clearfix`, `scrollBar`, `relative`) |
| `src/styles/transition.scss` | Vue transition classes (`fade-transform`, `breadcrumb-enter`, sidebar) |
| `src/styles/sidebar.scss` | Sidebar-specific styles (open/closed states, submenu, mobile) |
| `src/styles/btn.scss` | Custom button styles |
| `src/styles/element-ui.scss` | Element UI overrides |
| `src/styles/element-variables.scss` | Element UI SCSS variable overrides (colors, fonts) |

#### 8.8.2 Key SCSS Variables

```scss
// src/styles/variables.scss
$--color-primary: #1890ff;           // Primary color (can be overridden)
$--font-path: '~element-ui/lib/theme-chalk/fonts';

// Sidebar
$sideBarWidth: 210px;                // Expanded sidebar width
$hideSideBarWidth: 54px;             // Collapsed sidebar width
$sideBarDuration: 0.28s;             // Transition duration

// Header
$navBarHeight: 50px;                 // Top navbar height
$tagViewHeight: 34px;                // Tags view bar height
```

#### 8.8.3 Element UI Theme Variables

```scss
// src/styles/element-variables.scss
$--color-primary: #1890ff;
$--color-success: #13CE66;
$--color-warning: #FFBA00;
$--color-danger: #FF4949;
$--color-info: #1890ff;

// Sidebar menu colors
$--menu-dark-bg: #304156;
$--menu-dark-hover-bg: #263445;
$--menu-light-bg: #fff;
$--submenu-dark-bg: #1f2d3d;
$--submenu-light-bg: #e5e7eb;
```

#### 8.8.4 CSS Transition Classes

| Name | Used On | Effect |
|---|---|---|
| `fade-transform` | AppMain router-view | Opacity fade + slight vertical translate |
| `breadcrumb-enter` | Breadcrumb items | Translate X + opacity |
| `sidebar-logo-fade` | Sidebar logo | Opacity transition |
| `el-zoom-in-center` | Element UI | Scale transform from center |

### 8.9 Mock Server Architecture

#### 8.9.1 File Structure

```
mock/
├── index.js           # Exports mockXHR() for production + mocks array for dev
├── mock-server.js     # Express-style route registration for webpack-dev-server
├── utils.js           # param2Obj helper
├── user.js            # User login/info/logout + role CRUD
├── role/              # Role management
│   ├── index.js       # Routes (getRoles, addRole, deleteRole, updateRole)
│   └── routes.js      # Static route data (asyncRoutes mirror)
├── article.js         # Article CRUD + pageviews
├── remote-search.js   # User search
└── transaction.js     # Transaction list
```

#### 8.9.2 Mock User Database

The mock creates in-memory user data:

| Username | Password | Token | Roles |
|---|---|---|---|
| `admin` | `111111` | `admin-token` | `['admin']` |
| `editor` | `111111` | `editor-token` | `['editor']` |

Login validates against this map. `getInfo` maps tokens back to user data.

#### 8.9.3 Development Mode Registration

`mock/mock-server.js` hooks into webpack-dev-server's `devServer.before`:

```javascript
module.exports = function(app) {
  // Register mock routes on the dev server
  const mocksForServer = mocks.map(route => {
    return responseFake(route.url, route.type, route.response)
  })
  for (const mock of mocksForServer) {
    app[mock.type](mock.url, mock.handler)
  }
}
```

`responseFake` wraps each handler to:
1. Parse query params from URL
2. Parse body from request (for POST/PUT)
3. Call the response function with `{ query, body }` as `config`
4. Send the result as JSON

#### 8.9.4 Production Mode Registration

`mock/index.js` exports `mockXHR()`:

```javascript
export function mockXHR() {
  Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
  Mock.XHR.prototype.send = function() {
    if (this.custom.xhr) {
      this.custom.xhr.withCredentials = this.withCredentials || false
      // ... proxy through to real XHR if no mock matches
    }
    this.proxy_send(...arguments)
  }

  for (const i of mocks) {
    Mock.mock(new RegExp(i.url), i.type || 'get', (options) => {
      return i.response(options)
    })
  }
}
```

This intercepts `XMLHttpRequest` globally using Mock.js.

#### 8.9.5 Mock API Reference

| Method | URL Pattern | Handler | Notes |
|---|---|---|---|
| POST | `/vue-element-admin/user/login` | Validates username/password, returns token | Returns `{ code: 60204 }` on invalid credentials |
| GET | `/vue-element-admin/user/info` | Maps token to user data | Query param: `token` |
| POST | `/vue-element-admin/user/logout` | Always succeeds | Returns `{ code: 20000 }` |
| GET | `/vue-element-admin/roles` | Returns all roles with routes | Static + dynamic combo |
| POST | `/vue-element-admin/role` | Creates role | Body: `{ key, name, description, routes }` |
| PUT | `/vue-element-admin/role/:id` | Updates role | Body: `{ key, name, description, routes }` |
| DELETE | `/vue-element-admin/role/:id` | Deletes role | No body |
| GET | `/vue-element-admin/routes` | Returns full route tree | For route management UI |
| GET | `/vue-element-admin/article/list` | Paginated article list | Query: `{ type, title, page, limit, sort }` |
| GET | `/vue-element-admin/article/detail` | Single article | Query: `{ id }` |
| GET | `/vue-element-admin/article/pv` | Pageview stats | Returns 7-day PV data |
| POST | `/vue-element-admin/article/create` | Create article | Body: article object |
| POST | `/vue-element-admin/article/update` | Update article | Body: article object |
| GET | `/vue-element-admin/search/user` | Search users by name | Query: `{ name }` |
| GET | `/vue-element-admin/transaction/list` | Transaction list | Returns 20 mock transactions |

### 8.10 Views Reference

The `src/views/` directory contains all page-level components:

| Directory | Route | Components | Purpose |
|---|---|---|---|
| `dashboard/` | `/dashboard` | `admin/index.vue`, `editor/index.vue` | Dashboard with charts, stats (admin vs editor variants) |
| `login/` | `/login` | `index.vue`, `auth-redirect.vue` | Login form, OAuth redirect handler |
| `profile/` | `/profile` | `index.vue` | User profile (tabs: Activity, Timeline, Account) |
| `documentation/` | `/documentation` | `index.vue` | External link to docs |
| `guide/` | `/guide` | `index.vue` | driver.js feature tour |
| `permission/` | `/permission/*` | `page.vue`, `directive.vue`, `role.vue` | Permission demos |
| `icons/` | `/icon` | `index.vue`, `element-icons.vue`, `svg-icons.vue` | Icon browser |
| `components-demo/` | `/components/*` | 8 demo pages | Component showcases |
| `charts/` | `/charts/*` | `keyboard.vue`, `line.vue`, `mix-chart.vue` | ECharts demos |
| `nested/` | `/nested/*` | `menu1/`, `menu2/` | Nested route demo (3 levels) |
| `example/` | `/example/*` | `list.vue`, `create.vue`, `edit.vue` | Article CRUD example |
| `tab/` | `/tab` | `index.vue` | Dynamic tab creation |
| `error-page/` | `/404`, `/401` | `404.vue`, `401.vue` | Error pages |
| `error-log/` | `/error-log` | `index.vue` | Error log display |
| `excel/` | `/excel/*` | 4 views | Excel export/import/merge |
| `zip/` | `/zip` | `index.vue` | ZIP download |
| `pdf/` | `/pdf/*` | `index.vue`, `download.vue` | PDF generation |
| `theme/` | `/theme` | `index.vue` | Theme color picker |
| `clipboard/` | `/clipboard` | `index.vue` | Clipboard demo |
| `i18n-demo/` | `/i18n` | `index.vue`, `local.js` | i18n demo |
| `redirect/` | `/redirect/:path` | `index.vue` | Internal redirect handler |

### 8.11 Testing

#### 8.11.1 Test Structure

```
tests/
└── unit/
    ├── components/
    │   ├── Hamburger.spec.js    # Click toggle, isActive visual state
    │   └── SvgIcon.spec.js      # Icon class rendering, external icon detection
    └── utils/
        ├── formatTime.spec.js   # Relative time formatting
        ├── param2Obj.spec.js    # URL query string parsing
        ├── parseTime.spec.js    # Date formatting
        └── validate.spec.js     # URL, email, string, number validation
```

#### 8.11.2 Running Tests

```bash
# Run all tests
npm run test:unit

# Run with coverage
npx vue-cli-service test:unit --coverage

# Run specific test
npx vue-cli-service test:unit --testPathPattern="validate"

# CI mode (lint + test)
npm run test:ci
```

#### 8.11.3 Coverage Configuration

Collected from:
- `src/utils/**/*.{js,vue}` (excluding `auth.js`, `request.js`)
- `src/components/**/*.{js,vue}`

#### 8.11.4 Writing New Tests

Follow the existing pattern:

```javascript
import { validURL, validEmail } from '@/utils/validate'

describe('Utils:validate', () => {
  it('validURL', () => {
    expect(validURL('https://example.com')).toBe(true)
    expect(validURL('not-a-url')).toBe(false)
  })
})
```

Component tests use `@vue/test-utils`:

```javascript
import { shallowMount } from '@vue/test-utils'
import Hamburger from '@/components/Hamburger/index.vue'

describe('Hamburger.vue', () => {
  it('toggle click', () => {
    const wrapper = shallowMount(Hamburger)
    wrapper.find('.hamburger').trigger('click')
    expect(wrapper.emitted().toggleClick).toBeTruthy()
  })
})
```

---

## 9. Integration Points

### 9.1 Connecting to a Real API

To replace the mock server with a real backend:

1. **Remove mock initialization from `src/main.js`:**

```javascript
// DELETE these lines:
if (process.env.NODE_ENV === 'production') {
  const { mockXHR } = require('../mock')
  mockXHR()
}
```

2. **Remove mock from `vue.config.js` dev server:**

```javascript
// DELETE:
devServer: {
  before: require('./mock/mock-server.js')
}
```

3. **Update `.env.production`:**

```env
VUE_APP_BASE_API = 'https://api.your-domain.com'
```

4. **Configure proxy for development** (in `vue.config.js`):

```javascript
devServer: {
  proxy: {
    '/dev-api': {
      target: 'http://localhost:8080',
      changeOrigin: true,
      pathRewrite: { '^/dev-api': '' }
    }
  }
}
```

5. **Adapt response handling** in `src/utils/request.js`:

The interceptor expects `{ code: 20000, data: ... }`. If your API uses standard HTTP status codes:

```javascript
service.interceptors.response.use(
  response => {
    return response.data  // Return data directly
  },
  error => {
    const { status } = error.response || {}
    if (status === 401) {
      // Handle re-login
    }
    Message({ message: error.message, type: 'error', duration: 5000 })
    return Promise.reject(error)
  }
)
```

6. **Update auth token handling:**

Replace `X-Token` header with your backend's expected format (e.g., `Authorization: Bearer <token>`):

```javascript
service.interceptors.request.use(config => {
  if (store.getters.token) {
    config.headers['Authorization'] = `Bearer ${getToken()}`
  }
  return config
})
```

### 9.2 API Contract

For compatibility with the existing frontend code, a real backend should implement these endpoints:

| Method | Path | Request | Response |
|---|---|---|---|
| POST | `/user/login` | `{ username, password }` | `{ code: 20000, data: { token } }` |
| GET | `/user/info` | Header: `X-Token` | `{ code: 20000, data: { roles: [], name, avatar, introduction } }` |
| POST | `/user/logout` | Header: `X-Token` | `{ code: 20000, data: 'success' }` |
| GET | `/roles` | — | `{ code: 20000, data: [{ key, name, description, routes }] }` |
| GET | `/routes` | — | `{ code: 20000, data: routeTree }` |
| GET | `/article/list` | `?page=1&limit=20&title=&type=&sort=` | `{ code: 20000, data: { total, items } }` |

> [!WARNING]
> The `roles` field in the user info response **must** be a non-empty array. The permission system checks `roles.length > 0` — returning an empty array or omitting it causes an infinite redirect loop between the login page and the guard.

### 9.3 Adding New Route Modules

Step-by-step to add a new feature route:

1. **Create the view** (use plop or manually):

```bash
npm run new
# Select "view", enter name
```

Or manually create `src/views/my-feature/index.vue`:

```vue
<template>
  <div class="my-feature-container">
    <!-- content -->
  </div>
</template>

<script>
export default {
  name: 'MyFeature',  // MUST match route for keep-alive
}
</script>

<style scoped>
.my-feature-container {
  padding: 20px;
}
</style>
```

2. **Add to router** (`src/router/index.js`):

```javascript
// In asyncRoutes array:
{
  path: '/my-feature',
  component: Layout,
  children: [
    {
      path: 'index',
      component: () => import('@/views/my-feature/index'),
      name: 'MyFeature',
      meta: {
        title: 'My Feature',
        icon: 'component',
        roles: ['admin', 'editor']
      }
    }
  ]
}
```

3. **Add API module** (if needed):

```javascript
// src/api/my-feature.js
import request from '@/utils/request'

export function fetchData(params) {
  return request({ url: '/my-feature/list', method: 'get', params })
}
```

4. **Add mock** (for development):

```javascript
// mock/my-feature.js
module.exports = [
  {
    url: '/vue-element-admin/my-feature/list',
    type: 'get',
    response: config => ({
      code: 20000,
      data: { items: [], total: 0 }
    })
  }
]
```

Register in `mock/index.js`:
```javascript
const myFeature = require('./my-feature')
const mocks = [...user, ...role, ...article, ...search, ...myFeature]
```

5. **Add Vuex module** (if complex state needed):

```bash
npm run new
# Select "store", enter name
```

Register in `src/store/index.js`:
```javascript
import myFeature from './modules/my-feature'
// In modules: { ..., myFeature }
```

### 9.4 Adding New Components

1. **Create component** (use plop or manually):

```bash
npm run new
# Select "component", enter name
```

Creates `src/components/MyComponent/index.vue`.

2. **Use in views:**

```javascript
import MyComponent from '@/components/MyComponent'

export default {
  components: { MyComponent }
}
```

3. **Register globally** (if used in 3+ places):

```javascript
// src/main.js or a dedicated global-components.js
import MyComponent from '@/components/MyComponent'
Vue.component('MyComponent', MyComponent)
```

### 9.5 Adding SVG Icons

1. Place the `.svg` file in `src/icons/svg/`
2. Run SVGO optimization:
   ```bash
   npm run svgo
   ```
3. Use immediately — no registration needed:
   ```html
   <svg-icon icon-class="my-icon-name" />
   ```

The `require.context('./svg', false, /\.svg$/)` in `src/icons/index.js` auto-imports all SVGs.

### 9.6 Internationalization (i18n)

The project includes an i18n demo at `src/views/i18n-demo/` but does not have full i18n integrated. To add:

1. Install `vue-i18n`:
   ```bash
   npm install vue-i18n@8
   ```

2. Create language files in `src/lang/`:
   ```javascript
   // src/lang/en.js
   export default { navbar: { dashboard: 'Dashboard', ... } }
   // src/lang/zh.js
   export default { navbar: { dashboard: '首页', ... } }
   ```

3. Initialize in `main.js`:
   ```javascript
   import VueI18n from 'vue-i18n'
   Vue.use(VueI18n)
   const i18n = new VueI18n({ locale: 'en', messages: { en, zh } })
   new Vue({ i18n, ... })
   ```

4. Use Element UI's `locale` with i18n integration for component translations.

### 9.7 Excel Operations

The project includes full Excel integration via the `xlsx` library:

**Export to Excel** (`src/vendor/Export2Excel.js`):

```javascript
import('@/vendor/Export2Excel').then(excel => {
  excel.export_json_to_excel({
    header: ['Id', 'Name', 'Age'],
    data: [[1, 'Alice', 30], [2, 'Bob', 25]],
    filename: 'user-list',
    autoWidth: true,
    bookType: 'xlsx'     // xlsx, csv, txt
  })
})
```

**Export with multi-header/merge cells:**

```javascript
excel.export_json_to_excel({
  multiHeader: [['Header Group 1', '', '', 'Header Group 2', '', '']],
  header: ['Id', 'Name', 'Age', 'City', 'State', 'Zip'],
  data: tableData,
  merges: ['A1:C1', 'D1:F1'],
  filename: 'complex-table'
})
```

**Import from Excel** (via UploadExcel component):

```javascript
handleSuccess({ results, header }) {
  // results: array of row objects
  // header: array of column names
  this.tableData = results
  this.tableHeader = header
}
```

### 9.8 ZIP File Generation

Via `jszip` and `file-saver` (`src/vendor/Export2Zip.js`):

```javascript
import('@/vendor/Export2Zip').then(zip => {
  zip.export_txt_to_zip(
    ['file1.txt', 'file2.txt'],    // filenames
    ['content1', 'content2'],       // contents
    'archive'                       // zip filename
  )
})
```

### 9.9 ECharts Integration

Charts use ECharts 4.2.1. The pattern used in views:

```javascript
import echarts from 'echarts'
require('echarts/theme/macarons')  // Theme

export default {
  mounted() {
    this.chart = echarts.init(this.$refs.chart, 'macarons')
    this.chart.setOption({ /* config */ })
    window.addEventListener('resize', this.__resizeHandler)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.__resizeHandler)
    if (this.chart) { this.chart.dispose() }
  }
}
```

> [!TIP]
> Always dispose ECharts instances in `beforeDestroy` and remove resize listeners to prevent memory leaks, especially when views are cached by `keep-alive`.

### 9.10 Error Logging Integration

The ErrorLog component shows client-side errors. To integrate with a backend error tracking service:

```javascript
// src/main.js
import store from './store'

Vue.config.errorHandler = function(err, vm, info) {
  store.dispatch('errorLog/addErrorLog', {
    err,
    vm,
    info,
    url: window.location.href
  })

  // Send to external service (Sentry, etc.)
  // Sentry.captureException(err)
}
```

The ErrorLog component in the navbar shows a bug icon with a badge count. Clicking it reveals all captured errors.

---

## 10. Troubleshooting

### Error: `"need roles! Like v-permission=['admin','editor']"`

**Source:** `src/directive/permission/permission.js:20`

**Cause:** The `v-permission` directive was used without a valid array value, or with an empty array.

**Solution:** Provide a non-empty array of role strings:
```html
<!-- Wrong -->
<el-button v-permission>Delete</el-button>
<el-button v-permission="'admin'">Delete</el-button>

<!-- Correct -->
<el-button v-permission="['admin']">Delete</el-button>
```

### Error: `"el-$table must set the height"`

**Source:** `src/directive/el-table/adaptive.js:16`

**Cause:** The `v-el-height-adaptive-table` directive requires the `height` attribute on `el-table`.

**Solution:**
```html
<!-- Wrong -->
<el-table v-el-height-adaptive-table :data="data">

<!-- Correct -->
<el-table v-el-height-adaptive-table height="100px" :data="data">
```

The initial `height` value is replaced by the adaptive calculation — it just needs to be present.

### Error: `"error arguments"` in `deepClone`

**Source:** `src/utils/index.js:298`

**Cause:** `deepClone()` was called with a falsy value (`null`, `undefined`, `0`, `''`).

**Solution:** Validate input before calling:
```javascript
if (sourceData) {
  const cloned = deepClone(sourceData)
}
```

### Error: `"you should npm install clipboard"`

**Source:** `src/directive/clipboard/clipboard.js:4`

**Cause:** The `clipboard` npm package is missing from `node_modules`.

**Solution:**
```bash
npm install clipboard@2
```

### Error: Login redirects in infinite loop

**Cause:** The `getInfo` action returns a user object where `roles` is empty or undefined. The permission guard checks `roles.length > 0` — if false, it re-dispatches `getInfo` on every navigation.

**Solution:** Ensure the backend always returns `roles` as a non-empty array in the `/user/info` response.

### Error: Routes not showing in sidebar after login

**Cause:** Either the role returned by the API does not match any `meta.roles` in `asyncRoutes`, or `router.addRoutes()` failed silently.

**Solution:**
1. Check that roles match exactly (case-sensitive)
2. Verify `asyncRoutes` entries have `meta.roles` arrays containing the user's role
3. If user has `admin` role, all routes should appear (admin bypasses filtering)

### Error: `keep-alive` not caching views

**Cause:** The view component's `name` property does not match what the router expects, or `meta.noCache` is set.

**Solution:**
1. Ensure the component has `name: 'ExactComponentName'`
2. Check that the same name appears in route definition
3. Verify `meta.noCache` is not `true` for the route

### Error: Build fails with `digital envelope routines::unsupported`

**Cause:** Node.js 17+ uses OpenSSL 3, which breaks webpack 4's hash functions.

**Solution:**
```bash
# Linux/macOS
export NODE_OPTIONS=--openssl-legacy-provider
npm run build:prod

# Windows (CMD)
set NODE_OPTIONS=--openssl-legacy-provider
npm run build:prod

# Windows (PowerShell)
$env:NODE_OPTIONS="--openssl-legacy-provider"
npm run build:prod
```

### Error: `npm install` fails with `node-sass` errors

**Cause:** The project uses `sass` (Dart Sass) not `node-sass`, but some cached or transitive dependency may pull in `node-sass`.

**Solution:**
```bash
rm -rf node_modules package-lock.json
npm cache clean --force
npm install
```

### Error: Icons not displaying

**Cause:** svg-sprite-loader is not processing the icons, or the webpack rule is misconfigured.

**Solution:**
1. Verify files exist in `src/icons/svg/` with `.svg` extension
2. Check `vue.config.js` has the SVG rule chain (Section 3.3.2)
3. Run `npm run svgo` to ensure SVGs are valid
4. Clear webpack cache: delete `.cache` directory if present

---

## 11. Code Patterns and Conventions

### 11.1 File Naming

| Type | Convention | Example |
|---|---|---|
| Views | kebab-case directory + `index.vue` | `views/error-log/index.vue` |
| Components | PascalCase directory + `index.vue` | `components/BackToTop/index.vue` |
| Store modules | camelCase `.js` | `store/modules/tagsView.js` |
| Utils | camelCase `.js` | `utils/scrollTo.js` |
| API modules | kebab-case `.js` | `api/remote-search.js` |
| SCSS | kebab-case `.scss` | `styles/element-variables.scss` |

### 11.2 Component Conventions

- All components export `name` in PascalCase
- Views use scoped styles (`<style scoped>`)
- Components use BEM-like class naming
- Props validated with types (not all use `required`)
- Events emitted via `$emit` (no event bus)
- No mixins used in components (ResizeMixin in layout is the only exception)

### 11.3 Import Aliases

The `@` alias maps to `src/`:

```javascript
import request from '@/utils/request'      // src/utils/request.js
import Layout from '@/layout'              // src/layout/index.vue
import SvgIcon from '@/components/SvgIcon' // src/components/SvgIcon/index.vue
```

### 11.4 Route Component Lazy Loading

All async route components use dynamic `import()`:

```javascript
component: () => import('@/views/dashboard/index')
```

In development, `dynamic-import-node` converts these to synchronous `require()`. In production, each becomes a separate webpack chunk.

### 11.5 Vuex Access Patterns

```javascript
// In components — mapGetters
import { mapGetters } from 'vuex'
computed: {
  ...mapGetters(['sidebar', 'device', 'token', 'roles', 'permission_routes'])
}

// Dispatch actions
this.$store.dispatch('user/login', { username, password })

// Direct state access (less preferred)
this.$store.state.app.sidebar.opened
```

### 11.6 API Response Handling

```javascript
// Standard pattern in views
async fetchData() {
  this.loading = true
  try {
    const { data } = await fetchList(this.query)  // Destructure .data from response
    this.list = data.items
    this.total = data.total
  } catch (error) {
    // Error already shown via response interceptor toast
    console.error(error)
  } finally {
    this.loading = false
  }
}
```

### 11.7 Element UI Size System

Components throughout the app respect the global size setting:

| Size | Typical Use |
|---|---|
| `default` | Standard desktop |
| `medium` | Default in this project |
| `small` | Compact layouts |
| `mini` | Dense data tables |

Changed via SizeSelect component → `app/setSize` → page reload.

### 11.8 Cookie Usage

| Cookie | Purpose | Set By |
|---|---|---|
| `Admin-Token` | Auth token | `src/utils/auth.js` |
| `sidebarStatus` | Sidebar open/closed state | `store/modules/app.js` |
| `size` | Element UI component size | `store/modules/app.js` |

All managed via `js-cookie`. No expiration set (session cookies by default).

### 11.9 Known GitHub Issues

| Issue | Summary | Status |
|---|---|---|
| #4335 | Broken `githubusercontent` link | Open |
| #3988 | `npm install` failures analysis | Open |
| #4340 | Feature: global CTA click lock via Axios interceptors | Open |
| #3606 | Two theme switching methods cannot coexist | Open |
| #4230 | Upgrade to Vue 3 | Open |

### 11.10 Upgrade Path to Vue 3

Issue #4230 tracks the Vue 3 migration. Key changes needed:

1. Replace `new Vue()` with `createApp()`
2. Replace `Vue.use()` with `app.use()`
3. Replace `Vue.filter()` — global filters removed in Vue 3
4. Replace `Vue.directive()` — API changes
5. Replace Vuex 3 with Vuex 4 (or Pinia)
6. Replace vue-router 3 with vue-router 4
7. Replace Element UI with Element Plus
8. `$listeners` removed — merge into `$attrs`
9. `v-model` behavior changes (prop: `modelValue`, event: `update:modelValue`)
10. Composition API availability (optional but recommended for new code)

> [!NOTE]
> PanJiaChen maintains a separate Vue 3 project (`vue-element-plus-admin`) that may serve as a migration reference, though it is a different codebase rather than a direct upgrade path.

---

## 12. Quick Reference

### 12.1 Common Commands

```bash
# Development
npm run dev                          # Start dev server (port 9527)
port=3000 npm run dev                # Custom port

# Build
npm run build:prod                   # Production build → dist/
npm run build:stage                  # Staging build

# Quality
npm run lint                         # ESLint check
npm run test:unit                    # Jest tests
npm run test:ci                      # Lint + test (CI pipeline)

# Utilities
npm run new                          # Plop code generator
npm run svgo                         # Optimize SVG icons
npm run preview                      # Preview production build locally
```

### 12.2 Key Files Quick-Find

| Need | File |
|---|---|
| Add a route | `src/router/index.js` |
| Add an API call | `src/api/{domain}.js` |
| Change auth logic | `src/permission.js` |
| Change token header | `src/utils/request.js` |
| Change app title | `src/settings.js` |
| Change theme colors | `src/styles/element-variables.scss` |
| Add a Vuex module | `src/store/modules/{name}.js` + register in `src/store/index.js` |
| Add an SVG icon | Drop in `src/icons/svg/` |
| Add a mock endpoint | `mock/{domain}.js` + register in `mock/index.js` |
| Change sidebar width | `src/styles/variables.scss` |
| Change dev port | `vue.config.js` or `port` env var |
| Change base API URL | `.env.development` / `.env.production` |

### 12.3 Roles & Access Matrix

| Route Group | admin | editor | visitor |
|---|---|---|---|
| Dashboard | Yes | Yes | Yes |
| Documentation | Yes | Yes | Yes |
| Guide | Yes | Yes | Yes |
| Permission | Yes | Yes (except role page) | No |
| Icons | Yes | Yes | Yes |
| Components | Yes | Yes | Yes |
| Charts | Yes | Yes | Yes |
| Nested | Yes | Yes | Yes |
| Example | Yes | Yes | Yes |
| Tab | Yes | Yes | Yes |
| Error Pages | Yes | No | No |
| Error Log | Yes | Yes | Yes |
| Excel | Yes | Yes | Yes |
| ZIP | Yes | Yes | Yes |
| PDF | Yes | Yes | Yes |
| Theme | Yes | Yes | Yes |
| Clipboard | Yes | Yes | Yes |
| i18n | Yes | Yes | Yes |

### 12.4 Response Code Reference

| Code | Meaning | Action |
|---|---|---|
| `20000` | Success | Return data |
| `50008` | Illegal token | Force re-login |
| `50012` | Other client logged in | Force re-login |
| `50014` | Token expired | Force re-login |
| `60204` | Invalid username/password | Show error message |

---

_Generated May 2026 from PanJiaChen/vue-element-admin at 6858a9a. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
