Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

vue-element-admin Administrator & Developer Guide

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.

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:

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.

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:

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:

// 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:

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

Production:

// 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:

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)

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:

{
  "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):

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

# 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:

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:

# 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:

    publicPath: '/admin/'
    
  2. Set the router base:

    // src/router/index.js
    const createRouter = () => new Router({
      base: '/admin/',
      // ...
    })
    
  3. Update 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:

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

# 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:

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):
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)
        }
      }
    }
  }
]
  1. Add the file to the mock table in mock/index.js:
const order = require('./order')
const mocks = [...user, ...role, ...article, ...search, ...order]
  1. 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

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):

{{#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:

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)

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

<!-- 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)

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):

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):

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

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

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):

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:

<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:

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:

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:

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:

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

Request interceptor:

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

Response interceptor:

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):

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:

// 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:

<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:

<!-- 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:

<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:

<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:

<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:

<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

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

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

export function scrollTo(to, duration, callback)

Eases scroll position using requestAnimationFrame. Used by BackToTop component.

8.5.6 get-page-title.js — Document Title

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

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

Opens a centered popup window with specified dimensions.

8.5.8 clipboard.js — Copy to Clipboard

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

// 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

// 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:

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():

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

# 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:

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:

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:
// DELETE these lines:
if (process.env.NODE_ENV === 'production') {
  const { mockXHR } = require('../mock')
  mockXHR()
}
  1. Remove mock from vue.config.js dev server:
// DELETE:
devServer: {
  before: require('./mock/mock-server.js')
}
  1. Update .env.production:
VUE_APP_BASE_API = 'https://api.your-domain.com'
  1. Configure proxy for development (in vue.config.js):
devServer: {
  proxy: {
    '/dev-api': {
      target: 'http://localhost:8080',
      changeOrigin: true,
      pathRewrite: { '^/dev-api': '' }
    }
  }
}
  1. Adapt response handling in src/utils/request.js:

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

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)
  }
)
  1. Update auth token handling:

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

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):
npm run new
# Select "view", enter name

Or manually create src/views/my-feature/index.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>
  1. Add to router (src/router/index.js):
// 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']
      }
    }
  ]
}
  1. Add API module (if needed):
// src/api/my-feature.js
import request from '@/utils/request'

export function fetchData(params) {
  return request({ url: '/my-feature/list', method: 'get', params })
}
  1. Add mock (for development):
// 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:

const myFeature = require('./my-feature')
const mocks = [...user, ...role, ...article, ...search, ...myFeature]
  1. Add Vuex module (if complex state needed):
npm run new
# Select "store", enter name

Register in src/store/index.js:

import myFeature from './modules/my-feature'
// In modules: { ..., myFeature }

9.4 Adding New Components

  1. Create component (use plop or manually):
npm run new
# Select "component", enter name

Creates src/components/MyComponent/index.vue.

  1. Use in views:
import MyComponent from '@/components/MyComponent'

export default {
  components: { MyComponent }
}
  1. Register globally (if used in 3+ places):
// 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:
    npm run svgo
    
  3. Use immediately — no registration needed:
    <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:

    npm install vue-i18n@8
    
  2. Create language files in src/lang/:

    // src/lang/en.js
    export default { navbar: { dashboard: 'Dashboard', ... } }
    // src/lang/zh.js
    export default { navbar: { dashboard: '首页', ... } }
    
  3. Initialize in main.js:

    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):

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:

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):

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):

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:

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:

// 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:

<!-- 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:

<!-- 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:

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:

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:

# 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:

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/:

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():

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

// 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

// 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.

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

# 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