Skip to main content

elf-vue

Elf Express 團隊 Vue 3 + TypeScript 前端開發規範(目錄結構、SFC 寫法、Pinia、命名、環境變數、lint/format/typecheck、Vitest 單元測試)。 當任務涉及:新建或修改 `.vue` 元件、`<script setup lang="ts">`、props/emits/v-model 型別、Pinia store、composable、 `@/` 路徑別名、`apps/` 前端目錄、`VITE_*` 環境變數、ant-design-vue 元件註冊、eslint / prettier / vue-tsc 設定、 前端 `*.test.ts`(Vitest + @vue/test-utils)或覆蓋率門檻時觸發。

Source facts

Repository
elf-express/skillshare-source
Last source activity
September 27, 2026 at 01:26
Detected SKILL.md language
Chinese
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
3 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
elf-vue
description
Elf Express 團隊 Vue 3 + TypeScript 前端開發規範(目錄結構、SFC 寫法、Pinia、命名、環境變數、lint/format/typecheck、Vitest 單元測試)。 當任務涉及:新建或修改 `.vue` 元件、`<script setup lang="ts">`、props/emits/v-model 型別、Pinia store、composable、 `@/` 路徑別名、`apps/` 前端目錄、`VITE_*` 環境變數、ant-design-vue 元件註冊、eslint / prettier / vue-tsc 設定、 前端 `*.test.ts`(Vitest + @vue/test-utils)或覆蓋率門檻時觸發。
metadata
{"version":"1.0.0","owner":"Elf Express"}
# Elf Express Vue 3 + TypeScript 規範 > 事實來源:參考專案 `TypingTrainer/apps`(Vue 3.5、Vite 6、Pinia、vue-i18n、Vitest、ant-design-vue)。 > 團隊標準(Node 24.18、pnpm、UI 函式庫 ant-design-vue 4)覆蓋參考專案(參考專案用 npm)。 > 完整範本:[`references/templates.md`](references/templates.md);設定檔:[`references/config.md`](references/config.md)。 相關 skill: - `elf-stack` — 技術棧版本總表 - `elf-api-contract` / `elf-api-design` — API 形狀、錯誤格式、DTO 命名 - `elf-ui-pattern` — 元件 API、a11y、三態、design token - `elf-i18n` — 多語系、IME 輸入 - `elf-unit` — 單元測試通則;`elf-e2e` — Playwright;`elf-cicd-frontend` — CI 流程 --- ## 1. 何時使用 - 建立 / 修改任何 `.vue`、`src/**/*.ts` 前端程式 - 新增 Pinia store、composable、route、API 呼叫 - 設定或修改 `vite.config.ts`、`tsconfig.json`、`eslint.config.js`、`.prettierrc.json`、`.env.example` - 撰寫前端單元測試或調整覆蓋率 不適用:純後端(→ `elf-dotnet`)、E2E(→ `elf-e2e`)。 --- ## 2. 固定規則 ### 2.1 目錄結構(MUST) 前端一律放在 repo 根目錄的 `apps/`(pnpm workspace 成員),結構固定如下: ``` apps/ ├─ .env.example # 提交;列出所有 VITE_* 變數與說明 ├─ .env.local # 不提交(.gitignore: *.local / .env.*) ├─ env.d.ts # vite/client 參考、ImportMetaEnv、*.vue 宣告、define 常數 ├─ eslint.config.js # ESLint flat config ├─ index.html ├─ package.json # name: @<project>/web ├─ tsconfig.json ├─ vite.config.ts # 含 Vitest 的 test 區塊 └─ src/ ├─ main.ts # createApp + use(pinia/router/i18n),不放業務邏輯 ├─ App.vue # 殼層:layout、全域快捷鍵、初始載入 ├─ router/index.ts # 路由表;view 一律 lazy import ├─ views/ # 一個 route 一個 XxxView.vue;只做版面與文案 ├─ components/ # 可重用元件 PascalCase.vue ├─ composables/ # useXxx.ts ├─ stores/ # Pinia setup store,一檔一 store ├─ api/ # http.ts(axios 實例)、types.ts(DTO)、index.ts(api 物件)、mock/ ├─ i18n/ # index.ts + locales/*.json(見 elf-i18n) ├─ plugins/antd.ts # ant-design-vue 按需註冊清單(main.ts 與元件測試共用) ├─ lib/ # 純函式(無 Vue 依賴),最適合單元測試 ├─ data/ # 常數、共用型別 ├─ assets/ # 由程式 import 的圖片 / 字型 └─ styles/ # 全域 CSS 與 design token(見 elf-ui-pattern) ``` 1. **MUST** 維持分層:`views → stores → api`。View **MUST NOT** 直接 import `axios` 或 `@/api`;View 讀 store,store 呼叫 `api`。 2. **MUST NOT** 在 `views/` 放可重用元件;被兩個以上 view 使用即移到 `components/`。 3. **MUST** 將無 Vue 依賴的計算邏輯放 `lib/`,並附 `*.test.ts`。 4. 參考專案使用 `src/router.ts` 單檔;新專案 **MUST** 用 `src/router/index.ts`(import 路徑同為 `@/router`)。 ### 2.2 SFC 寫法(MUST) 5. **MUST** 使用 `<script setup lang="ts">`。**MUST NOT** 使用 Options API、`defineComponent({ ... })`、`this`。 6. **MUST** 區塊順序:`<script setup lang="ts">` → `<template>` → `<style scoped>`。 7. **MUST** `<script setup>` 內部順序: 1. `import`(vue → 第三方 → `@/` → `import type`) 2. `defineProps` / `defineEmits` / `defineModel` 3. composable / store / `useI18n()` / `useRoute()` 4. `ref` / `reactive` 狀態 5. `computed` 6. 函式 7. `watch` 8. 生命週期(`onMounted` / `onBeforeUnmount`) 8. **MUST** props 用型別宣告:`withDefaults(defineProps<{ ... }>(), { ... })`;**MUST NOT** 用 runtime 物件宣告(`defineProps({ foo: String })`)。 9. **MUST** emits 用具名 tuple 語法:`defineEmits<{ select: [id: string]; close: [] }>()`。 10. **MUST** 雙向綁定用 `defineModel<T>()`,新程式 **MUST NOT** 手寫 `modelValue` + `update:modelValue`。 11. **MUST** 樣式用 `<style scoped>`;覆寫 ant-design-vue 內部 class(`.ant-*`)用 `:deep()`,且優先改 `theme.token`(見 `elf-ui-pattern` 規則 29)。**MUST NOT** 在元件寫未 scoped 的全域樣式(全域樣式只放 `src/styles/`)。 12. **MUST NOT** 在 template 寫多於一個運算子的邏輯;抽成 `computed`。 13. **MUST** 以 `const xxxRef = ref<HTMLElement | null>(null)` + `ref="xxxRef"` 取 DOM;存取前判斷 `null`。 14. **MUST** 在 `onMounted` 註冊的 `addEventListener` / `setInterval` / `ResizeObserver`,於 `onBeforeUnmount` 移除。 ### 2.3 Pinia(MUST) 15. **MUST** 用 setup 風格:`defineStore('<id>', () => { ...; return { ... } })`。**MUST NOT** 用 options 風格(`state/getters/actions`)。 16. **MUST** 命名:檔案 `stores/<id>.ts`,id 與檔名相同(小寫),匯出 `use<Id>Store`(例:`stores/library.ts` → `useLibraryStore`,id `'library'`)。 17. **MUST** 回傳所有 state(`ref`)、getter(`computed`)、action(function);未回傳的 state 在 devtools 不可見、測試無法設定。 18. **MUST** 非同步載入的 store 帶 `loading` / `loaded`(或 `status` union)旗標並以 `try/finally` 還原;已載入或載入中時直接 return。 19. **MUST NOT** 在 store 回傳已翻譯字串;回傳 i18n key(例:`statusKey`),由元件 `t()` 翻譯。 20. **MUST** 在元件中解構 store state 時用 `storeToRefs()`;直接 `const { x } = store` 會失去響應性。action 可直接解構。 ### 2.4 命名與匯入(MUST) | 對象 | 規則 | 範例 | | --- | --- | --- | | 元件檔 | PascalCase.vue | `ToggleSwitch.vue`、`AppSider.vue` | | 頁面檔 | PascalCase + `View` | `PracticeView.vue` | | composable | `use` + PascalCase,檔名同函式 | `useViewport.ts` → `export function useViewport()` | | store | 見 2.3 | `useSettingsStore` | | 型別 / interface | PascalCase;API 傳輸型別加 `Dto` 後綴 | `TextDto`、`Category` | | 常數 | UPPER_SNAKE_CASE | `TOKEN_KEY`、`SRS_DAYS` | | route name | 小寫 | `{ name: 'practice' }` | | 測試 | `<同名>.test.ts` 放在受測檔旁 | `analyze.ts` → `analyze.test.ts` | 21. **MUST** 跨目錄 import 使用 `@/`(對應 `apps/src`);同目錄可用 `./`。**MUST NOT** 使用 `../`。 22. **MUST** TS 模組使用具名匯出(`export function` / `export const`);`.vue` 由編譯器提供 default export。 23. **MUST NOT** 使用 `any`;未知型別用 `unknown` 再收窄。**MUST NOT** 使用 `var`。 24. 頁面元件名稱允許單字(`Practice`、`Result`),因此 ESLint 關閉 `vue/multi-word-component-names`。 ### 2.5 環境變數(MUST) 25. **MUST** 前端可讀的變數以 `VITE_` 開頭;透過 `import.meta.env.VITE_XXX` 讀取,**MUST** 在 `env.d.ts` 的 `ImportMetaEnv` 宣告型別。 26. **MUST** 提交 `apps/.env.example`(每個變數附註解說明用途與可選值)。**MUST NOT** 提交 `.env`、`.env.local`、`.env.*.local`。 27. **MUST NOT** 在 `VITE_*` 放任何秘密(API key、token、密碼)— 打包後會出現在 JS bundle。 28. **MUST** 讀取時給預設值並集中在 `api/http.ts` 或單一 config 模組(例:`import.meta.env.VITE_API_BASE_URL || '/api'`),**MUST NOT** 散落在元件中。 ### 2.6 API 呼叫(MUST) 29. **MUST** 所有 HTTP 經過 `src/api/http.ts` 的單一 axios 實例(baseURL、timeout、Bearer token、401 處理、錯誤正規化為 `ApiError`)。`http.ts`、`ApiError`、`normalizeError` 的**唯一範本**在 `elf-api-contract` references/client-templates.md §1,本 skill 不另給一份。 30. **MUST** DTO 型別放 `src/api/types.ts`,欄位與後端契約逐字一致(見 `elf-api-contract`);**MUST NOT** 在元件內宣告 API 回應型別。 31. **MUST** 以 `VITE_USE_MOCK` 切換 `api/mock/` fixture,且**預設開**:`USE_MOCK = import.meta.env.VITE_USE_MOCK !== 'false'`;`apps/.env.example` 寫 `VITE_USE_MOCK=true`。正式映像由 `web.Dockerfile` 的 `ARG VITE_USE_MOCK=false` 關閉(見 `elf-cicd-docker`)。**MUST NOT** 改成 `=== 'true'`。 ### 2.7 品質工具(MUST) 32. **MUST** 通過 `pnpm verify`(= `format:check` → `lint` → `typecheck` → `test:coverage` → `build`,與 CI web job 同序,見 `elf-cicd-frontend`)才可提交 / 開 PR。 33. **MUST** 型別檢查用 `vue-tsc --noEmit`;`build` 腳本 **MUST** 先跑 `vue-tsc --noEmit` 再 `vite build`。 34. **MUST** ESLint 用 flat config(`eslint.config.js`:`@eslint/js` + `typescript-eslint` + `eslint-plugin-vue` `flat/recommended`,`eslint-config-prettier` 放最後)。 35. **MUST** Prettier:`semi: false`、`singleQuote: true`、`printWidth: 110`、`trailingComma: 'all'`。排版交給 Prettier,ESLint 不管排版。 36. **MUST** `.editorconfig`:UTF-8、LF、2 空格、結尾換行。 37. **MUST NOT** 用 `// eslint-disable` 或 `// @ts-ignore` 繞過錯誤;確有必要時用 `// @ts-expect-error <原因>`,並在 PR 說明。 ### 2.8 測試(MUST) 38. **MUST** 測試檔為 `Xxx.test.ts`,放在受測檔旁(`ToggleSwitch.vue` → `ToggleSwitch.test.ts`)。**MUST NOT** 建立 `.test.vue`、`__tests__/` 或 `src/**/*.spec.ts`(`.spec.ts` 保留給 `e2e/`)。 39. **MUST** 使用 Vitest + `@vue/test-utils`,環境 `jsdom`,`globals: false`(從 `vitest` 明確 import `describe/it/expect/vi`)。 40. **MUST** store 測試在 `beforeEach` 執行 `setActivePinia(createPinia())`。 41. **MUST** 行覆蓋率(lines)≥ **55%**,由 `vite.config.ts` 的 `coverage.thresholds.lines: 55` 強制,未達標即失敗。計算範圍固定:`include: ['src/**/*.{ts,vue}']`,`exclude` 為測試檔、`.d.ts`、`src/main.ts`、`src/api/mock/**`(與 `elf-cicd-frontend` 相同)。 42. **MUST** 修 bug 時先寫會紅的測試;修好後暫時拿掉修正,確認測試會紅(參考專案曾兩次出現測試對已壞程式碼仍綠燈)。 43. jsdom 觀察不到 focus、IME 組字、瀏覽器事件順序 — 這類行為 **MUST** 寫在 `e2e/`(見 `elf-e2e`)。 ### 2.9 工具鏈(MUST) 44. **MUST** Node `>=24.18`(`package.json` `engines`;repo 根目錄 `.nvmrc` = `24.18`),engine-strict **只**寫在 `pnpm-workspace.yaml`(`engineStrict: true`);`.npmrc` 只放 registry / auth(見 references/config.md、`elf-stack`)。 45. **MUST** 使用 pnpm,`packageManager` 寫 `pnpm@<PNPM_VERSION>` 完整版號(版號見 `elf-stack`)。**MUST NOT** 使用 corepack。**MUST NOT** 提交 `package-lock.json` / `yarn.lock`;只提交 `pnpm-lock.yaml`。 46. **MUST** CI 安裝用 `pnpm install --frozen-lockfile`(CI 細節見 `elf-cicd-frontend`)。前端依賴 **MUST** 加在 `apps`(`pnpm --filter ./apps add`),不加在 repo 根目錄。 ### 2.10 UI 函式庫(MUST,已決議) 47. **MUST** UI 函式庫使用 **ant-design-vue 4**(版本見 `elf-stack`),在 `src/plugins/antd.ts` 按需註冊(範本見 references/templates.md §1)。**MUST NOT** 自行引入第二套 UI 函式庫或改用其他函式庫。對話框、確認、載入、空狀態、錯誤結果等一律用對應 `a-*` 元件(對照表見 `elf-ui-pattern` 2.8)。 48. SoybeanAdmin / NaiveUI 系既有 repo(例 `platform.client`:`@sa/*` 套件、kebab-case 檔名、`src/service/api/`、`declare namespace Api`)為**歷史例外**:在該 repo 內工作時**以該 repo 的 CLAUDE.md 為準**,不套用本檔的目錄結構與 UI 函式庫規則,也**MUST NOT** 把 ant-design-vue 帶進去。 --- ## 3. 標準範本 完整可複製版本在 [`references/templates.md`](references/templates.md)(main.ts + `plugins/antd.ts`、元件、View、router、store、composable、api 型別與 index、元件測試、store 測試;`http.ts` 見 `elf-api-contract`) 與 [`references/config.md`](references/config.md)(package.json、pnpm-workspace.yaml、vite.config.ts、tsconfig、eslint、prettier、env)。 ### 3.1 元件骨架 ```vue <script setup lang="ts"> import { computed } from 'vue' import { useI18n } from 'vue-i18n' import type { TextItem } from '@/data/types' const props = withDefaults(defineProps<{ item: TextItem; compact?: boolean }>(), { compact: false }) const emit = defineEmits<{ select: [title: string] }>() const { t } = useI18n() const label = computed(() => (props.compact ? props.item.title.slice(0, 12) : props.item.title)) function onClick() { emit('select', props.item.title) } </script> <template> <button type="button" class="text-card" :class="{ 'is-compact': props.compact }" @click="onClick"> <span class="title">{{ label }}</span> <span class="meta">{{ t('library.chars', { count: props.item.chars }) }}</span> </button> </template> <style scoped> .text-card { display: flex; gap: 8px; padding: 8px 12px; border: 1px solid var(--line); background: var(--surface); color: var(--text); } </style> ``` ### 3.2 Setup store 骨架 ```ts import { computed, ref } from 'vue' import { defineStore } from 'pinia' import { api } from '@/api'
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub