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 only —
preinstallscript 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 --noEmit — use 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 bothvite.config.ts(resolve.alias) andtsconfig.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
- Route guard (
src/plugins/permission.ts) runsbeforeEach - Checks
localStorage.getItem("accessToken")for token - White-listed paths (no auth required):
/software,/helper,/autoLogin,/resetPassword - If token exists but no roles loaded → calls
userStore.getUserInfo()→permissionStore.generateRoutes()→router.addRoute() - Token invalid →
A0230code from backend →resetToken()+ redirect to/software - Button-level auth via
hasAuth()— checkspermsarray;ROOTrole bypasses all button checks - Directives:
v-permissioninsrc/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()fromsrc/utils/request.ts - The
request.tsaxios instance:baseURL=import.meta.env.VITE_APP_BASE_API(the proxy prefix)- Request interceptor: attaches
Authorizationheader fromlocalStorage.accessToken - Response interceptor: unwraps
{ code, data, msg }— returnsdataon 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,LoginResultinsrc/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 wrapuseXxxStore(piniaInstance)for use in non-component code (interceptors, guards) - Token stored in localStorage under key
"accessToken"(fromCacheEnum.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 metadataAppSettings— app configuration interfaceOptionType— select/dropdown datasource ({ value, label, children? })
Components
- Auto-registered:
unplugin-vue-componentsscanssrc/components/andsrc/**/components/ - DTS generation is OFF (
dts: falsein vite.config.ts) — types for auto-imported components are insrc/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 explicitimport { ref } from "vue"in new files- The existing
src/types/auto-imports.d.tsand.eslintrc-auto-import.jsonare checked-in snapshots — they are NOT auto-updated - When adding new auto-imported APIs (e.g., from @vueuse/core): enable
dtsandeslintrc.enabledtemporarily, 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: primary → var(--el-color-primary), primary_dark → var(--el-color-primary-light-5) — these reference Element Plus CSS variables, not hardcoded values.
SCSS
- Global SCSS variables injected via
vite.config.tscss.preprocessorOptions.scss.additionalData:This means every@use "@/styles/variables.scss" as *;.vueand.scssfile can use variables fromsrc/styles/variables.scsswithout explicit@use.
Build Details
- Minifier:
terser(not esbuild) — configured withdrop_console: trueanddrop_debugger: truefor 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-stagedruns ESLint + Prettier + Stylelint on staged files automatically
ESLint Specifics
- Config:
.eslintrc.cjs(legacy format, NOT flat config) - Parser:
vue-eslint-parser→@typescript-eslint/parserfor<script>blocks - Relaxed rules (common gotcha for agents that try to "fix" these):
@typescript-eslint/no-explicit-any: OFF —anyis allowed@typescript-eslint/no-unused-vars: OFF — unused vars allowedvue/multi-word-component-names: OFF — single-word component names OKvue/html-self-closing: enforced (void: always, normal: never, component: always)
- Global:
OptionTypeis 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
-
Adding explicit imports for auto-imported APIs —
ref,reactive,computed,watch,defineStore,useRouter,useRoute,ElMessage,ElNotification, etc. are injected at compile time. Do not writeimport { ref } from "vue". -
Importing Element Plus components manually —
unplugin-vue-componentshandles this.<el-button>just works; noimport { ElButton } from 'element-plus'. -
Using
import.meta.env.VITE_APP_API_URLas the axios baseURL — the baseURL isVITE_APP_BASE_API(the proxy prefix/dev-api), NOT the actual backend URL. The proxy rewrite happens in the Vite dev server. -
Writing unit tests — there is no test framework installed. Do not create
*.test.tsor*.spec.tsfiles or install testing dependencies unless explicitly asked. -
Hardcoding Element Plus color values — use
var(--el-color-primary)or UnoCSSprimaryshortcut, not hex values like#409EFF. -
Modifying
src/types/auto-imports.d.tsorcomponents.d.tsby hand — these are generated files. If the auto-import plugin is re-enabled, they will be overwritten. -
Using
git commitinstead ofpnpm commit— commitlint will reject non-conventional messages. Always usepnpm commit. -
Forgetting the
@alias — use@/for all internal imports, never relative../../paths for cross-module references. -
Changing the
tsconfig.jsonmodule resolution — it usesnode16module resolution withtype: "module"in package.json. This is intentional; don't change tobundlerornodewithout understanding the impact. -
Token in Pinia vs localStorage — the source of truth for auth is
localStorage.getItem("accessToken"). The PiniasessionTokenis just a reactive mirror; the route guard reads localStorage directly.