# vue-element-admin User Guide

vue-element-admin is a production-ready front-end solution for building admin interfaces. Built on Vue 2.6 and Element UI 2.13, it provides a complete admin dashboard framework with 30+ reusable components, a role-based permission system, mock server for development, and built-in support for Excel/ZIP/PDF export, rich text editing, drag-and-drop interfaces, and data visualization. Version 4.4.0, MIT licensed, by PanJiaChen.

This guide covers everything you need to install, configure, and use vue-element-admin for your projects. For architecture details, deployment strategies, and internals, see the [Admin Guide](/PanJiaChen/vue-element-admin/admin-guide/).

---

## 1. Quick Start

Get a working admin dashboard running in under five minutes.

### 1.1 Prerequisites

| Requirement | Minimum Version | Recommended |
|---|---|---|
| Node.js | 8.9+ | 12.x or 14.x |
| npm | 3.0.0+ | 6.x+ |
| Git | Any recent version | Latest |

> [!NOTE]
> vue-element-admin is built with Vue CLI 4.4.4. Node versions above 16 may require the `--openssl-legacy-provider` flag due to OpenSSL 3.0 changes. If you encounter hash-related build errors, set `NODE_OPTIONS=--openssl-legacy-provider` before running dev or build commands.

### 1.2 Clone and Run

```bash
# Clone the repository
git clone https://github.com/PanJiaChen/vue-element-admin.git

# Enter the project directory
cd vue-element-admin

# Install dependencies
npm install

# Start the development server
npm run dev
```

The dev server starts on **port 9527**. Open `http://localhost:9527` in your browser.

### 1.3 First Login

The project ships with a mock server (MockJS) that intercepts HTTP requests — no backend needed for development.

| Username | Password | Role | Access Level |
|---|---|---|---|
| `admin` | any password | admin | Full access to all routes and features |
| `editor` | any password | editor | Limited access (restricted routes) |

Type `admin` as the username, enter anything for the password, and click Login. You'll land on the Dashboard with the full sidebar menu available.

### 1.4 Project Structure at a Glance

```
vue-element-admin/
├── build/              # Build scripts (preview server)
├── mock/               # Mock server data and routes
├── plop-templates/     # Scaffolding templates (npm run new)
├── public/             # Static assets (index.html, favicon)
├── src/
│   ├── api/            # API request modules (one per resource)
│   ├── assets/         # Static assets (images, custom themes)
│   ├── components/     # 30+ reusable components
│   ├── directive/      # Custom Vue directives
│   ├── filters/        # Global template filters
│   ├── icons/          # SVG icons (auto-registered)
│   ├── layout/         # App layout (sidebar, navbar, tags)
│   ├── router/         # Route definitions (constant + async)
│   ├── store/          # Vuex state management
│   ├── styles/         # Global SCSS (variables, mixins, transitions)
│   ├── utils/          # Utility functions
│   └── views/          # Page components (one folder per feature)
├── tests/              # Jest unit tests
├── .env.development    # Dev environment config
├── .env.production     # Production environment config
├── .env.staging        # Staging environment config
└── vue.config.js       # Vue CLI configuration
```

---

## 2. Installation

### 2.1 Fresh Installation

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

### 2.2 Handling npm Install Failures

If `npm install` fails (a known issue, see GitHub #3988), try the following in order:

#### Clear npm cache and retry

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

#### Use a specific npm registry

```bash
npm install --registry=https://registry.npmmirror.com
```

#### Pin Node version

```bash
# Use Node 14.x for best compatibility
nvm install 14
nvm use 14
npm install
```

### 2.3 Development Server

```bash
npm run dev
```

This runs `vue-cli-service serve`, which:

- Starts a webpack dev server on port **9527**
- Enables hot module replacement (HMR)
- Activates the MockJS interceptor for all API calls
- Opens the browser automatically

#### Custom port

Override the default port via environment variable:

```bash
# Using port env var
port=8080 npm run dev

# Using npm_config_port
npm_config_port=8080 npm run dev
```

### 2.4 Production Build

```bash
# Production build
npm run build:prod

# Staging build
npm run build:stage
```

Both commands run `vue-cli-service build`. The staging build uses `--mode staging` to load `.env.staging` variables. Output lands in `dist/`.

### 2.5 Preview the Production Build

```bash
npm run preview
```

This runs `node build/index.js --preview`, which serves the `dist/` directory locally so you can verify the production build before deploying.

### 2.6 Other NPM Scripts

| Script | Command | Purpose |
|---|---|---|
| `npm run lint` | `eslint --ext .js,.vue src` | Lint all source files |
| `npm run new` | `plop` | Scaffold a new view, component, or store module |
| `npm run svgo` | `svgo -f src/icons/svg` | Optimize SVG icon files |
| `npm run test:unit` | `jest --clearCache && vue-cli-service test:unit` | Run unit tests |
| `npm run test:ci` | lint + test:unit | Full CI check |

---

## 3. Configuration

### 3.1 Application Settings

The primary configuration file is `src/settings.js`:

```javascript
module.exports = {
  title: 'Vue Element Admin',    // Page title (shown in browser tab)
  showSettings: true,            // Show the settings gear panel
  tagsView: true,                // Show the tags/tabs navigation bar
  fixedHeader: false,            // Fix the navbar header to the top
  sidebarLogo: false,            // Show a logo above the sidebar
  errorLog: 'production'         // Which environments collect error logs
}
```

| Setting | Type | Default | Effect |
|---|---|---|---|
| `title` | String | `'Vue Element Admin'` | Browser tab title; combined with route meta.title |
| `showSettings` | Boolean | `true` | Toggles the gear icon that opens the right-side settings panel |
| `tagsView` | Boolean | `true` | Toggles the tabbed navigation bar below the header |
| `fixedHeader` | Boolean | `false` | When true, the navbar stays pinned while scrolling content |
| `sidebarLogo` | Boolean | `false` | When true, displays a logo image above the sidebar menu |
| `errorLog` | String/Array | `'production'` | Environments where error logs are captured. Use `['production', 'development']` to enable in multiple environments |

> [!TIP]
> You can change `showSettings`, `tagsView`, `fixedHeader`, and `sidebarLogo` at runtime using the settings panel (the gear icon in the top-right corner). These runtime changes are managed through the Vuex `settings` store module and persist only for the session.

### 3.2 Environment Variables

Three environment files control the API base URL:

| File | Build Mode | Typical `VUE_APP_BASE_API` Value |
|---|---|---|
| `.env.development` | `npm run dev` | `/dev-api` |
| `.env.production` | `npm run build:prod` | `/prod-api` |
| `.env.staging` | `npm run build:stage` | `/stage-api` |

The `VUE_APP_BASE_API` variable is used by `src/utils/request.js` as the Axios `baseURL`. In development mode, MockJS intercepts requests before they reach the network, so the actual base URL value does not matter — any path works. In production, you must point this to your real backend.

#### Adding Custom Environment Variables

Any variable prefixed with `VUE_APP_` is embedded into the client bundle by Vue CLI and accessible via `process.env.VUE_APP_*`:

```bash
# In .env.production
VUE_APP_TITLE=My Admin Panel
VUE_APP_CDN_URL=https://cdn.example.com
```

```javascript
// In your code
console.log(process.env.VUE_APP_TITLE)
```

### 3.3 Vue CLI Configuration

The `vue.config.js` file controls build behavior:

- **Dev server proxy** — Configure proxy rules for forwarding API requests to a real backend during development
- **Source maps** — Disabled in production by default for performance
- **Webpack chain** — SVG sprite loader for the icon system, script preloading
- **Build optimization** — CDN externals, chunk splitting

### 3.4 Axios Request Configuration

All HTTP requests go through `src/utils/request.js`, which creates a configured Axios instance:

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

**Request interceptor:** Attaches the `X-Token` header from the cookie store on every outgoing request.

**Response interceptor:** Checks `res.data.code`:
- `20000` — Success, returns `res.data`
- `50008` — Illegal token
- `50012` — Other client logged in
- `50014` — Token expired

Codes 50008, 50012, and 50014 trigger a re-login confirmation dialog that clears the token and redirects to `/login`.

> [!IMPORTANT]
> When connecting to a real backend, your API must return responses in the format `{ code: 20000, data: ... }` for the interceptor to treat them as successful. Any other code triggers an error message. Adjust the interceptor if your backend uses a different response structure.

### 3.5 Theme Configuration

#### Element UI Theme

The project includes a built-in theme picker component accessible from the navbar. It dynamically replaces Element UI's CSS variables to apply a new primary color across the entire application.

To set a permanent default theme, modify the Element UI SCSS variables in `src/styles/element-variables.scss`:

```scss
$--color-primary: #1890ff;
```

#### Custom SCSS Variables

Global style variables live in `src/styles/variables.scss`:

| Variable | Purpose | Default |
|---|---|---|
| `$menuText` | Sidebar menu text color | `#bfcbd9` |
| `$menuActiveText` | Active sidebar menu text | `#409EFF` |
| `$subMenuActiveText` | Active sub-menu text | `#f4f4f5` |
| `$menuBg` | Sidebar background | `#304156` |
| `$menuHover` | Sidebar hover background | `#263445` |
| `$subMenuBg` | Sub-menu background | `#1f2d3d` |
| `$subMenuHover` | Sub-menu hover background | `#001528` |
| `$sideBarWidth` | Sidebar width | `210px` |

These variables are available in any `.vue` component without importing, as they are injected globally via `vue.config.js`.

### 3.6 Internationalization (i18n)

vue-element-admin supports internationalization via `vue-i18n`. The i18n branch (`i18n` branch on GitHub) contains the full implementation. The main branch does not include i18n by default — if you need multi-language support, merge or cherry-pick from the i18n branch.

---

## 4. Core Usage

### 4.1 Layout System

The application layout consists of four main regions:

```
┌──────────────────────────────────────────┐
│                 Navbar                    │
├──────────┬───────────────────────────────┤
│          │         Tags View             │
│ Sidebar  ├───────────────────────────────┤
│          │                               │
│          │       Main Content            │
│          │       (AppMain / router-view) │
│          │                               │
└──────────┴───────────────────────────────┘
```

**Sidebar** — Collapsible navigation menu, auto-generated from the route table. Supports nested menus, icons, and external links. Toggle with the hamburger icon.

**Navbar** — Top bar with hamburger toggle, breadcrumb navigation, and right-side controls (search, screenfull, Element UI size selector, user dropdown).

**Tags View** — Tab bar showing open pages. Right-click any tab for options: Close, Close Others, Close All, Refresh. Pages listed with `meta.affix: true` cannot be closed (Dashboard and Documentation are affixed by default).

**AppMain** — The `<router-view>` that renders the current page. Uses `<keep-alive>` to cache views listed in the Tags View `cachedViews` array.

### 4.2 Sidebar Navigation

The sidebar is auto-generated from the route configuration in `src/router/index.js`. Every route object maps to a menu item.

#### Route Meta Fields That Control the Sidebar

| Meta Field | Type | Effect |
|---|---|---|
| `title` | String | Display text in the sidebar menu |
| `icon` | String | SVG icon name (from `src/icons/svg/`) |
| `hidden` | Boolean | When `true`, the route is hidden from the sidebar |
| `alwaysShow` | Boolean | When `true`, always shows the parent menu even with one child |
| `noCache` | Boolean | When `true`, the page is not cached by `<keep-alive>` |
| `affix` | Boolean | When `true`, the tag is permanently pinned in Tags View |
| `breadcrumb` | Boolean | When `false`, hidden from breadcrumb |
| `activeMenu` | String | Highlights a different sidebar item (e.g., for detail pages) |
| `roles` | Array | Which roles can access this route (e.g., `['admin', 'editor']`) |

#### Nesting Rules

- **No children** — Renders as a single menu item
- **One child** — By default, shows only the child (flattened). Set `alwaysShow: true` on the parent to always show the parent wrapper
- **Multiple children** — Renders as a nested sub-menu automatically

#### External Links

Add an external URL as the path to create a sidebar link that opens in a new tab:

```javascript
{
  path: 'https://github.com/PanJiaChen/vue-element-admin',
  component: Layout,
  children: [{
    path: 'https://github.com/PanJiaChen/vue-element-admin',
    meta: { title: 'External Link', icon: 'link' }
  }]
}
```

### 4.3 Tags View (Tabs Navigation)

The Tags View bar shows each visited route as a tab. It is managed by the `tagsView` Vuex store module.

**Features:**
- Click a tab to navigate to that page
- Right-click a tab to see the context menu (Refresh, Close, Close Other, Close All)
- Affixed tabs (like Dashboard) cannot be closed
- Cached views persist their state when switching between tabs
- Scroll horizontally when many tabs are open

**Disabling Tags View:** Set `tagsView: false` in `src/settings.js` or toggle it via the settings panel.

**Disabling cache for a specific page:** Set `noCache: true` in the route's meta to prevent `<keep-alive>` from caching that page:

```javascript
{
  path: 'my-page',
  component: () => import('@/views/my-page'),
  meta: { title: 'My Page', noCache: true }
}
```

### 4.4 Breadcrumb Navigation

Breadcrumbs auto-generate from the current route's matched hierarchy. They appear in the navbar, to the right of the hamburger toggle.

Routes with `meta.breadcrumb: false` are excluded from the breadcrumb trail. The last breadcrumb (current page) is not clickable.

### 4.5 Header Search

Press the search icon in the navbar (or use keyboard shortcut) to open a search dialog. It uses **Fuse.js** for fuzzy matching against all route titles. Type a few characters and select from the filtered results to navigate instantly.

The search indexes all sidebar-visible routes. Hidden routes (`meta.hidden: true`) are excluded from search results.

### 4.6 Screenfull (Fullscreen Mode)

Click the expand icon in the navbar to toggle fullscreen mode using the browser's Fullscreen API. This uses the `screenfull` library for cross-browser compatibility.

### 4.7 Element UI Size Selector

The navbar includes a size selector dropdown that lets you switch Element UI's global component size between `medium`, `small`, `mini`, and `default`. This changes the size of all Element UI components (buttons, inputs, tables, etc.) across the entire application. The selected size is stored in the `app` Vuex module.

---

## 5. Common Tasks

### 5.1 Adding a New Page

#### Method 1: Using the Scaffold Generator

```bash
npm run new
```

Select `view` from the prompt. Enter a name, and plop creates a new view file from the template in `plop-templates/view/`.

#### Method 2: Manual Creation

**Step 1: Create the view component**

```bash
mkdir -p src/views/my-feature
```

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

```vue
<template>
  <div class="my-feature-container">
    <h2>My Feature</h2>
  </div>
</template>

<script>
export default {
  name: 'MyFeature'
}
</script>

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

> [!TIP]
> Always set a `name` property on your view component. The `name` must match the route's `name` for `<keep-alive>` caching to work correctly with Tags View.

**Step 2: Add the route**

In `src/router/index.js`, add to `constantRoutes` (for all users) or `asyncRoutes` (for role-restricted access):

```javascript
// In constantRoutes (accessible to everyone)
{
  path: '/my-feature',
  component: Layout,
  children: [
    {
      path: 'index',
      name: 'MyFeature',
      component: () => import('@/views/my-feature/index'),
      meta: { title: 'My Feature', icon: 'example' }
    }
  ]
}
```

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

**Step 3 (Optional): Add the API module**

If the page needs API calls, create `src/api/my-feature.js`:

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

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

export function createItem(data) {
  return request({
    url: '/my-feature/create',
    method: 'post',
    data
  })
}
```

**Step 4 (Optional): Add mock data**

Create `mock/my-feature.js` and register it in `mock/index.js`:

```javascript
// mock/my-feature.js
const Mock = require('mockjs')

const List = Mock.mock({
  'items|30': [{
    id: '@id',
    title: '@sentence(5, 10)',
    'status|1': ['published', 'draft'],
    timestamp: +Mock.Random.date('T')
  }]
})

module.exports = [
  {
    url: '/my-feature/list',
    type: 'get',
    response: config => {
      return { code: 20000, data: { items: List.items } }
    }
  }
]
```

### 5.2 Adding a New Component

#### Using the scaffold

```bash
npm run new
```

Select `component`, enter a name. Plop creates the component in `src/components/`.

#### Manual creation

Create `src/components/MyComponent/index.vue`:

```vue
<template>
  <div class="my-component">
    <slot />
  </div>
</template>

<script>
export default {
  name: 'MyComponent',
  props: {
    value: {
      type: String,
      default: ''
    }
  }
}
</script>
```

Use it in any view:

```vue
<template>
  <my-component value="hello">Content</my-component>
</template>

<script>
import MyComponent from '@/components/MyComponent'

export default {
  components: { MyComponent }
}
</script>
```

### 5.3 Adding a Vuex Store Module

#### Using the scaffold

```bash
npm run new
```

Select `store`, enter the module name. Plop generates the module file and auto-registers it.

#### Manual creation

Create `src/store/modules/my-module.js`:

```javascript
const state = {
  items: []
}

const mutations = {
  SET_ITEMS: (state, items) => {
    state.items = items
  }
}

const actions = {
  fetchItems({ commit }) {
    // Call API, then commit
    commit('SET_ITEMS', [])
  }
}

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

Register in `src/store/index.js`:

```javascript
import myModule from './modules/my-module'

const store = new Vuex.Store({
  modules: {
    // ...existing modules,
    myModule
  }
})
```

### 5.4 Adding SVG Icons

vue-element-admin uses an SVG sprite system. All SVG files in `src/icons/svg/` are auto-registered as icon components.

**Step 1:** Place your SVG file in `src/icons/svg/`:

```
src/icons/svg/my-icon.svg
```

**Step 2:** Optimize it (optional but recommended):

```bash
npm run svgo
```

**Step 3:** Use it in templates:

```vue
<svg-icon icon-class="my-icon" />
```

The `SvgIcon` component is globally registered — no import needed. The `icon-class` value matches the SVG filename without the `.svg` extension.

#### Using icons in sidebar routes

Set the `icon` meta field to the SVG filename:

```javascript
meta: { title: 'My Page', icon: 'my-icon' }
```

### 5.5 Working with the Mock Server

The mock server is powered by MockJS and intercepts Axios requests during development. Mock definitions live in the `mock/` directory.

#### Mock file structure

```
mock/
├── article.js      # Article CRUD endpoints
├── index.js        # Auto-registers all mock modules
├── mock-server.js  # Express-based mock server for preview
├── remote-search.js # Search endpoint
├── role/           # Role management endpoints
├── table.js        # Table data
└── user.js         # Login, logout, user info
```

#### How mock routing works

Each mock module exports an array of route objects:

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

The `config` parameter contains `query` (GET params), `body` (POST body), and other request metadata.

#### Disabling the mock server

To connect to a real backend during development, remove or comment out the mock setup in `main.js` or `mock/index.js`, then update `VUE_APP_BASE_API` in `.env.development` to point to your backend URL. You may also need to configure a proxy in `vue.config.js` to avoid CORS issues.

### 5.6 Connecting to a Real Backend

To transition from the mock server to a real backend:

**Step 1:** Update `.env.development`:

```bash
VUE_APP_BASE_API = 'http://localhost:3000/api'
```

**Step 2:** Remove mock imports from the entry point (check `main.js` or the Webpack dev config).

**Step 3:** Configure the dev server proxy in `vue.config.js` if your backend runs on a different port:

```javascript
devServer: {
  proxy: {
    '/api': {
      target: 'http://localhost:3000',
      changeOrigin: true
    }
  }
}
```

**Step 4:** Update `.env.production` to point to your production API:

```bash
VUE_APP_BASE_API = 'https://api.yourdomain.com'
```

**Step 5:** Adjust the response interceptor in `src/utils/request.js` if your backend does not return `{ code: 20000, data: ... }` format.

> [!WARNING]
> The Axios response interceptor in `src/utils/request.js` expects all successful responses to have `code: 20000`. If your backend returns standard HTTP status codes (200, 201, etc.) with a different body structure, you must rewrite the response interceptor or your app will treat every response as an error.

---

## 6. Permission System

### 6.1 Overview

vue-element-admin implements permission control at three levels:

| Level | Mechanism | Granularity |
|---|---|---|
| **Route-level** | Route meta `roles` array + dynamic route generation | Entire pages |
| **Element-level** | `v-permission` directive | Individual UI elements |
| **Programmatic** | `checkPermission()` utility | Conditional logic in code |

### 6.2 Authentication Flow

The authentication flow is controlled by the route guard in `src/permission.js`:

```
User navigates to a route
  ├── Has token (cookie 'Admin-Token')?
  │   ├── Route is /login → Redirect to /
  │   └── Route is not /login
  │       ├── Has roles in Vuex? → Allow navigation
  │       └── No roles yet
  │           ├── Call getInfo() → Get roles from server
  │           ├── Call generateRoutes(roles) → Filter asyncRoutes
  │           ├── router.addRoutes() → Add permitted routes
  │           └── Allow navigation
  └── No token
      ├── Route is in whitelist (/login, /auth-redirect)? → Allow
      └── Route is not in whitelist → Redirect to /login
```

The token is stored in a cookie named `Admin-Token` via the `js-cookie` library. Cookie access is centralized in `src/utils/auth.js` with `getToken()`, `setToken()`, and `removeToken()` functions.

### 6.3 Login and Logout

**Login:** The `user/login` Vuex action sends credentials to the mock endpoint, receives a token, and stores it in the cookie. The mock server accepts `admin` or `editor` as usernames with any password.

**Logout:** The `user/logout` action calls the logout endpoint, resets the token cookie, clears roles from Vuex, and resets the dynamically added routes via `resetRouter()`.

**Token-based re-login:** If the backend returns error codes 50008, 50012, or 50014, the response interceptor shows a confirmation dialog asking the user to re-login. Confirming clears the token and reloads the page.

### 6.4 Route-Level Permissions

Routes in `asyncRoutes` are dynamically filtered based on the user's roles. The filtering logic is in the `permission` Vuex store module.

**Admin role:** Gets all `asyncRoutes` without filtering.

**Other roles:** Each route's `meta.roles` array is checked. If the user's role is included, the route is accessible. If a parent route has `meta.roles`, all its children inherit that restriction unless they define their own.

Example from the source:

```javascript
// Only admin can access
{
  path: '/permission',
  component: Layout,
  meta: { roles: ['admin'] },
  children: [
    {
      path: 'page',
      component: () => import('@/views/permission/page'),
      meta: { title: 'Page Permission', roles: ['admin'] }
    },
    {
      path: 'directive',
      component: () => import('@/views/permission/directive'),
      meta: { title: 'Directive Permission' }
      // No roles = accessible to admin AND editor (inherits from parent being in asyncRoutes)
    },
    {
      path: 'role',
      component: () => import('@/views/permission/role'),
      meta: { title: 'Role Permission', roles: ['admin'] }
    }
  ]
}
```

### 6.5 Element-Level Permissions with v-permission

The `v-permission` directive controls visibility of individual elements:

```vue
<template>
  <div>
    <!-- Only visible to admin -->
    <el-button v-permission="['admin']">Admin Action</el-button>

    <!-- Visible to admin and editor -->
    <el-button v-permission="['admin','editor']">Edit</el-button>
  </div>
</template>
```

The directive compares the provided roles against the user's roles from the Vuex store. If the user lacks the required role, the element is **removed from the DOM** (not hidden — completely removed).

> [!CAUTION]
> The `v-permission` directive removes elements from the DOM. Once removed, they cannot be toggled back without a re-render. For elements that need to toggle visibility based on dynamic conditions, use `v-if` with `checkPermission()` instead.

### 6.6 Programmatic Permission Checks

For cases where a directive is not suitable (computed properties, method logic), use the `checkPermission` utility:

```javascript
import checkPermission from '@/utils/permission'

export default {
  methods: {
    hasAdminAccess() {
      return checkPermission(['admin'])
    }
  }
}
```

This function returns `true` if the current user has any of the specified roles.

### 6.7 Adding a New Role

To add a new role (e.g., `viewer`):

1. **Backend/Mock:** Return the new role from the user info endpoint
2. **Routes:** Add `'viewer'` to the `meta.roles` arrays of routes the new role should access
3. **Elements:** Use `v-permission="['viewer']"` or `checkPermission(['viewer'])` where needed
4. **Mock user:** Add a new user entry in `mock/user.js` that returns the `viewer` role

---

## 7. Components

vue-element-admin ships with 30+ reusable components in `src/components/`. Each is self-contained and can be imported individually.

### 7.1 Rich Text Editors

#### Tinymce

A full-featured WYSIWYG editor loaded from CDN. Located at `src/components/Tinymce/`.

```vue
<template>
  <tinymce v-model="content" :height="400" />
</template>

<script>
import Tinymce from '@/components/Tinymce'

export default {
  components: { Tinymce },
  data() {
    return { content: '' }
  }
}
</script>
```

Features: formatting toolbar, image upload, fullscreen editing, HTML source view. The editor script is loaded from a CDN URL — it is not bundled locally.

Demo: Navigate to **Components > Tinymce** in the sidebar.

#### Markdown Editor

Based on `tui-editor` (Toast UI Editor). Located at `src/components/MarkdownEditor/`.

```vue
<template>
  <markdown-editor v-model="content" height="500px" />
</template>

<script>
import MarkdownEditor from '@/components/MarkdownEditor'

export default {
  components: { MarkdownEditor },
  data() {
    return { content: '# Hello' }
  }
}
</script>
```

Supports both Markdown and WYSIWYG editing modes, live preview, and standard Markdown syntax.

Demo: Navigate to **Components > Markdown** in the sidebar.

#### JSON Editor

Powered by CodeMirror. Located at `src/components/JsonEditor/`.

```vue
<template>
  <json-editor v-model="jsonValue" />
</template>

<script>
import JsonEditor from '@/components/JsonEditor'

export default {
  components: { JsonEditor },
  data() {
    return { jsonValue: { key: 'value' } }
  }
}
</script>
```

Provides syntax highlighting, auto-formatting, and validation for JSON content.

Demo: Navigate to **Components > JSON Editor** in the sidebar.

### 7.2 Upload Components

#### Avatar Upload

An image cropper and uploader component with preview. Located at `src/components/ImageCropper/`.

Features: drag-and-drop upload, crop area selection, zoom, rotation, and preview in multiple sizes (circle and square).

Demo: Navigate to **Components > Avatar Upload** in the sidebar.

#### Dropzone

Drag-and-drop file upload area. Located at `src/components/Dropzone/`.

Features: drag files onto the drop area, progress display, file type filtering.

Demo: Navigate to **Components > Dropzone** in the sidebar.

#### Excel Upload

Upload `.xlsx` or `.xls` files and parse them into JSON. Located at `src/components/UploadExcel/`.

```vue
<template>
  <upload-excel :on-success="handleSuccess" :before-upload="beforeUpload" />
</template>

<script>
import UploadExcel from '@/components/UploadExcel'

export default {
  components: { UploadExcel },
  methods: {
    handleSuccess({ results, header }) {
      // results = array of row objects
      // header = array of column names
      this.tableData = results
      this.tableHeader = header
    },
    beforeUpload(file) {
      const isLt1M = file.size / 1024 / 1024 < 1
      if (!isLt1M) {
        this.$message.error('File must be smaller than 1MB')
      }
      return isLt1M
    }
  }
}
</script>
```

Uses the `xlsx` library to parse spreadsheet files client-side.

Demo: Navigate to **Excel > Upload Excel** in the sidebar.

### 7.3 Drag-and-Drop Components

#### Drag Dialog

Make any `el-dialog` draggable by adding the `v-el-drag-dialog` directive:

```vue
<template>
  <el-dialog v-el-drag-dialog title="Draggable Dialog" :visible.sync="visible">
    <p>You can drag this dialog by its title bar.</p>
  </el-dialog>
</template>

<script>
import elDragDialog from '@/directive/el-drag-dialog'

export default {
  directives: { elDragDialog },
  data() {
    return { visible: true }
  }
}
</script>
```

Demo: Navigate to **Components > Drag Dialog** in the sidebar.

#### Drag Select

Select multiple items by dragging a selection area. Located at `src/components/DragSelect/`.

Demo: Navigate to **Components > Drag Select** in the sidebar.

#### DnD List

Drag-and-drop sortable list using `sortablejs`. Located at `src/components/DndList/`.

Demo: Navigate to **Components > DnD List** in the sidebar.

#### Kanban Board

Drag-and-drop Kanban board. Located at `src/components/Kanban/`.

Demo: Navigate to **Components > Drag Kanban** in the sidebar.

### 7.4 Data Display Components

#### Pagination

Standardized pagination wrapper for Element UI's `el-pagination`. Located at `src/components/Pagination/`.

```vue
<template>
  <pagination
    :total="total"
    :page.sync="listQuery.page"
    :limit.sync="listQuery.limit"
    @pagination="getList"
  />
</template>

<script>
import Pagination from '@/components/Pagination'

export default {
  components: { Pagination },
  data() {
    return {
      total: 0,
      listQuery: { page: 1, limit: 20 }
    }
  },
  methods: {
    getList() {
      // Fetch data with this.listQuery.page and this.listQuery.limit
    }
  }
}
</script>
```

#### CountTo

Animated number counter. Smoothly transitions from 0 to a target value.

Demo: Navigate to **Components > Count To** in the sidebar.

#### BackToTop

Scroll-to-top button that appears when the user scrolls down. Located at `src/components/BackToTop/`.

```vue
<template>
  <div>
    <!-- Page content -->
    <back-to-top :visibility-height="300" :back-position="0" />
  </div>
</template>

<script>
import BackToTop from '@/components/BackToTop'

export default {
  components: { BackToTop }
}
</script>
```

Demo: Navigate to **Components > Back To Top** in the sidebar.

### 7.5 UI Enhancement Components

#### Sticky

Make any element stick to the top of the viewport on scroll. Located at `src/components/Sticky/`.

```vue
<template>
  <sticky :sticky-top="0" class-name="sticky-header">
    <div>This sticks to the top when scrolling</div>
  </sticky>
</template>

<script>
import Sticky from '@/components/Sticky'

export default {
  components: { Sticky }
}
</script>
```

Demo: Navigate to **Components > Sticky** in the sidebar.

#### SplitPane

Resizable split-panel layout. Drag the divider to resize.

Demo: Navigate to **Components > SplitPane** in the sidebar.

#### PanThumb

Thumbnail component with hover pan effect (image moves on mouse hover).

#### GithubCorner

The GitHub corner ribbon component shown in the top-right of the application.

#### RightPanel

The sliding settings panel triggered by the gear icon. Used internally for the settings panel but can be reused for any slide-out panel need.

#### ErrorLog

Displays a badge in the navbar showing the count of JavaScript errors. Click to see error details. Controlled by the `errorLog` setting in `src/settings.js`.

### 7.6 Charts (ECharts)

The project integrates ECharts 4.2.1 for data visualization. Chart components are in `src/views/charts/` (view-level) and `src/components/Charts/` (reusable wrappers).

Three demo charts are included:

| Chart | Path | Type |
|---|---|---|
| Keyboard Chart | `src/views/charts/keyboard` | Custom interactive |
| Line Chart | `src/views/charts/line` | Line chart |
| Mix Chart | `src/views/charts/mix` | Mixed bar + line |

Charts auto-resize on window resize via a mixin. Navigate to **Charts** in the sidebar to see the demos.

To add a new chart:

```vue
<template>
  <div ref="chart" style="width: 100%; height: 400px;" />
</template>

<script>
import echarts from 'echarts'
import resize from '@/components/Charts/mixins/resize'

export default {
  mixins: [resize],
  mounted() {
    this.chart = echarts.init(this.$refs.chart)
    this.chart.setOption({
      // ECharts option config
      xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
      yAxis: { type: 'value' },
      series: [{ data: [120, 200, 150], type: 'bar' }]
    })
  },
  beforeDestroy() {
    if (this.chart) {
      this.chart.dispose()
      this.chart = null
    }
  }
}
</script>
```

The `resize` mixin handles `window.addEventListener('resize', ...)` cleanup automatically.

---

## 8. Dashboard

The Dashboard is the landing page after login. It is affixed in the Tags View and cannot be closed.

The dashboard page is located at `src/views/dashboard/index.vue` and typically includes:

- **Panel Group** — Summary cards showing key metrics (new visits, messages, purchases, shopping count) with animated counters
- **Line Chart** — Trend data visualization
- **Bar Chart** — Category comparisons
- **Table** — Recent transaction or order data
- **Todo List** — Task management widget

All dashboard data comes from the mock server during development. Replace the API calls in `src/api/` with real endpoints when connecting to a backend.

---

## 9. Tables

Tables are a core feature of any admin panel. vue-element-admin includes four table implementations demonstrating different patterns.

### 9.1 Dynamic Table

Configurable columns that can be shown/hidden and reordered at runtime. Users can check/uncheck columns from a dropdown, and drag to reorder them.

Navigate to **Table > Dynamic Table** in the sidebar.

### 9.2 Drag Table

Table rows that can be reordered via drag-and-drop using `sortablejs`. Useful for priority lists, ordering, and manual sorting.

Navigate to **Table > Drag Table** in the sidebar.

### 9.3 Inline Edit Table

Click a cell to edit it in-place. Each row has Edit/Cancel buttons that toggle between display and edit modes.

Navigate to **Table > Inline Edit Table** in the sidebar.

### 9.4 Complex Table

Full-featured table with:

- Search and filter controls
- Column sorting
- Pagination
- Row status tags (published, draft, deleted)
- Action buttons (Edit, Publish, Delete)
- Create/Edit dialogs
- CSV export

Navigate to **Table > Complex Table** in the sidebar.

### 9.5 Table Patterns for Your Own Pages

The most common table pattern from the source:

```vue
<template>
  <div class="app-container">
    <!-- Filters -->
    <div class="filter-container">
      <el-input v-model="listQuery.title" placeholder="Title" style="width: 200px;" @keyup.enter.native="handleFilter" />
      <el-button type="primary" icon="el-icon-search" @click="handleFilter">Search</el-button>
    </div>

    <!-- Table -->
    <el-table :data="list" v-loading="listLoading" border>
      <el-table-column label="Title" prop="title" />
      <el-table-column label="Status" prop="status">
        <template slot-scope="{row}">
          <el-tag :type="row.status | statusFilter">{{ row.status }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column label="Actions" width="200">
        <template slot-scope="{row}">
          <el-button size="mini" @click="handleEdit(row)">Edit</el-button>
          <el-button size="mini" type="danger" @click="handleDelete(row)">Delete</el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- Pagination -->
    <pagination :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.limit" @pagination="getList" />
  </div>
</template>

<script>
import { fetchList } from '@/api/article'
import Pagination from '@/components/Pagination'

export default {
  components: { Pagination },
  data() {
    return {
      list: [],
      total: 0,
      listLoading: true,
      listQuery: { page: 1, limit: 20, title: '' }
    }
  },
  created() {
    this.getList()
  },
  methods: {
    getList() {
      this.listLoading = true
      fetchList(this.listQuery).then(response => {
        this.list = response.data.items
        this.total = response.data.total
        this.listLoading = false
      })
    },
    handleFilter() {
      this.listQuery.page = 1
      this.getList()
    },
    handleEdit(row) { /* ... */ },
    handleDelete(row) { /* ... */ }
  }
}
</script>
```

---

## 10. Excel, ZIP, and PDF Export

### 10.1 Export Table to Excel

Navigate to **Excel > Export Excel** in the sidebar.

The export feature uses the `xlsx` library (`file-saver` + `xlsx`). Export functions are in `src/vendor/Export2Excel.js`.

```javascript
import('@/vendor/Export2Excel').then(excel => {
  const tHeader = ['Id', 'Title', 'Author', 'Date']
  const data = this.list.map(v => [v.id, v.title, v.author, v.date])
  excel.export_json_to_excel({
    header: tHeader,
    data,
    filename: 'my-export',        // Default: 'excel-list'
    autoWidth: true,               // Auto-fit column widths
    bookType: 'xlsx'               // 'xlsx', 'csv', 'txt'
  })
})
```

#### Export Selected Rows

Navigate to **Excel > Export Selected**. Select rows using table checkboxes, then click Export. The export function receives only the selected rows.

#### Merge Header Export

Navigate to **Excel > Merge Header**. Demonstrates multi-level column headers in the exported spreadsheet.

### 10.2 Upload and Parse Excel

Navigate to **Excel > Upload Excel**. Uses the `UploadExcel` component to parse `.xlsx` files and display data in a table. See Section 7.2 for the component usage.

### 10.3 Export to ZIP

Navigate to **ZIP > Export Zip**. Uses `jszip` + `file-saver` to bundle data into a `.zip` download:

```javascript
import('@/vendor/Export2Zip').then(zip => {
  const tHeader = ['Id', 'Title']
  const data = this.list.map(v => [v.id, v.title])
  zip.export_txt_to_zip(tHeader, data, 'my-export', 'my-file')
})
```

### 10.4 Export to PDF

Navigate to **PDF** in the sidebar. Uses `html2canvas` + `jspdf` to capture the current view as a PDF. The export is purely client-side — it renders the DOM to a canvas and converts it to a PDF document.

---

## 11. Example: Article CRUD

The **Example** section in the sidebar demonstrates a complete CRUD workflow for articles. This serves as a reference implementation for building your own CRUD pages.

### 11.1 Article List

Path: **Example > Article List**

Features:
- Table with title, author, reviewer, importance rating, status, and timestamp
- Sortable columns
- Status filter (published, draft, deleted)
- Importance filter (1-3 stars)
- Full-text search by title
- Pagination
- Inline edit button opens a detail page

### 11.2 Create Article

Path: **Example > Create Article**

Features:
- Title input
- Rich text editor (Tinymce) for content body
- Remark input
- Source URL field
- Importance rating (1-3 stars)
- Publish and Draft buttons
- Form validation

### 11.3 Edit Article

Click Edit on any article in the list. The edit page pre-fills all fields. The route uses a dynamic parameter (`/example/edit/:id`) with `meta.activeMenu` set to highlight the article list in the sidebar.

This pattern of list + create + edit pages with shared API calls is the most common CRUD pattern in the codebase.

---

## 12. Custom Directives

### 12.1 v-permission

Role-based element visibility. See Section 6.5 for details.

```vue
<el-button v-permission="['admin']">Admin Only</el-button>
```

### 12.2 v-clipboard

Copy or cut text to the clipboard:

```vue
<template>
  <div>
    <el-button v-clipboard:copy="inputData" v-clipboard:success="clipboardSuccess">
      Copy
    </el-button>
  </div>
</template>

<script>
import clipboard from '@/directive/clipboard'

export default {
  directives: { clipboard },
  data() {
    return { inputData: 'Text to copy' }
  },
  methods: {
    clipboardSuccess() {
      this.$message.success('Copied!')
    }
  }
}
</script>
```

Modifiers: `v-clipboard:copy` (copy to clipboard), `v-clipboard:cut` (cut to clipboard).

Demo: Navigate to **Clipboard** in the sidebar.

### 12.3 v-el-drag-dialog

Makes any `el-dialog` draggable by its title bar. See Section 7.3 for usage.

### 12.4 v-el-table (Adaptive)

Auto-adjusts `el-table` height to fill available viewport space. Useful for full-page tables that should resize with the window.

### 12.5 v-waves

Adds a Material Design ripple effect on click:

```vue
<el-button v-waves>Click Me</el-button>
```

The ripple emanates from the click position. Import the directive:

```javascript
import waves from '@/directive/waves'
export default {
  directives: { waves }
}
```

### 12.6 v-sticky

Makes an element stick to the viewport edge on scroll. An alternative to the `Sticky` component for simpler use cases.

---

## 13. Global Filters

Filters registered globally in `src/filters/index.js` can be used in any template:

### 13.1 Date and Time Filters

#### parseTime

Formats a timestamp into a formatted date string:

```vue
<!-- Output: "2024-10-24 15:30:00" -->
{{ 1729783800000 | parseTime('{y}-{m}-{d} {h}:{i}:{s}') }}

<!-- Short format -->
{{ timestamp | parseTime('{y}-{m}-{d}') }}
```

Format tokens: `{y}` year, `{m}` month, `{d}` day, `{h}` hour, `{i}` minute, `{s}` second, `{a}` day of week.

#### formatTime

Returns a human-readable relative time for recent dates:

```vue
<!-- Output: "3 minutes ago", "1 hour ago", "1 day ago", or "10-24" -->
{{ timestamp | formatTime }}
```

Returns relative times for durations under 30 days; returns a date string for older dates.

#### timeAgo

Returns relative time strings:

```vue
<!-- Output: "3 minutes ago" -->
{{ timestamp | timeAgo }}
```

### 13.2 Number Filters

#### numberFormatter

Abbreviates large numbers:

```vue
<!-- Output: "10k" -->
{{ 10000 | numberFormatter }}

<!-- Output: "9.9k" -->
{{ 9900 | numberFormatter }}
```

Numbers below 10,000 display as-is.

#### toThousandFilter

Adds comma separators:

```vue
<!-- Output: "10,000,000" -->
{{ 10000000 | toThousandFilter }}
```

### 13.3 String Filters

#### uppercaseFirst

Capitalizes the first character:

```vue
<!-- Output: "Hello" -->
{{ 'hello' | uppercaseFirst }}
```

---

## 14. Utility Functions

The `src/utils/` directory provides shared utility functions used throughout the application.

### 14.1 src/utils/index.js

| Function | Purpose | Example |
|---|---|---|
| `parseTime(time, cFormat)` | Format timestamp to string | `parseTime(Date.now(), '{y}-{m}-{d}')` |
| `formatTime(time)` | Relative time string | `formatTime(someTimestamp)` |
| `getQueryObject(url)` | Parse URL query string to object | `getQueryObject(window.location.href)` |
| `byteLength(str)` | String byte length (CJK = 2) | `byteLength('hello')` → 5 |
| `cleanArray(arr)` | Remove falsy values | `cleanArray([0, 1, null, 3])` → `[1, 3]` |
| `param(json)` | Object to URL query string | `param({a: 1, b: 2})` → `'a=1&b=2'` |
| `param2Obj(url)` | URL query string to object | `param2Obj('?a=1&b=2')` → `{a: '1', b: '2'}` |
| `html2Text(val)` | Strip HTML tags | `html2Text('<p>hi</p>')` → `'hi'` |
| `objectMerge(target, source)` | Deep merge objects | `objectMerge(defaults, overrides)` |
| `debounce(func, wait)` | Debounce a function | `debounce(handleResize, 300)` |
| `deepClone(source)` | Deep clone an object | `deepClone(complexObject)` |
| `uniqueArr(arr)` | Deduplicate array | `uniqueArr([1, 2, 2, 3])` → `[1, 2, 3]` |
| `createUniqueString()` | Generate unique string | Used for element keys |
| `toggleClass(el, cls)` | Toggle CSS class | `toggleClass(element, 'active')` |
| `hasClass(el, cls)` | Check if element has class | `hasClass(element, 'active')` |
| `addClass(el, cls)` | Add CSS class | `addClass(element, 'active')` |
| `removeClass(el, cls)` | Remove CSS class | `removeClass(element, 'active')` |

### 14.2 src/utils/request.js

Axios instance configuration. See Section 3.4.

### 14.3 src/utils/auth.js

Token management:

| Function | Purpose |
|---|---|
| `getToken()` | Read `Admin-Token` from cookie |
| `setToken(token)` | Write token to cookie |
| `removeToken()` | Delete token cookie |

### 14.4 src/utils/validate.js

Validation helpers:

| Function | Purpose | Example |
|---|---|---|
| `validUsername(str)` | Check if username is valid | Mock users only: `admin`, `editor` |
| `validURL(url)` | Validate URL format | Returns boolean |
| `validLowerCase(str)` | Check all lowercase | Returns boolean |
| `validUpperCase(str)` | Check all uppercase | Returns boolean |
| `validAlphabets(str)` | Check all alphabetic | Returns boolean |
| `validEmail(str)` | Validate email format | Returns boolean |
| `isString(str)` | Check if value is string | Returns boolean |
| `isArray(arg)` | Check if value is array | Returns boolean |

### 14.5 src/utils/permission.js

The `checkPermission(value)` function for programmatic role checks. See Section 6.6.

### 14.6 src/utils/scroll-to.js

Smooth scroll utility used by the BackToTop component:

```javascript
import { scrollTo } from '@/utils/scroll-to'

// Scroll to top
scrollTo(0, 800)  // scrollTo(targetY, duration)
```

### 14.7 src/utils/clipboard.js

Clipboard copy utility function (used internally by the `v-clipboard` directive):

```javascript
import clip from '@/utils/clipboard'

clip(text, event)  // Copies text to clipboard
```

---

## 15. Nested Routes (Multi-Level Menus)

vue-element-admin supports multi-level nested menus. The sidebar auto-generates sub-menus from nested route children.

Navigate to **Nested** in the sidebar to see a three-level deep menu:

```
Nested
├── Menu 1
│   ├── Menu 1-1
│   ├── Menu 1-2
│   │   ├── Menu 1-2-1
│   │   └── Menu 1-2-2
│   └── Menu 1-3
└── Menu 2
```

Each level corresponds to a nested `children` array in the route definition. There is no hard limit on nesting depth, but more than three levels becomes unwieldy for sidebar navigation.

---

## 16. Error Handling

### 16.1 Error Log

The ErrorLog component captures JavaScript errors using `Vue.config.errorHandler` and displays them via a badge icon in the navbar. This is controlled by the `errorLog` setting in `src/settings.js`.

```javascript
// src/settings.js
errorLog: 'production'   // Only capture in production

// To enable in development too:
errorLog: ['production', 'development']
```

Click the bug icon in the navbar to view captured errors with:
- Error message
- Stack trace
- URL where the error occurred
- Timestamp

Navigate to **Error Log** in the sidebar to see the error log demo page. It deliberately throws an error to demonstrate the capture mechanism.

### 16.2 Error Pages

Two error pages are included:

- **401 Unauthorized** — Shown when a user tries to access a page without permission
- **404 Not Found** — Shown for any unrecognized route (the `*` catch-all route redirects to `/404`)

Both pages feature illustrated graphics. Navigate to **Error Pages > 401** or **Error Pages > 404** in the sidebar to preview them.

---

## 17. Theme Customization

### 17.1 Runtime Theme Switching

Click the theme color circle in the navbar to open the theme picker. Select a new primary color, and all Element UI components update immediately. This works by:

1. Fetching Element UI's compiled CSS
2. Replacing color values with regex
3. Appending a new `<style>` tag with the recolored CSS

The selected theme color persists in Vuex for the session but resets on page reload unless you implement persistence (e.g., localStorage).

### 17.2 SCSS Variable Customization

For permanent theme changes, edit `src/styles/element-variables.scss`:

```scss
/* Primary theme color */
$--color-primary: #1890ff;

/* Override other Element UI variables */
$--color-success: #67C23A;
$--color-warning: #E6A23C;
$--color-danger: #F56C6C;
$--color-info: #909399;

/* Font */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
```

Then rebuild the project for changes to take effect.

### 17.3 Sidebar Theme

Edit `src/styles/variables.scss` to change sidebar colors:

```scss
$menuBg: #304156;           // Sidebar background
$menuHover: #263445;         // Sidebar hover color
$menuText: #bfcbd9;          // Menu text color
$menuActiveText: #409EFF;    // Active menu item text
$subMenuBg: #1f2d3d;         // Sub-menu background
$subMenuHover: #001528;      // Sub-menu hover color
$subMenuActiveText: #f4f4f5; // Active sub-menu text
$sideBarWidth: 210px;        // Sidebar width
```

> [!NOTE]
> Both runtime theme switching and SCSS variable customization are available, but they operate independently. Runtime switching only affects Element UI component colors; sidebar and custom styles require SCSS changes. See GitHub issue #3606 for the known limitation that the two methods cannot fully coexist.

---

## 18. Settings Panel

The settings panel is the gear icon in the top-right corner. When `showSettings: true` in `src/settings.js`, clicking it slides out a right-side panel with toggle switches for:

| Toggle | Setting | Effect |
|---|---|---|
| Show Tags View | `tagsView` | Show/hide the tab navigation bar |
| Fixed Header | `fixedHeader` | Pin/unpin the navbar during scroll |
| Show Sidebar Logo | `sidebarLogo` | Show/hide the logo above the sidebar |

Changes take effect immediately and are stored in the Vuex `settings` module. They reset on page reload.

To make settings persistent across reloads, save them to `localStorage` in the Vuex settings module:

```javascript
// In src/store/modules/settings.js
const actions = {
  changeSetting({ commit }, data) {
    commit('CHANGE_SETTING', data)
    // Persist to localStorage
    localStorage.setItem(`setting_${data.key}`, JSON.stringify(data.value))
  }
}
```

---

## 19. Workflows and Recipes

### 19.1 Building a User Management Page

1. Create `src/api/user-management.js` with list, create, update, delete functions
2. Add mock data in `mock/user-management.js`
3. Create `src/views/user-management/index.vue` with el-table, filters, and pagination
4. Add the route to `asyncRoutes` with `meta.roles: ['admin']`
5. Add an Edit dialog using `el-dialog` with form validation
6. Use `v-permission="['admin']"` on the delete button

### 19.2 Adding a Dashboard Widget

1. Create a new component in `src/views/dashboard/components/`
2. Import and register it in the dashboard index page
3. Add an API call in `src/api/` for the widget data
4. Add corresponding mock data
5. Use ECharts if it is a chart widget (import the resize mixin)

### 19.3 Implementing File Upload with Preview

1. Use the `el-upload` component from Element UI
2. Configure `action` to point to your upload endpoint
3. Use `:before-upload` for file validation (size, type)
4. Use `:on-success` to handle the response and store the file URL
5. Display uploaded files using `el-image` or a custom preview component

### 19.4 Adding a Multi-Tab Detail Page

1. Create a view with `el-tabs` containing multiple `el-tab-pane` components
2. Use route query parameters to track the active tab: `this.$route.query.tab`
3. Set `noCache: true` if tab state should reset on re-entry
4. Use `meta.activeMenu` to keep the parent menu highlighted

### 19.5 Creating a Wizard/Stepper Form

1. Use `el-steps` component for the progress indicator
2. Track current step in component data
3. Use `v-show` (not `v-if`) for each step's form fields to preserve validation state
4. Validate each step with `this.$refs.form.validateField(fields)` before advancing
5. Submit all collected data on the final step

### 19.6 Building a Tree-Based Navigation

1. Use `el-tree` with `default-expand-all` or lazy loading
2. Handle `@node-click` to load detail in a right panel
3. Use `el-tree` props: `data`, `props`, `node-key`, `highlight-current`
4. Combine with `SplitPane` component for resizable tree + detail layout

### 19.7 Implementing Global Search

The HeaderSearch component already provides fuzzy route search. To add data search:

1. Create a search API endpoint
2. Use `el-autocomplete` or `el-select` with remote search
3. Debounce the search input (use the `debounce` utility from `src/utils/index.js`)
4. Navigate to the result on selection

### 19.8 Adding Keyboard Shortcuts

1. Listen for keydown events in your component's `mounted` hook
2. Clean up in `beforeDestroy`
3. Use the existing pattern from HeaderSearch (which uses `keyup` events)
4. Consider using a library like `mousetrap` for complex shortcuts

### 19.9 Batch Operations on Table Data

1. Add `selection` type column to `el-table`: `<el-table-column type="selection" />`
2. Handle `@selection-change` to track selected rows
3. Add batch action buttons above the table
4. Confirm with `this.$confirm` before destructive batch operations
5. Show loading state during the batch API call

### 19.10 Implementing Infinite Scroll

1. Use `el-table` with a fixed height
2. Listen for the table's scroll event
3. When scroll reaches bottom, load the next page
4. Append new data to the existing array (do not replace)
5. Show a loading indicator at the bottom

### 19.11 Form Validation Patterns

Element UI's `el-form` with rules:

```vue
<template>
  <el-form ref="form" :model="form" :rules="rules">
    <el-form-item label="Name" prop="name">
      <el-input v-model="form.name" />
    </el-form-item>
    <el-form-item label="Email" prop="email">
      <el-input v-model="form.email" />
    </el-form-item>
    <el-button @click="submitForm">Submit</el-button>
  </el-form>
</template>

<script>
import { validEmail } from '@/utils/validate'

export default {
  data() {
    const validateEmail = (rule, value, callback) => {
      if (!validEmail(value)) {
        callback(new Error('Invalid email'))
      } else {
        callback()
      }
    }
    return {
      form: { name: '', email: '' },
      rules: {
        name: [{ required: true, message: 'Name is required', trigger: 'blur' }],
        email: [{ required: true, trigger: 'blur' }, { validator: validateEmail, trigger: 'blur' }]
      }
    }
  },
  methods: {
    submitForm() {
      this.$refs.form.validate(valid => {
        if (valid) {
          // Submit
        }
      })
    }
  }
}
</script>
```

### 19.12 Conditional Form Fields

Show/hide form fields based on selections:

```vue
<el-form-item label="Type" prop="type">
  <el-select v-model="form.type">
    <el-option label="Basic" value="basic" />
    <el-option label="Advanced" value="advanced" />
  </el-select>
</el-form-item>
<el-form-item v-if="form.type === 'advanced'" label="Config" prop="config">
  <json-editor v-model="form.config" />
</el-form-item>
```

### 19.13 Auto-Saving Form Drafts

Persist form state to localStorage to prevent data loss:

```javascript
export default {
  watch: {
    form: {
      handler(val) {
        localStorage.setItem('draft_article', JSON.stringify(val))
      },
      deep: true
    }
  },
  created() {
    const draft = localStorage.getItem('draft_article')
    if (draft) {
      this.form = JSON.parse(draft)
    }
  }
}
```

### 19.14 Dynamic Column Configuration

Let users pick which table columns to show:

```vue
<template>
  <div>
    <el-checkbox-group v-model="visibleColumns">
      <el-checkbox v-for="col in allColumns" :key="col.prop" :label="col.prop">
        {{ col.label }}
      </el-checkbox>
    </el-checkbox-group>
    <el-table :data="list">
      <el-table-column v-for="col in filteredColumns" :key="col.prop" :label="col.label" :prop="col.prop" />
    </el-table>
  </div>
</template>

<script>
export default {
  data() {
    return {
      visibleColumns: ['id', 'title', 'status'],
      allColumns: [
        { prop: 'id', label: 'ID' },
        { prop: 'title', label: 'Title' },
        { prop: 'author', label: 'Author' },
        { prop: 'status', label: 'Status' },
        { prop: 'date', label: 'Date' }
      ]
    }
  },
  computed: {
    filteredColumns() {
      return this.allColumns.filter(c => this.visibleColumns.includes(c.prop))
    }
  }
}
</script>
```

### 19.15 Integrating a Third-Party Library

1. Install: `npm install library-name`
2. If the library has a Vue wrapper, register it globally in `main.js` or locally in the component
3. If it requires DOM access, initialize in `mounted()` and clean up in `beforeDestroy()`
4. For large libraries, use dynamic import to keep the main bundle small: `const lib = () => import('library-name')`
5. If the library has CSS, import it in the component or globally

### 19.16 Responsive Sidebar Behavior

The sidebar automatically collapses on mobile-width screens. The `app` Vuex module tracks the `device` state (`desktop` or `mobile`). On mobile:

- The sidebar opens as an overlay
- Clicking outside the sidebar closes it
- The hamburger toggle controls the overlay

This is handled automatically by the layout system. Breakpoint detection happens in `src/layout/mixin/ResizeHandler.js`.

### 19.17 Adding a Notification System

1. Use `el-badge` for unread count in the navbar
2. Create a notification dropdown with `el-popover`
3. Poll the notification API on a timer (or use WebSocket)
4. Mark as read on click, update the badge count
5. Use `Notification` component from Element UI for real-time push alerts

### 19.18 Exporting Filtered Table Data

1. Apply filters to the table data
2. Use the `Export2Excel` utility on the filtered dataset:

```javascript
handleExport() {
  import('@/vendor/Export2Excel').then(excel => {
    const tHeader = this.filteredColumns.map(c => c.label)
    const filterVal = this.filteredColumns.map(c => c.prop)
    const data = this.filteredList.map(v => filterVal.map(j => v[j]))
    excel.export_json_to_excel({
      header: tHeader,
      data,
      filename: `export_${parseTime(Date.now(), '{y}{m}{d}')}`
    })
  })
}
```

### 19.19 Password Change Flow

1. Create a dialog with old password, new password, and confirm password fields
2. Validate that new password matches confirmation
3. Validate minimum length and complexity
4. Call the update password API
5. On success, force logout and redirect to login

### 19.20 Role-Specific Dashboard Views

Show different dashboard content based on user role:

```vue
<template>
  <div>
    <admin-dashboard v-if="isAdmin" />
    <editor-dashboard v-else />
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters(['roles']),
    isAdmin() {
      return this.roles.includes('admin')
    }
  }
}
</script>
```

### 19.21 Page Loading States

Use Element UI's `v-loading` directive for loading states:

```vue
<el-table v-loading="listLoading" :data="list">
  <!-- columns -->
</el-table>
```

For full-page loading, use a loading overlay:

```javascript
const loading = this.$loading({
  lock: true,
  text: 'Processing...',
  background: 'rgba(0, 0, 0, 0.7)'
})
// When done:
loading.close()
```

### 19.22 Custom Error Boundaries

Wrap unreliable components in error boundaries using Vue's `errorCaptured` hook:

```javascript
export default {
  errorCaptured(err, vm, info) {
    console.error('Component error:', err, info)
    this.hasError = true
    return false  // Prevent propagation
  }
}
```

### 19.23 Printing Specific Sections

Use `window.print()` with print-specific CSS:

```vue
<el-button @click="handlePrint">Print</el-button>
```

```scss
@media print {
  .sidebar-container,
  .navbar,
  .tags-view-container,
  .right-panel {
    display: none !important;
  }
  .app-main {
    margin-left: 0 !important;
    padding: 0 !important;
  }
}
```

### 19.24 Deep Linking to Tabs

Preserve active tab state in the URL for shareable links:

```javascript
computed: {
  activeTab: {
    get() {
      return this.$route.query.tab || 'first'
    },
    set(val) {
      this.$router.replace({ query: { ...this.$route.query, tab: val } })
    }
  }
}
```

---

## 20. Troubleshooting

### 20.1 Installation Issues

#### Error: "node-sass" build failures

**Cause:** `node-sass` requires native compilation and frequently fails on newer Node versions.

**Solution:**

```bash
# Option 1: Use a compatible Node version
nvm use 14

# Option 2: Replace node-sass with dart-sass
npm uninstall node-sass
npm install sass --save-dev
```

**Prevention:** Use Node 12.x or 14.x for best compatibility with this project's dependency tree.

#### Error: "gyp ERR! build error" during npm install

**Cause:** Missing native build tools (Python 2.7, Visual C++ Build Tools on Windows, Xcode Command Line Tools on macOS).

**Solution (Windows):**

```bash
npm install --global windows-build-tools
```

**Solution (macOS):**

```bash
xcode-select --install
```

**Solution (Linux):**

```bash
sudo apt-get install build-essential python
```

#### Error: "ERESOLVE unable to resolve dependency tree"

**Cause:** npm 7+ enforces strict peer dependency resolution.

**Solution:**

```bash
npm install --legacy-peer-deps
```

#### Error: "ERR_OSSL_EVP_UNSUPPORTED" or hash-related build failures

**Cause:** Node 17+ uses OpenSSL 3.0 which removed support for some legacy hash algorithms used by webpack.

**Solution:**

```bash
# Set environment variable before running dev or build
export NODE_OPTIONS=--openssl-legacy-provider

# Windows (PowerShell)
$env:NODE_OPTIONS="--openssl-legacy-provider"

# Windows (CMD)
set NODE_OPTIONS=--openssl-legacy-provider

npm run dev
```

#### Error: "Cannot find module" after git pull

**Cause:** New dependencies were added but not installed.

**Solution:**

```bash
rm -rf node_modules
npm install
```

### 20.2 Development Server Issues

#### Error: "Port 9527 is already in use"

**Cause:** Another process is using port 9527.

**Solution:**

```bash
# Use a different port
port=8080 npm run dev

# Or kill the process using 9527
# Linux/macOS:
lsof -ti:9527 | xargs kill -9
# Windows:
netstat -ano | findstr :9527
taskkill /PID <pid> /F
```

#### Dev server starts but page is blank

**Cause:** Usually a JavaScript error in the console preventing Vue from mounting.

**Solution:**
1. Open browser DevTools (F12)
2. Check the Console tab for errors
3. Common causes: missing environment variable, import path typo, syntax error in a recently edited file

#### Hot Module Replacement (HMR) not working

**Cause:** File system watchers may have limits, or the file is outside the watched directory.

**Solution:**
1. Restart the dev server
2. On Linux, increase file watchers: `echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p`
3. Verify the edited file is under `src/`

#### Error: "Request failed with status code 404" on API calls

**Cause:** Mock server route does not match the request URL.

**Solution:**
1. Check that the URL in `src/api/*.js` matches the `url` field in the corresponding `mock/*.js` file
2. Mock URLs must match exactly, including the path prefix
3. Check the Network tab in DevTools to see the actual requested URL

### 20.3 Build Issues

#### Error: "JavaScript heap out of memory"

**Cause:** Large project exceeds Node's default memory limit.

**Solution:**

```bash
# Increase Node memory limit
export NODE_OPTIONS="--max-old-space-size=4096"
npm run build:prod
```

#### Build succeeds but app shows blank page in production

**Cause:** Usually a `publicPath` issue when deploying to a subdirectory.

**Solution:**

In `vue.config.js`, set `publicPath` to match your deploy path:

```javascript
module.exports = {
  publicPath: '/admin/'  // If deploying to example.com/admin/
}
```

Also ensure your web server is configured to serve `index.html` for all routes (SPA fallback).

#### Build output is too large

**Cause:** Large dependencies bundled without optimization.

**Solution:**
1. Use CDN for large libraries by configuring `externals` in `vue.config.js`
2. Check bundle analysis: add `--report` flag to the build command
3. Lazy-load routes (already done by default with `() => import(...)`)
4. Remove unused components and dependencies

### 20.4 Authentication Issues

#### Error: "Illegal token" / "Token expired" popup keeps appearing

**Cause:** Response interceptor detected error codes 50008, 50012, or 50014 from the backend.

**Solution:**
1. Clear cookies and log in again
2. If using a real backend, verify the token TTL and refresh mechanism
3. Check that the backend sends `code: 20000` for valid responses

#### Login redirects back to login page

**Cause:** Token is being set but `getInfo()` fails, causing the route guard to redirect.

**Solution:**
1. Check the Network tab for the user info API response
2. Verify the response contains `roles` data
3. Ensure `code: 20000` is returned

#### After login, sidebar shows no menu items

**Cause:** The user's role does not match any `asyncRoutes`.

**Solution:**
1. Verify the roles returned by `getInfo()` match the role strings in route `meta.roles`
2. Remember: roles are case-sensitive
3. Admin role gets all async routes; other roles need explicit listing

### 20.5 Routing Issues

#### Error: "Duplicate named routes" warning in console

**Cause:** `router.addRoutes()` called multiple times (e.g., on page refresh while already logged in).

**Solution:** The project includes `resetRouter()` in `src/router/index.js` which creates a fresh matcher. Ensure it is called before `addRoutes()`.

#### Page cached when it should not be (stale data)

**Cause:** `<keep-alive>` caches the page. The component's `name` matches the route's `name`, enabling caching.

**Solution:**
1. Set `meta.noCache: true` on the route
2. Or use a different component `name` than the route `name`
3. Or manually remove from cache: `this.$store.dispatch('tagsView/delCachedView', view)`

#### 404 page shows for a valid route

**Cause:** The `*` catch-all route is evaluated before async routes are added.

**Solution:** The `*` catch-all must be the last route. It is placed at the end of `asyncRoutes` in the source code, so it is only added after `addRoutes()`. Do not add it to `constantRoutes`.

### 20.6 Component Issues

#### Tinymce editor does not load

**Cause:** CDN URL for Tinymce script is unreachable (network issues, CDN down).

**Solution:**
1. Check the CDN URL in the Tinymce component
2. Try an alternative CDN or host Tinymce locally
3. Check browser DevTools Network tab for failed script loads
4. GitHub issue #4335 reports broken githubusercontent links

#### ECharts chart does not resize with the window

**Cause:** The resize mixin is not included or the chart instance variable name does not match.

**Solution:**
1. Import and use the resize mixin: `mixins: [resize]`
2. Store the chart instance as `this.chart` (the mixin expects this property name)
3. Make sure the chart container has a percentage-based or responsive width

#### Element UI components are too large/small

**Cause:** Global size setting may have been changed.

**Solution:**
1. Check the Element UI size selector in the navbar
2. Reset to `medium` or `default`
3. The size is stored in the `app` Vuex module and can be set programmatically

#### v-permission directive not working

**Cause:** Component is rendered before roles are loaded, or the directive is not imported.

**Solution:**
1. Import the directive: `import permission from '@/directive/permission'`
2. Register it: `directives: { permission }`
3. Or use the globally registered `v-permission` (which should be available in all components)
4. Verify roles are loaded in Vuex store

### 20.7 Styling Issues

#### Custom styles are overridden by Element UI

**Cause:** Element UI styles have higher specificity or are loaded after custom styles.

**Solution:**

```scss
/* Use deep selector for scoped styles */
::v-deep .el-button {
  background-color: #1890ff;
}

/* Or increase specificity */
.my-container .el-button {
  background-color: #1890ff;
}
```

#### Sidebar width changes cause layout shift

**Cause:** `$sideBarWidth` variable changed but transitions not adjusted.

**Solution:** Update `$sideBarWidth` in `src/styles/variables.scss`. The sidebar collapse animation uses CSS transitions, so extreme width changes may look jarring. Keep the width between 180px and 260px for best results.

#### Print output includes sidebar and navbar

**Cause:** No print-specific CSS rules.

**Solution:** See Recipe 19.23 for print CSS media query rules.

### 20.8 Performance Issues

#### Page loads slowly with many tabs open in Tags View

**Cause:** All cached views remain in memory due to `<keep-alive>`.

**Solution:**
1. Set `noCache: true` on routes with heavy components
2. Limit the maximum number of cached views by customizing the `tagsView` store module
3. Close unused tabs via right-click context menu

#### Large table (1000+ rows) is sluggish

**Cause:** Element UI's `el-table` renders all rows by default.

**Solution:**
1. Use server-side pagination (do not load all rows at once)
2. Use virtual scrolling for very large datasets (third-party plugin required)
3. Reduce the number of reactive properties in each row object

#### Bundle size is over 2MB

**Cause:** Large dependencies included in the main bundle.

**Solution:**
1. Load Tinymce from CDN (already default)
2. Load ECharts on-demand: `import('echarts/lib/chart/bar')` instead of `import echarts from 'echarts'`
3. Use `vue.config.js` externals to exclude Element UI and Vue from the bundle when serving from CDN
4. Enable gzip compression on your web server

### 20.9 Deployment Issues

#### Deployed app shows 404 on page refresh

**Cause:** The web server does not fall back to `index.html` for SPA routes.

**Solution:**

**Nginx:**

```nginx
location / {
  try_files $uri $uri/ /index.html;
}
```

**Apache (.htaccess):**

```apache
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>
```

#### Assets not loading (CSS/JS 404)

**Cause:** `publicPath` in `vue.config.js` does not match the deployment path.

**Solution:** Set `publicPath` to `/` for root deployment or `/your-path/` for subdirectory deployment.

#### CORS errors in production

**Cause:** Backend and frontend are on different domains, and CORS headers are not configured.

**Solution:** Configure CORS on your backend, or use a reverse proxy (like Nginx) to serve both frontend and API from the same domain.

### 20.10 Mock Server Issues

#### Mock data returns but table shows empty

**Cause:** Response format mismatch. The table expects `response.data.items` but mock returns a different structure.

**Solution:** Check the mock response structure matches what the component expects. The standard pattern is `{ code: 20000, data: { items: [...], total: N } }`.

#### Mock server not intercepting requests

**Cause:** Mock setup was removed or the URL path does not match.

**Solution:**
1. Verify `mock/index.js` is imported in the entry file
2. Check that mock `url` fields match the API request URLs exactly
3. MockJS intercepts XMLHttpRequest — if you are using `fetch`, MockJS will not intercept it

---

## 21. FAQ

**Q: What version of Vue does vue-element-admin use?**
Vue 2.6.10. It is not compatible with Vue 3.x. Migrating to Vue 3 would require rewriting components, directives, filters (removed in Vue 3), and the Vuex store.

**Q: What version of Element UI is included?**
Element UI 2.13.2. This is the Vue 2 version. For Vue 3, the equivalent library is Element Plus, but it is not a drop-in replacement.

**Q: Can I use this with TypeScript?**
Not out of the box. The project is entirely JavaScript. To add TypeScript, you would need to configure `tsconfig.json`, add Vue type declarations, and gradually convert `.js` files. This is a non-trivial migration for a project of this size.

**Q: Is there a simplified version without all the demo pages?**
Yes. PanJiaChen maintains `vue-admin-template`, a minimal version with only the core layout, login, and a blank dashboard. Use it as a starting point if you do not need the 30+ demo components.

**Q: How do I remove demo pages I do not need?**
1. Delete the view folder from `src/views/` (e.g., `src/views/charts/`)
2. Remove the corresponding route from `src/router/index.js`
3. Remove the corresponding API module from `src/api/` if any
4. Remove the corresponding mock module from `mock/`
5. Run `npm run lint` to catch any broken imports

**Q: How do I change the app title?**
Edit `title` in `src/settings.js`. The title appears in the browser tab, combined with each page's `meta.title` via the route `afterEach` guard in `src/permission.js`.

**Q: How do I change the logo?**
1. Replace the logo image in `src/assets/` or the sidebar logo component
2. The sidebar logo component is in `src/layout/components/Sidebar/Logo.vue`
3. Set `sidebarLogo: true` in `src/settings.js` to display it

**Q: How do I add a new sidebar menu with an icon?**
Add a route with `meta.icon` set to the name of an SVG file in `src/icons/svg/`. See Section 5.1 for the full process.

**Q: Can I have a route visible in the sidebar but not accessible to certain roles?**
Yes. Add the route to `asyncRoutes` with `meta.roles` specifying which roles can access it. Roles not listed will not see the route in their sidebar.

**Q: How do I make a page always accessible regardless of role?**
Add the route to `constantRoutes` instead of `asyncRoutes`. Constant routes are available to all authenticated users.

**Q: How do I hide a page from the sidebar but still make it accessible?**
Set `meta.hidden: true` on the route. The route remains functional via direct URL or programmatic navigation, but it will not appear in the sidebar.

**Q: How do I redirect from one route to another?**
Use the `redirect` property on a route:

```javascript
{
  path: '/old-path',
  redirect: '/new-path'
}
```

**Q: How do I open an external URL from the sidebar?**
Add the full URL (starting with `http://` or `https://`) as the route path. The sidebar renders it as an `<a>` tag with `target="_blank"`.

**Q: How do I add breadcrumb navigation to a nested page?**
Breadcrumbs are automatic. They generate from the route's `matched` array. Ensure each parent route has `meta.title` set. Set `meta.breadcrumb: false` to hide a specific level.

**Q: How do I keep a page's state when switching between tabs?**
By default, pages with matching `name` properties are cached by `<keep-alive>`. Ensure your component's `name` matches the route's `name`. If caching does not work, check that `meta.noCache` is not set to `true`.

**Q: How do I force a page to reload when its tab is clicked?**
Right-click the tab and select "Refresh" from the context menu. Programmatically, dispatch `tagsView/delCachedView` and then re-navigate.

**Q: How do I customize the login page?**
Edit `src/views/login/index.vue`. The login form, background, and validation are all in this single file. The login API call goes to `src/api/user.js`.

**Q: Can I use a different authentication method (JWT, OAuth)?**
Yes. Replace the token handling in `src/utils/auth.js` and the login/getInfo actions in `src/store/modules/user.js`. The route guard in `src/permission.js` only checks for the presence of a token — it does not care how the token was obtained.

**Q: How do I add a loading spinner to a page?**
Use Element UI's `v-loading` directive on any container element. For full-page loading, use `this.$loading()`. See Recipe 19.21.

**Q: How do I handle 401/403 errors from a real backend?**
The response interceptor in `src/utils/request.js` handles codes 50008/50012/50014. For standard HTTP 401/403 status codes, add handling in the response interceptor's error block:

```javascript
service.interceptors.response.use(
  response => { /* ... */ },
  error => {
    if (error.response && error.response.status === 401) {
      // Handle unauthorized
      store.dispatch('user/resetToken').then(() => {
        location.reload()
      })
    }
    return Promise.reject(error)
  }
)
```

**Q: How do I add Google Analytics or similar tracking?**
Add the tracking script to `public/index.html`, or install the Vue plugin (`vue-analytics`, `vue-gtag`) and configure it in `main.js`.

**Q: How do I change the default language of Element UI?**
In `main.js`, import the locale and set it:

```javascript
import locale from 'element-ui/lib/locale/lang/en'
Vue.use(ElementUI, { locale })
```

Replace `en` with the desired language code (e.g., `zh-CN`, `ja`, `ko`).

**Q: How do I deploy to a CDN?**
1. Set `publicPath` in `vue.config.js` to your CDN URL
2. Run `npm run build:prod`
3. Upload the `dist/` contents to your CDN
4. Configure your web server to serve `index.html` locally and reference CDN assets

**Q: How do I add a watermark to pages?**
Create a canvas-based watermark utility and apply it as a directive or in the layout component. The project does not include a watermark feature by default.

**Q: Why does my page flash/flicker on load?**
This is often caused by route guard redirects. The route guard checks the token, then fetches user info, then generates routes — all before showing the page. If `getInfo()` is slow, add a full-page loading indicator in `src/permission.js`.

**Q: How do I debug Vuex state changes?**
Install Vue DevTools browser extension. It provides a Vuex inspector showing all state, mutations, and actions in real-time. Works with Vue 2.6.

**Q: Can I use this project without the mock server?**
Yes. Remove the mock imports and point `VUE_APP_BASE_API` to your real backend. See Section 5.6 for the full process.

**Q: What IDE setup is recommended?**
VSCode with these extensions:
- Vetur (Vue tooling)
- ESLint (code quality)
- Prettier (formatting — configure to match the project's ESLint rules)

The project includes `.eslintrc.js` for consistent code style.

**Q: How do I update Element UI to a newer version?**
```bash
npm update element-ui
```
Then check for breaking changes in the Element UI changelog. Minor updates within 2.x should be safe. Do not upgrade to Element Plus (that is for Vue 3).

---

## 22. Route Reference

### 22.1 Constant Routes (All Roles)

These routes are available to every authenticated user:

| Path | Page | Notes |
|---|---|---|
| `/` | Redirect to `/dashboard` | |
| `/dashboard` | Dashboard | Affix (pinned tab) |
| `/documentation` | Documentation | Affix (pinned tab) |
| `/guide` | Guide | Interactive guide page |
| `/profile` | User Profile | Hidden from sidebar |
| `/login` | Login | Hidden from sidebar |
| `/auth-redirect` | Auth Redirect | Hidden from sidebar |
| `/401` | 401 Error | Hidden from sidebar |
| `/404` | 404 Error | Hidden from sidebar |

### 22.2 Async Routes (Role-Filtered)

These routes are dynamically added based on user roles:

| Path | Page | Required Roles | Children |
|---|---|---|---|
| `/permission` | Permission | admin, editor | Page (admin), Directive, Role (admin) |
| `/icon` | Icons | all | Icon gallery |
| `/components` | Components | all | 14 component demos |
| `/charts` | Charts | all | Keyboard, Line, Mix |
| `/nested` | Nested | all | Multi-level menus |
| `/table` | Table | all | Dynamic, Drag, Inline Edit, Complex |
| `/example` | Example | all | Article List, Create, Edit |
| `/tab` | Tab | all | Tab demo |
| `/error` | Error Pages | all | 401, 404 |
| `/error-log` | Error Log | all | Error log page |
| `/excel` | Excel | all | Export, Selected, Merge, Upload |
| `/zip` | Zip | all | Export Zip |
| `/pdf` | PDF | all | PDF export |
| `/theme` | Theme | all | Theme picker |
| `/clipboard` | Clipboard | all | Clipboard demo |
| (external link) | External Link | all | Opens in new tab |

---

## 23. Vuex Store Reference

### 23.1 Modules

| Module | State | Key Actions | Purpose |
|---|---|---|---|
| `app` | `sidebar.opened`, `sidebar.withoutAnimation`, `device`, `size` | `toggleSideBar`, `closeSideBar`, `toggleDevice`, `setSize` | App-level UI state |
| `errorLog` | `logs` | `addErrorLog`, `clearErrorLog` | JavaScript error collection |
| `permission` | `routes`, `addRoutes` | `generateRoutes(roles)` | Dynamic route generation |
| `settings` | `theme`, `showSettings`, `tagsView`, `fixedHeader`, `sidebarLogo` | `changeSetting({ key, value })` | UI settings panel state |
| `tagsView` | `visitedViews`, `cachedViews` | `addView`, `delView`, `delCachedView`, `delOthersViews`, `delAllViews`, `updateVisitedView` | Tab navigation state |
| `user` | `token`, `name`, `avatar`, `introduction`, `roles` | `login`, `getInfo`, `logout`, `resetToken`, `changeRoles` | Authentication and user data |

### 23.2 Getters

Centralized getters in `src/store/getters.js`:

| Getter | Source | Returns |
|---|---|---|
| `sidebar` | `app.sidebar` | Sidebar state object |
| `size` | `app.size` | Element UI component size |
| `device` | `app.device` | `'desktop'` or `'mobile'` |
| `visitedViews` | `tagsView.visitedViews` | Array of visited route objects |
| `cachedViews` | `tagsView.cachedViews` | Array of cached component names |
| `token` | `user.token` | Auth token string |
| `avatar` | `user.avatar` | User avatar URL |
| `name` | `user.name` | User display name |
| `introduction` | `user.introduction` | User bio/introduction |
| `roles` | `user.roles` | Array of role strings |
| `permission_routes` | `permission.routes` | All accessible routes |
| `errorLogs` | `errorLog.logs` | Array of error log entries |

---

## 24. Keyboard Shortcuts and Navigation Tips

### 24.1 Built-in Shortcuts

| Shortcut | Action |
|---|---|
| Click search icon in navbar | Open header search (fuzzy route finder) |
| Type in header search | Filter routes by title (Fuse.js fuzzy match) |
| Click screenfull icon | Toggle browser fullscreen |
| Right-click a tag/tab | Open context menu (Refresh, Close, Close Others, Close All) |

### 24.2 Navigation Tips

- **Quick navigation:** Use the header search to jump to any page by typing part of its title
- **Organize tabs:** Right-click and "Close Others" to clear clutter
- **Refresh a cached page:** Right-click its tab and select "Refresh"
- **Mobile mode:** The sidebar auto-collapses below the mobile breakpoint; tap the hamburger to toggle
- **Collapse sidebar:** Click the hamburger icon for more content space; the sidebar collapses to icon-only mode

---

## 25. Best Practices

### 25.1 Project Organization

- **One view per folder** — Each page lives in `src/views/{feature}/index.vue` with sub-components alongside it
- **API modules mirror views** — `src/api/article.js` serves `src/views/article/`
- **Component naming** — Use PascalCase for component names, match the folder name
- **Lazy load all routes** — Use `() => import(...)` for every route component to enable code splitting

### 25.2 Performance

- **Avoid large Vuex state** — Do not store entire API responses if only a subset is needed
- **Use `noCache` wisely** — Cache pages that are expensive to render; skip caching for pages with rapidly changing data
- **Lazy load heavy components** — Tinymce, ECharts, and JSON editor are already lazy-loaded via dynamic imports. Follow this pattern for any large dependency
- **Debounce search inputs** — Use the `debounce` utility for search fields that trigger API calls

### 25.3 Security

- **Never store sensitive data in Vuex or localStorage** — Tokens in cookies (with httpOnly in production) are safer
- **Validate on the server** — Client-side permission checks (v-permission, route guards) are UX features, not security boundaries. Always validate roles and permissions on your backend
- **Sanitize user input** — Use `html2Text` from utils when displaying user-generated content to prevent XSS

> [!WARNING]
> The mock server is for development only. It provides no real authentication or authorization. Do not deploy the mock server to production. All permission checks in the frontend are for UX purposes — your real backend must enforce authorization independently.

### 25.4 Code Style

- Follow the ESLint configuration in `.eslintrc.js`
- Run `npm run lint` before committing
- Use `scoped` styles in single-file components
- Use `@/` path alias for imports (maps to `src/`)

---

## 26. Version Information

| Package | Version |
|---|---|
| vue-element-admin | 4.4.0 |
| Vue.js | 2.6.10 |
| Element UI | 2.13.2 |
| Vuex | 3.1.0 |
| Vue Router | 3.0.2 |
| Axios | 0.18.1 |
| ECharts | 4.2.1 |
| MockJS | 1.0.1-beta3 |
| Vue CLI | 4.4.4 |
| Webpack | 4.x (via Vue CLI) |

---

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