247 lines
13 KiB
Markdown
247 lines
13 KiB
Markdown
# 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** — `preinstall` 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 --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 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`:
|
|
|
|
```js
|
|
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**: `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.ts` `css.preprocessorOptions.scss.additionalData`:
|
|
```scss
|
|
@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`: **OFF** — `any` 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 APIs** — `ref`, `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 manually** — `unplugin-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.
|