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:
- Import core dependencies: Vue, App.vue, store, router
- Import global styles:
@/styles/index.scss - 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
- Size read from Vuex store (
- Register global filters: All exports from
@/filtersattached toVue.filter() - Register SVG icons:
@/iconstriggersrequire.context()for automatic SVG registration - Register permission guard:
@/permissioninstalls the globalrouter.beforeEachhook - Conditionally initialize mock: If
process.env.NODE_ENV === 'production', import../mockwhich callsmockXHR()to intercept XMLHttpRequest - Set production tip:
Vue.config.productionTip = false - 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, dispatchesapp/toggleDeviceto'mobile'and closes sidebar - Listens to
window.resizeevent
AppMain (src/layout/components/AppMain.vue):
- Uses
<keep-alive :include="cachedViews">wherecachedViewscomes fromstore.getters.cachedViews - The
:keyis set to$route.pathto force re-creation on path change while allowing caching by component name - Only components whose
namematches an entry incachedViewsare cached
Important
For
keep-alivecaching to work, each view component must have anameproperty that matches the route's component name. Ifmeta.noCacheis not set, the component name is added tocachedViewsautomatically.
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.4which bundles webpack 4. Node.js versions above 16 may encounter OpenSSL-related build failures. If using Node 17+, setNODE_OPTIONS=--openssl-legacy-providerbefore 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.jsvia script-loader insrc/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_APIvalue in.env.productionmust match the Nginx proxy location. If set to'/prod-api', configurelocation /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/):
-
Set
publicPathinvue.config.js:publicPath: '/admin/' -
Set the router base:
// src/router/index.js const createRouter = () => new Router({ base: '/admin/', // ... }) -
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
- 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)
}
}
}
}
]
- Add the file to the mock table in
mock/index.js:
const order = require('./order')
const mocks = [...user, ...role, ...article, ...search, ...order]
- 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
- Place SVG files in
src/icons/svg/ - Run
npm run svgoto optimize (removes fills, strokes, unnecessary attributes) - Icons are auto-registered via
require.contextinsrc/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
nameis instore.state.tagsView.cachedViews - Opt out: Set
meta.noCache: trueon 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 ofmounted()— 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
lazyloading onel-treecomponents - Implement virtual scrolling (not built in — requires external library)
- The
v-el-tableadaptive 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, orSameSiteattributes. In a production deployment with a real authentication backend, configure the server to set the token as anHttpOnly; Secure; SameSite=Strictcookie 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-permissiondirective uses DOM removal (el.parentNode.removeChild(el)) which means the element is permanently removed. Usev-ifwithcheckPermission()if you need reactive show/hide behavior when roles change during a session (e.g., viachangeRolesaction).
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.jsfile 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-Tokenheader is a custom header, requiringAccess-Control-Allow-Headers: X-Tokenon 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:
insertedhook: checksstore.getters.rolesagainst 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
Clipboardinstance onbind, stores it onel._v_clipboard - Updates on
updatehook, destroys onunbind - 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__headerand.el-dialogwithin the directive element - Sets
cursor: moveon header - On
mousedown→ tracksmousemove→ calculates delta → applies CSSmargin-leftandmargin-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
bindandwindow.resize: calculateswindow.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
validUsernameis 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.titlefrompermission_routesgetter - Builds a Fuse.js search index on component mount
- Shows
el-selectwith remote search on click - Navigates to selected route on selection
- Closes on
clickoutside (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:
- Fetches Element UI CSS from CDN (
unpkg.com/element-ui@{version}/lib/theme-chalk/index.css) - Replaces all color values using regex
- Generates tint colors (for hover states, etc.)
- Appends modified CSS as a
<style>tag in<head> - 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:
- Parse query params from URL
- Parse body from request (for POST/PUT)
- Call the response function with
{ query, body }asconfig - 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}(excludingauth.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:
- Remove mock initialization from
src/main.js:
// DELETE these lines:
if (process.env.NODE_ENV === 'production') {
const { mockXHR } = require('../mock')
mockXHR()
}
- Remove mock from
vue.config.jsdev server:
// DELETE:
devServer: {
before: require('./mock/mock-server.js')
}
- Update
.env.production:
VUE_APP_BASE_API = 'https://api.your-domain.com'
- Configure proxy for development (in
vue.config.js):
devServer: {
proxy: {
'/dev-api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/dev-api': '' }
}
}
}
- 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)
}
)
- 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
rolesfield in the user info response must be a non-empty array. The permission system checksroles.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:
- 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>
- 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']
}
}
]
}
- 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 })
}
- 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]
- 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
- Create component (use plop or manually):
npm run new
# Select "component", enter name
Creates src/components/MyComponent/index.vue.
- Use in views:
import MyComponent from '@/components/MyComponent'
export default {
components: { MyComponent }
}
- 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
- Place the
.svgfile insrc/icons/svg/ - Run SVGO optimization:
npm run svgo - 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:
-
Install
vue-i18n:npm install vue-i18n@8 -
Create language files in
src/lang/:// src/lang/en.js export default { navbar: { dashboard: 'Dashboard', ... } } // src/lang/zh.js export default { navbar: { dashboard: '首页', ... } } -
Initialize in
main.js:import VueI18n from 'vue-i18n' Vue.use(VueI18n) const i18n = new VueI18n({ locale: 'en', messages: { en, zh } }) new Vue({ i18n, ... }) -
Use Element UI's
localewith 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
beforeDestroyand remove resize listeners to prevent memory leaks, especially when views are cached bykeep-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:
- Check that roles match exactly (case-sensitive)
- Verify
asyncRoutesentries havemeta.rolesarrays containing the user's role - If user has
adminrole, 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:
- Ensure the component has
name: 'ExactComponentName' - Check that the same name appears in route definition
- Verify
meta.noCacheis nottruefor 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:
- Verify files exist in
src/icons/svg/with.svgextension - Check
vue.config.jshas the SVG rule chain (Section 3.3.2) - Run
npm run svgoto ensure SVGs are valid - Clear webpack cache: delete
.cachedirectory 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
namein 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.
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:
- Replace
new Vue()withcreateApp() - Replace
Vue.use()withapp.use() - Replace
Vue.filter()— global filters removed in Vue 3 - Replace
Vue.directive()— API changes - Replace Vuex 3 with Vuex 4 (or Pinia)
- Replace vue-router 3 with vue-router 4
- Replace Element UI with Element Plus
$listenersremoved — merge into$attrsv-modelbehavior changes (prop:modelValue, event:update:modelValue)- 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 |
| 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 |