Files

13 KiB

AGENTS.md — 唱作网 (ichangzuo)

This file tells AI agents what they would likely get wrong without help. Every line answers: "Would an agent miss this on first contact?" If the answer is no, it doesn't belong here.

Project Identity

  • Name: ichangzuo (唱作网) — a fork of vue3-element-admin, heavily customized
  • Type: Vue 3 admin panel / SPA (single-page app, no SSR/SSG)
  • Package manager: pnpm onlypreinstall script enforces this (npx only-allow pnpm); npm/yarn will fail

Tech Stack (versions from package.json, not README badges)

Layer Package Version
Framework vue ^3.5.13
Build vite ^6.0.2
Language typescript ^5.7.2
UI element-plus ^2.9.0
State pinia ^2.2.8
Routing vue-router ^4.5.0
i18n vue-i18n 10.0.5
CSS UnoCSS + SCSS ^0.65.0 / sass ^1.82.0
HTTP axios ^1.7.8
Node >=18 (20.6.0 broken)

Commands

Purpose Command Notes
Dev server pnpm dev Reads VITE_APP_PORT from .env.development (default 3000)
Type check pnpm type-check vue-tsc --noEmituse this before committing
Build (prod) pnpm build Runs vue-tsc --noEmit & vite build in parallel (Windows & = background)
Build only pnpm build-only vite build without type check
Lint JS/TS/Vue pnpm lint:eslint eslint --fix
Format pnpm lint:prettier prettier --write
Lint CSS/Vue pnpm lint:stylelint stylelint --fix
Commit pnpm commit Interactive cz-git; do NOT git commit directly

No test runner is configured. There is no test script, no test framework, and no test files in this repo.

Path Alias

  • @src/ — configured in both vite.config.ts (resolve.alias) and tsconfig.json (paths).
  • Import as @/api/auth, @/store/modules/user, etc.

Environment Variables

Defined in src/types/env.d.ts and .env.* files:

Variable Dev Default Prod Purpose
VITE_APP_PORT 3000 Dev server port
VITE_APP_BASE_API /dev-api /prod-api Proxy prefix — Vite rewrites requests under this prefix to VITE_APP_API_URL
VITE_APP_API_URL http://localhost:8989 Backend API target
VITE_MOCK_DEV_SERVER false Enable mock server (currently disabled in code)

Agent trap: The proxy prefix is NOT a real URL path on the backend. The Vite dev server strips it before forwarding. So a request to /dev-api/api/v1/auth/login is proxied to http://localhost:8989/api/v1/auth/login.

Architecture & Wiring

App Bootstrap Order (src/plugins/index.ts)

main.ts → createApp(App) → app.use(setupPlugins) → mount("#app")
  setupPlugins.install(app):
    1. setupDirective(app)   — custom directives (e.g., v-permission)
    2. setupRouter(app)      — vue-router
    3. setupStore(app)       — Pinia
    4. setupI18n(app)        — vue-i18n
    5. setupElIcons(app)     — Element Plus icons
    6. setupPermission()     — route guards (NProgress + auth check)

Route System

  • Static routes defined in src/router/index.ts (constantRoutes) — includes /software, /helper, /dashboard, /autoLogin, /resetPassword, error pages (401, 404)
  • Dynamic routes injected at runtime by permissionStore.generateRoutes() after login — fetched from backend API
  • Default redirect: //software
  • History mode: createWebHistory() (HTML5 history, NOT hash mode)

Permission / Auth Flow

  1. Route guard (src/plugins/permission.ts) runs beforeEach
  2. Checks localStorage.getItem("accessToken") for token
  3. White-listed paths (no auth required): /software, /helper, /autoLogin, /resetPassword
  4. If token exists but no roles loaded → calls userStore.getUserInfo()permissionStore.generateRoutes()router.addRoute()
  5. Token invalid → A0230 code from backend → resetToken() + redirect to /software
  6. Button-level auth via hasAuth() — checks perms array; ROOT role bypasses all button checks
  7. Directives: v-permission in src/directive/ for template-level auth

API Layer Pattern

  • All API modules live in src/api/ — one file per domain (auth, user, role, menu, dict, etc.)
  • Each exports a static class with methods that call request() from src/utils/request.ts
  • The request.ts axios instance:
    • baseURL = import.meta.env.VITE_APP_BASE_API (the proxy prefix)
    • Request interceptor: attaches Authorization header from localStorage.accessToken
    • Response interceptor: unwraps { code, data, msg } — returns data on success (00000), rejects otherwise
    • Token invalid (A0230): auto-redirect to login
  • Request/response types exported alongside the class in the same file (e.g., AuthAPI, LoginData, LoginResult in src/api/auth.ts)

State Management (Pinia)

  • Store modules in src/store/modules/: app, permission, settings, tagsView, user
  • Composition API style — uses defineStore("name", () => { ... })
  • Outside-component usage: useUserStoreHook(), useAppStoreHook() etc. — these wrap useXxxStore(piniaInstance) for use in non-component code (interceptors, guards)
  • Token stored in localStorage under key "accessToken" (from CacheEnum.TOKEN_KEY), NOT in Pinia state directly

Global Types (src/types/global.d.ts)

These are ambient global types (no import needed):

  • ResponseData<T>{ code, data, msg }
  • PageQuery{ pageNum, pageSize }
  • PageResult<T>{ list, total }
  • TagView — tab/nav view metadata
  • AppSettings — app configuration interface
  • OptionType — select/dropdown datasource ({ value, label, children? })

Components

  • Auto-registered: unplugin-vue-components scans src/components/ and src/**/components/
  • DTS generation is OFF (dts: false in vite.config.ts) — types for auto-imported components are in src/types/components.d.ts (checked in, not auto-generated)
  • Element Plus components: auto-imported via ElementPlusResolver — no manual imports needed
  • Icons: use <i-ep-xxx /> (e.g., <i-ep-edit />) via unplugin-icons with @iconify-json/ep
  • SVG icons: place SVGs in src/assets/icons/, use <SvgIcon icon-class="name" />

Auto-Import: Critical Trap

unplugin-auto-import is configured in vite.config.ts:

AutoImport({
  imports: ["vue", "@vueuse/core", "pinia", "vue-router", "vue-i18n"],
  resolvers: [ElementPlusResolver(), IconsResolver({})],
  eslintrc: { enabled: false },  // NOT generating eslint rules
  dts: false,                     // NOT generating .d.ts
})

This means:

  • ref, reactive, computed, watch, onMounted, defineStore, useRouter, useRoute, createI18n, etc. are auto-imported — do NOT add explicit import { ref } from "vue" in new files
  • The existing src/types/auto-imports.d.ts and .eslintrc-auto-import.json are checked-in snapshots — they are NOT auto-updated
  • When adding new auto-imported APIs (e.g., from @vueuse/core): enable dts and eslintrc.enabled temporarily, run dev server once, then disable both again and commit the updated files
  • ElMessage, ElNotification, ElMessageBox etc. are auto-imported — no need to import them manually

UnoCSS Shortcuts

Defined in uno.config.ts — use these instead of writing full utility classes:

Shortcut Expands To
flex-center flex justify-center items-center
flex-x-center flex justify-center
flex-y-center flex items-center
wh-full w-full h-full
flex-x-between flex items-center justify-between
flex-x-end flex items-center justify-end
absolute-lt absolute left-0 top-0
absolute-rt absolute right-0 top-0
fixed-lt fixed left-0 top-0

Theme colors: primaryvar(--el-color-primary), primary_darkvar(--el-color-primary-light-5) — these reference Element Plus CSS variables, not hardcoded values.

SCSS

  • Global SCSS variables injected via vite.config.ts css.preprocessorOptions.scss.additionalData:
    @use "@/styles/variables.scss" as *;
    
    This means every .vue and .scss file can use variables from src/styles/variables.scss without explicit @use.

Build Details

  • Minifier: terser (not esbuild) — configured with drop_console: true and drop_debugger: true for production
  • Manual chunks: vue (vue+router+pinia), vue-i18n, element-plus — split into separate bundles
  • Asset naming: js/[name].[hash].js, img/[name].[hash].[ext], fonts/[name].[hash].[ext], media/[name].[hash].[ext]
  • __APP_INFO__: build-time global constant (name, version, engines, dependencies, buildTimestamp) — do NOT try to modify at runtime

Git Commit Convention

Enforced by husky + commitlint + lint-staged:

  • Always use pnpm commit — launches cz-git interactive prompt
  • Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, revert, chore, wip
  • Subject case: not enforced (rule level 0)
  • Pre-commit: lint-staged runs ESLint + Prettier + Stylelint on staged files automatically

ESLint Specifics

  • Config: .eslintrc.cjs (legacy format, NOT flat config)
  • Parser: vue-eslint-parser@typescript-eslint/parser for <script> blocks
  • Relaxed rules (common gotcha for agents that try to "fix" these):
    • @typescript-eslint/no-explicit-any: OFFany is allowed
    • @typescript-eslint/no-unused-vars: OFF — unused vars allowed
    • vue/multi-word-component-names: OFF — single-word component names OK
    • vue/html-self-closing: enforced (void: always, normal: never, component: always)
  • Global: OptionType is declared as readonly global

Directory Map

src/
├── api/            # API modules (one class per file + request/response types)
├── assets/icons/   # SVG icons (loaded by vite-plugin-svg-icons)
├── components/     # Shared components (CURD, SvgIcon, Pagination, Upload, WangEditor, etc.)
├── directive/      # Custom directives (v-permission)
├── enums/          # Const enums (ResultEnum, CacheEnum, LayoutEnum, ThemeEnum, etc.)
├── lang/           # i18n (vue-i18n, zh-CN + en)
├── layout/         # App shell layout (sidebar, header, tags-view)
├── plugins/        # App plugin registration (icons, permission guard)
├── router/         # Vue Router (static + dynamic routes)
├── store/modules/  # Pinia stores (app, permission, settings, tagsView, user)
├── styles/         # Global SCSS (variables, reset, login styles)
├── types/          # Global .d.ts (env, auto-imports, components, router types)
├── utils/          # Utilities (request.ts = axios wrapper, i18n, format, nprogress)
└── views/          # Page components
    ├── software/   # Main product page (default landing page)
    ├── helper/      # Help/assistant page
    ├── dashboard/   # Account info
    ├── system/      # Admin: users, roles, menus, dicts, etc.
    ├── account/     # User account management
    └── ...

Things Agents Get Wrong

  1. Adding explicit imports for auto-imported APIsref, reactive, computed, watch, defineStore, useRouter, useRoute, ElMessage, ElNotification, etc. are injected at compile time. Do not write import { ref } from "vue".

  2. Importing Element Plus components manuallyunplugin-vue-components handles this. <el-button> just works; no import { ElButton } from 'element-plus'.

  3. Using import.meta.env.VITE_APP_API_URL as the axios baseURL — the baseURL is VITE_APP_BASE_API (the proxy prefix /dev-api), NOT the actual backend URL. The proxy rewrite happens in the Vite dev server.

  4. Writing unit tests — there is no test framework installed. Do not create *.test.ts or *.spec.ts files or install testing dependencies unless explicitly asked.

  5. Hardcoding Element Plus color values — use var(--el-color-primary) or UnoCSS primary shortcut, not hex values like #409EFF.

  6. Modifying src/types/auto-imports.d.ts or components.d.ts by hand — these are generated files. If the auto-import plugin is re-enabled, they will be overwritten.

  7. Using git commit instead of pnpm commit — commitlint will reject non-conventional messages. Always use pnpm commit.

  8. Forgetting the @ alias — use @/ for all internal imports, never relative ../../ paths for cross-module references.

  9. Changing the tsconfig.json module resolution — it uses node16 module resolution with type: "module" in package.json. This is intentional; don't change to bundler or node without understanding the impact.

  10. Token in Pinia vs localStorage — the source of truth for auth is localStorage.getItem("accessToken"). The Pinia sessionToken is just a reactive mirror; the route guard reads localStorage directly.