create-dwy 0.14.7 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/bin/index.js +8 -2
- package/package.json +1 -1
- package/src/skills-install.js +154 -0
- package/src/sync-all.js +7 -0
- package/templates/ai-tools/CLAUDE.md +28 -1
- package/templates/ai-tools/hooks/Git/pre-git-commit-sensitive-check.sh +10 -2
- package/templates/ai-tools/rules/Android/dwy-android-pitfalls.md +52 -0
- package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-strict.md +221 -0
- package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +193 -0
- package/templates/ai-tools/rules/Python/dwy-python-backend.md +16 -63
- package/templates/ai-tools/rules/Python/dwy-python-base-lib.md +89 -0
- package/templates/ai-tools/rules/Python/dwy-python-pitfalls.md +49 -0
- package/templates/ai-tools/rules/Vue/dwy-vue-base-lib.md +127 -0
- package/templates/ai-tools/rules/Vue/dwy-vue-core.md +22 -86
- package/templates/ai-tools/rules/Vue/dwy-vue-pitfalls.md +66 -0
- package/templates/ai-tools/rules/iOS/dwy-ios-pitfalls.md +159 -0
- package/templates/ai-tools/rules/iOS/dwy-swift-core.md +35 -0
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-dependency-freshness.md +59 -0
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-development.md +47 -2
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-acceptance/SKILL.md +24 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-architecture/SKILL.md +70 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-commercial/SKILL.md +58 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-competitor/SKILL.md +23 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-explore/SKILL.md +83 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-mvp/SKILL.md +24 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-poc/SKILL.md +24 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prd/SKILL.md +23 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/SKILL.md +73 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/references/state-and-contract.md +155 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prototype/SKILL.md +42 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-release/SKILL.md +27 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-design/SKILL.md +30 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-dev/SKILL.md +54 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-launch/SKILL.md +37 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-prd/SKILL.md +31 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-ship/SKILL.md +30 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tasks/SKILL.md +32 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tdd-dev/SKILL.md +34 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-validate/SKILL.md +29 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-version/SKILL.md +24 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish/SKILL.md +365 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish/references/electron-release-workflow-template.yml +98 -0
- package/templates/ai-tools/skills//{350/277/220/347/273/264/345/217/221/345/270/203/dwy-cibuildwheel → 345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish}/references/release-workflow-template.yml +3 -3
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +31 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +31 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/backend-deploy.md +23 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +37 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/frontend-deploy.md +23 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/generic-release.md +27 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +25 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +32 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/monitor-notify.md +36 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/publish-config.md +65 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/version-sync.md +30 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/scripts/release_config.sh +115 -0
- package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/SKILL.md +31 -21
- package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/references/python-commercial-guide.md +1 -1
- package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/references/sensitive-patterns.md +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/scripts/init_config.sh +91 -0
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-whiteboard-prototype/SKILL.md +70 -0
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-whiteboard-prototype/assets/template.html +353 -0
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/init_config.sh +0 -97
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-0to1/SKILL.md +0 -113
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-0to1/templates/STATE.md +0 -56
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-dev-qa/SKILL.md +0 -117
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-market-analysis/SKILL.md +0 -92
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-coarse/SKILL.md +0 -72
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-coarse/templates/prd-coarse.md +0 -61
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-detailed/SKILL.md +0 -76
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-detailed/templates/prd-detailed.md +0 -119
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prototype/SKILL.md +0 -104
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-publish/SKILL.md +0 -50
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-publish/references/application-release.md +0 -106
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-publish/references/sdk-release.md +0 -93
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-spike/SKILL.md +0 -81
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-dev/SKILL.md +0 -212
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-dev/templates/impl-plan.md +0 -115
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tech-feasibility/SKILL.md +0 -105
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-version-plan/SKILL.md +0 -64
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-version-plan/templates/version-plan.md +0 -72
- package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-cibuildwheel/SKILL.md +0 -356
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/assets/files-whitelist-snippet.json" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/assets/js-commercial.npmignore" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/assets/python-commercial-MANIFEST.in" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/assets/python-commercial.gitignore" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/references/js-commercial-guide.md" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/references/publish-blacklist.md" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/scripts/check_comments.py" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/scripts/check_js_publish.sh" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/scripts/check_python_publish.sh" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/scripts/detect_sdk.sh" +0 -0
- /package/templates/ai-tools/skills//345/{274/200/345/217/221/346/265/201/347/250/213 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-semver/SKILL.md" +0 -0
- /package/templates/ai-tools/skills//345/{274/200/345/217/221/346/265/201/347/250/213 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-semver/references/semver-spec.md" +0 -0
- /package/templates/ai-tools/skills//345/{274/200/345/217/221/346/265/201/347/250/213 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-semver/scripts/bump.py" +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Vue 3 + TS + Vite + Tailwind
|
|
2
|
+
description: Vue 3 + TS + Vite + Tailwind 应用层通用规范(业务聚合结构、组件 / TS / 路由 / API / 模板 / 样式 / 错误处理 / 注释)
|
|
3
3
|
paths:
|
|
4
4
|
- "**/*.vue"
|
|
5
5
|
- "**/*.ts"
|
|
@@ -8,50 +8,7 @@ paths:
|
|
|
8
8
|
|
|
9
9
|
# Vue 3 应用层开发规范
|
|
10
10
|
|
|
11
|
-
适用于基于 Vue 3 + TypeScript + Vite + Tailwind CSS
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## 零、优先使用 eui / ekit 基础库(强制)
|
|
16
|
-
|
|
17
|
-
`@dwydev/eui` 与 `@dwydev/ekit` 是团队内部 npm 基础库,分别覆盖 UI 组件层与通用工具层。**凡是 eui / ekit 已提供的能力,禁止自行实现,也禁止引入其他库重复造轮子。**
|
|
18
|
-
|
|
19
|
-
### 必须复用的能力域
|
|
20
|
-
|
|
21
|
-
| 能力域 | 归属 |
|
|
22
|
-
|--------|------|
|
|
23
|
-
| 通用 UI 组件(Button / Input / Select / Dialog / Drawer / Form / Table 等) | eui |
|
|
24
|
-
| 主题 / 设计 tokens / 暗色模式 | eui |
|
|
25
|
-
| 表单校验(vee-validate + zod 集成) | eui + ekit |
|
|
26
|
-
| 中后台 / 落地页设计规范 | eui(references) |
|
|
27
|
-
| HTTP 客户端(拦截器 / token / 刷新 / 解包) | ekit |
|
|
28
|
-
| 本地存储 / Cookie | ekit(storage / cookie) |
|
|
29
|
-
| 日期 / 时间格式化 | ekit(date) |
|
|
30
|
-
| 数据校验 schema | ekit(validators) |
|
|
31
|
-
| PII 数据脱敏 | ekit(masking) |
|
|
32
|
-
| 剪贴板 / 文件下载 / qs 序列化 | ekit |
|
|
33
|
-
| VueUse 再导出(debounce / throttle / clickOutside / mediaQuery 等) | ekit(hooks) |
|
|
34
|
-
|
|
35
|
-
### 查阅方式(关键)
|
|
36
|
-
|
|
37
|
-
**在编写任何 Vue 代码前,以及调用任何 eui / ekit 提供的 API 之前,必须先用 `Skill` 工具加载对应 skill 查阅当前最新接口。** 本文件只规定约束与边界,**不固化**组件 props、函数签名 —— 这些会随版本演进,固化在 rule 里会与实际包不同步。
|
|
38
|
-
|
|
39
|
-
```
|
|
40
|
-
Skill 工具调用:
|
|
41
|
-
- skill="dwy-eui" 组件库 API 与设计规范
|
|
42
|
-
- skill="dwy-ekit" 工具库 API 与契约
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### 强制规则
|
|
46
|
-
|
|
47
|
-
- **禁止**业务代码出现 `axios` / `fetch` / `new XMLHttpRequest`,HTTP 走 ekit `createRequest`
|
|
48
|
-
- **禁止**直接 `localStorage.getItem` / `setItem` / `removeItem`,走 ekit storage / `useStorage`
|
|
49
|
-
- **禁止**直接 `document.cookie` 或单独引入 `js-cookie`,走 ekit cookie
|
|
50
|
-
- **禁止**业务代码 `new Date()` / `toLocaleString` / 直接 `import dayjs`,走 ekit date
|
|
51
|
-
- **禁止**手写 `navigator.clipboard` / `document.addEventListener('click', ...)` 监听,走 ekit copy / hooks
|
|
52
|
-
- **禁止**自定义已有 UI 组件(Button / Input / Select / Dialog / Form / Table 等),走 eui
|
|
53
|
-
- **禁止**自定义已有日期 / 时间 / 验证 / 脱敏 / 下载 工具函数,走 ekit
|
|
54
|
-
- 涉及任一能力域,**先查 skill 拿到当前 API,再写代码**
|
|
11
|
+
适用于基于 Vue 3 + TypeScript + Vite + Tailwind CSS 的前端项目。团队内部 eui / ekit 基础库强制复用见 `dwy-vue-base-lib` rule;状态管理见 `dwy-vue-pinia` rule;测试见 `dwy-vue-testing` rule。
|
|
55
12
|
|
|
56
13
|
---
|
|
57
14
|
|
|
@@ -72,9 +29,9 @@ Skill 工具调用:
|
|
|
72
29
|
|
|
73
30
|
```
|
|
74
31
|
src/
|
|
75
|
-
├──
|
|
32
|
+
├── features/ # 业务功能根目录
|
|
76
33
|
│ ├── users/ # 用户功能
|
|
77
|
-
│ │ ├── api.ts # 该业务的 API
|
|
34
|
+
│ │ ├── api.ts # 该业务的 API(用全局 HTTP 客户端)
|
|
78
35
|
│ │ ├── store.ts # 该业务的 Pinia store
|
|
79
36
|
│ │ ├── types.ts # 该业务的类型
|
|
80
37
|
│ │ ├── views/ # 该业务的页面组件
|
|
@@ -92,7 +49,7 @@ src/
|
|
|
92
49
|
│ ├── composables/
|
|
93
50
|
│ └── constants/
|
|
94
51
|
├── core/ # 全局基础设施:HTTP client / 路由根 / 主题 / 启动
|
|
95
|
-
│ ├── http.ts #
|
|
52
|
+
│ ├── http.ts # 全局唯一 HTTP 客户端实例
|
|
96
53
|
│ ├── router.ts
|
|
97
54
|
│ └── main.ts
|
|
98
55
|
├── App.vue
|
|
@@ -135,12 +92,11 @@ export default {
|
|
|
135
92
|
```vue
|
|
136
93
|
<script setup lang="ts">
|
|
137
94
|
// 1. 类型导入
|
|
138
|
-
import type { UserInfo } from '@/
|
|
95
|
+
import type { UserInfo } from '@/features/users/types'
|
|
139
96
|
|
|
140
97
|
// 2. 组件 / 工具导入
|
|
141
98
|
import { ref, computed, onMounted } from 'vue'
|
|
142
99
|
import { useRouter } from 'vue-router'
|
|
143
|
-
import { EButton, EInput } from '@dwydev/eui'
|
|
144
100
|
|
|
145
101
|
// 3. Props / Emits
|
|
146
102
|
const props = defineProps<{ userId: number; title?: string }>()
|
|
@@ -250,7 +206,7 @@ const router = createRouter({
|
|
|
250
206
|
{
|
|
251
207
|
path: '/dashboard',
|
|
252
208
|
name: 'Dashboard',
|
|
253
|
-
component: () => import('@/
|
|
209
|
+
component: () => import('@/features/dashboard/views/DashboardView.vue'),
|
|
254
210
|
meta: { requiresAuth: true },
|
|
255
211
|
},
|
|
256
212
|
],
|
|
@@ -297,7 +253,7 @@ export function useList<T>(fetchFn: () => Promise<T[]>) {
|
|
|
297
253
|
}
|
|
298
254
|
```
|
|
299
255
|
|
|
300
|
-
通用 hooks(debounce / throttle / clickOutside / mediaQuery
|
|
256
|
+
通用 hooks(debounce / throttle / clickOutside / mediaQuery 等)**禁止**自己造,优先复用成熟现成方案。
|
|
301
257
|
|
|
302
258
|
---
|
|
303
259
|
|
|
@@ -305,27 +261,17 @@ export function useList<T>(fetchFn: () => Promise<T[]>) {
|
|
|
305
261
|
|
|
306
262
|
### 强制规则
|
|
307
263
|
|
|
308
|
-
- HTTP
|
|
309
|
-
- **禁止**业务代码 `axios.create()` / `fetch()` / `new XMLHttpRequest()`
|
|
264
|
+
- HTTP 客户端全局唯一实例放 `core/http.ts`,业务代码从该实例导入,**禁止**每个模块各自创建客户端
|
|
310
265
|
- 每个业务模块的 API 函数集中在该业务目录的 `api.ts`,按业务域隔离
|
|
311
266
|
- 每个 API 函数**独立导出**,**禁止** `default export`
|
|
312
267
|
- 请求参数 / 响应**必须**类型标注
|
|
313
|
-
|
|
268
|
+
|
|
269
|
+
> 团队项目 HTTP 客户端的创建(createRequest)、响应解包契约、错误处理守卫见 `dwy-vue-base-lib` rule。
|
|
314
270
|
|
|
315
271
|
### 示例
|
|
316
272
|
|
|
317
273
|
```typescript
|
|
318
|
-
//
|
|
319
|
-
import { createRequest } from '@dwydev/ekit'
|
|
320
|
-
|
|
321
|
-
export const request = createRequest({
|
|
322
|
-
baseURL: import.meta.env.VITE_API_BASE_URL,
|
|
323
|
-
plugins: [
|
|
324
|
-
/* tokenPlugin / refreshTokenPlugin / unwrapPlugin —— 具体用法查 dwy-ekit skill */
|
|
325
|
-
],
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
// modules/users/api.ts —— 该业务的 API
|
|
274
|
+
// features/users/api.ts —— 该业务的 API
|
|
329
275
|
import { request } from '@/core/http'
|
|
330
276
|
import type { UserInfo, UserCreate } from './types'
|
|
331
277
|
|
|
@@ -340,8 +286,7 @@ export function createUser(data: UserCreate) {
|
|
|
340
286
|
|
|
341
287
|
### 错误处理契约
|
|
342
288
|
|
|
343
|
-
- 401 / 422 /
|
|
344
|
-
- 业务侧 catch 用 ekit 提供的 `isApiBusinessError` 类型守卫做分支(`NOT_FOUND` / `VALIDATION_ERROR` / `PERMISSION_DENIED` 等)
|
|
289
|
+
- 401 / 422 / 业务错误的统一处理放在 HTTP 客户端拦截层,不散落到每个调用点
|
|
345
290
|
- **禁止**每个 API 调用都包 `try/catch`,只在页面级或 store 级统一处理
|
|
346
291
|
|
|
347
292
|
---
|
|
@@ -360,7 +305,7 @@ export function createUser(data: UserCreate) {
|
|
|
360
305
|
<UserCard :user="user" @click="handleSelect" />
|
|
361
306
|
|
|
362
307
|
<!-- 双向绑定优先 v-model -->
|
|
363
|
-
<
|
|
308
|
+
<MyInput v-model="form.name" placeholder="请输入" />
|
|
364
309
|
```
|
|
365
310
|
|
|
366
311
|
**禁止**:
|
|
@@ -384,7 +329,7 @@ export function createUser(data: UserCreate) {
|
|
|
384
329
|
|
|
385
330
|
### 允许 scoped CSS 的场景
|
|
386
331
|
|
|
387
|
-
-
|
|
332
|
+
- 第三方组件库内部样式覆盖(`:deep(...)`)
|
|
388
333
|
- 复杂动画定义
|
|
389
334
|
- 无法用 Tailwind 表达的样式
|
|
390
335
|
|
|
@@ -448,7 +393,7 @@ async function handleSubmit() {
|
|
|
448
393
|
}
|
|
449
394
|
```
|
|
450
395
|
|
|
451
|
-
- 401 / 422 /
|
|
396
|
+
- 401 / 422 / 业务错误的拦截放在 HTTP 客户端拦截层统一处理,**禁止**每个 API 都包 try-catch
|
|
452
397
|
- **禁止**空 catch(`try { ... } catch {}` 吞异常)
|
|
453
398
|
- 异步操作**必须**有 loading 状态
|
|
454
399
|
|
|
@@ -482,16 +427,12 @@ VITE_API_BASE_URL=/api
|
|
|
482
427
|
import { ref, computed } from 'vue'
|
|
483
428
|
import { useRouter } from 'vue-router'
|
|
484
429
|
|
|
485
|
-
// 2. 第三方
|
|
430
|
+
// 2. 第三方 / 内部包
|
|
486
431
|
import { storeToRefs } from 'pinia'
|
|
487
432
|
|
|
488
|
-
// 3.
|
|
489
|
-
import {
|
|
490
|
-
import {
|
|
491
|
-
|
|
492
|
-
// 4. 项目内(@ 别名)
|
|
493
|
-
import { useAuthStore } from '@/modules/users/store'
|
|
494
|
-
import type { UserInfo } from '@/modules/users/types'
|
|
433
|
+
// 3. 项目内(@ 别名)
|
|
434
|
+
import { useAuthStore } from '@/features/users/store'
|
|
435
|
+
import type { UserInfo } from '@/features/users/types'
|
|
495
436
|
```
|
|
496
437
|
|
|
497
438
|
### 强制规则
|
|
@@ -555,19 +496,14 @@ AI 编写或审查 Vue 代码时,**必须**检查以下违规模式:
|
|
|
555
496
|
| **高** | 必须修正后才能继续,向用户说明 |
|
|
556
497
|
| **中** | 提示风险,建议修正 |
|
|
557
498
|
|
|
499
|
+
> 团队内部 eui / ekit 基础库相关的违规项(未查 skill、重复造轮子、组件直调 axios、直接 localStorage / new Date 等)见 `dwy-vue-base-lib` rule 第七节。
|
|
500
|
+
|
|
558
501
|
| 检查项 | 违规模式 | 严重程度 |
|
|
559
502
|
|--------|---------|---------|
|
|
560
|
-
| 未查 skill | 使用 eui / ekit API 前未先查 `dwy-eui` / `dwy-ekit` skill | 高 |
|
|
561
|
-
| ekit 重复造轮子 | 自造 HTTP / storage / cookie / date / 校验 / 脱敏 等 ekit 已提供能力 | 高 |
|
|
562
|
-
| eui 重复造轮子 | 自造已有 UI 组件(Button / Input / Dialog 等) | 高 |
|
|
563
|
-
| 组件直调 axios | 组件内 `axios.get` / `fetch` / `new XMLHttpRequest` | **致命 → STOP** |
|
|
564
|
-
| 直接 localStorage | `localStorage.getItem` / `setItem` 出现在业务代码 | 高 |
|
|
565
|
-
| 直接 new Date | `new Date()` / `toLocaleString` / 直接 `import dayjs` | 高 |
|
|
566
503
|
| 技术层散落 | 顶级 `views/` / `components/` / `stores/` / `api/` 把不同业务混放 | 高 |
|
|
567
504
|
| Options API | `<script>` 中 `export default { data, methods, ... }` | **致命 → STOP** |
|
|
568
505
|
| Options Store | `defineStore(id, { state, actions })` | **致命 → STOP** |
|
|
569
506
|
| any 无注释 | 使用 `any` 但未注释说明原因 | 高 |
|
|
570
|
-
| 旧 axios 例子 | 业务里出现 `axios.create()` / `interceptors.request.use(...)` | **致命 → STOP** |
|
|
571
507
|
| `v-if` + `v-for` 同元素 | 同一元素既 `v-if` 又 `v-for` | 高 |
|
|
572
508
|
| 模板复杂表达式 | 模板中 `.filter(...).length > 0` 等多语句 | 中 |
|
|
573
509
|
| 解构 store 丢响应性 | `const { x } = store`(应用 `storeToRefs`) | 高 |
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Vue 3 前端开发避坑清单(CSS 动画 / 响应式 / 路由 / 构建 / SSR 踩坑),按分类持续追加
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.vue"
|
|
5
|
+
- "**/*.ts"
|
|
6
|
+
- "**/*.css"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Vue 3 前端开发坑清单
|
|
10
|
+
|
|
11
|
+
> 动手前先对照本清单避开已知坑;踩到新坑随手按四栏格式(现象 / 根因 / 正确做法 / 适用)追加到对应分类,没有合适分类就新建一个。
|
|
12
|
+
>
|
|
13
|
+
> 通用开发规范见 `dwy-vue-core` rule,本文件只记**非显而易见的实战坑**。
|
|
14
|
+
|
|
15
|
+
## 分类索引
|
|
16
|
+
|
|
17
|
+
- CSS 动画 / 过渡
|
|
18
|
+
- 响应式 / 组件(ref / reactive / 生命周期 / props)
|
|
19
|
+
- 路由(vue-router / 守卫 / 懒加载)
|
|
20
|
+
- 构建 / SSR(Vite / vite-ssg / 预渲染)
|
|
21
|
+
- i18n / 内容
|
|
22
|
+
- 排查方法论
|
|
23
|
+
- 其他分类遇到再加
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## CSS 动画 / 过渡
|
|
28
|
+
|
|
29
|
+
### View Transitions 圆形过渡:收回方向(clip-path 缩到 0)收尾闪一帧
|
|
30
|
+
|
|
31
|
+
- **现象**:用 View Transitions API + `clip-path: circle()` 做主题切换的圆形润开过渡。散开方向(圆 0→大)正常;收回方向(圆 大→0,旧层缩回圆心露出底下新主题)动画走完后页面闪一下。
|
|
32
|
+
- **根因**:CSS keyframes 默认 `animation-fill-mode: none`——**动画结束瞬间元素恢复到原始属性值,再等浏览器销毁 VT 伪元素树**。收回方向动画挂在 `::view-transition-old(root)`(旧主题层),从 `circle(175vmax)` 缩到 `circle(0)`;动画一结束,`clip-path` 因 `fill-mode:none` **回弹到无裁切(全屏可见)**,旧主题瞬间全屏重现一帧 → 这就是「闪」。散开方向不闪是因为它的动画挂在 new 层,回弹后的终态(全屏新主题)恰好 == 真实 DOM 态,肉眼看不出。不对称的本质:正在动画的上层,其回弹终态是否等于真实页面态。
|
|
33
|
+
- **正确做法**(纯 CSS,不动 JS):① 给做圆形动画的层加 **`forwards`**,动画结束后保持终态(缩到 `circle(0)` 的层保持不可见),杜绝回弹全屏闪现——这是关键修复;② 给静止的对侧层补一段**恒等铺满动画 + `both`**(`from`/`to` 都是 `circle(175vmax)`),让它整个过渡期间也作为活动合成层,与对侧合成待遇对称,收尾两层合成组同步解散,消除高对比(纯黑↔纯白)下的交接重绘。
|
|
34
|
+
```css
|
|
35
|
+
@keyframes vt-hold-full {
|
|
36
|
+
from { clip-path: circle(175vmax at var(--vt-x, 50%) var(--vt-y, 50%)); }
|
|
37
|
+
to { clip-path: circle(175vmax at var(--vt-x, 50%) var(--vt-y, 50%)); }
|
|
38
|
+
}
|
|
39
|
+
html[data-vt-direction='conceal']::view-transition-new(root) {
|
|
40
|
+
z-index: 1; animation: vt-hold-full var(--vt-dur) var(--vt-ease) both;
|
|
41
|
+
}
|
|
42
|
+
html[data-vt-direction='conceal']::view-transition-old(root) {
|
|
43
|
+
z-index: 2; animation: vt-circle-conceal var(--vt-dur) var(--vt-ease) forwards;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
- **适用**:通用。推广教训:① 凡 keyframes 动画**结束态偏离元素静止态**(clip-path / transform / opacity 终值非默认),必须显式 `animation-fill-mode: forwards`(或 `both`),否则结束瞬间回弹一帧;不限 View Transitions。② 高对比色块下任何一帧亮度跳变都肉眼可见,动画收尾要让「正在动画的上层终态 == 真实 DOM 态」。③ 逐帧闪烁靠截图基本抓不到(只在单帧出现),定位靠根因推理 + CSSOM 确认规则生效,别依赖视觉快照验证。
|
|
47
|
+
|
|
48
|
+
## 响应式 / 组件
|
|
49
|
+
|
|
50
|
+
<!-- 待填:踩到坑按四栏格式追加 -->
|
|
51
|
+
|
|
52
|
+
## 路由
|
|
53
|
+
|
|
54
|
+
<!-- 待填 -->
|
|
55
|
+
|
|
56
|
+
## 构建 / SSR
|
|
57
|
+
|
|
58
|
+
<!-- 待填 -->
|
|
59
|
+
|
|
60
|
+
## i18n / 内容
|
|
61
|
+
|
|
62
|
+
<!-- 待填 -->
|
|
63
|
+
|
|
64
|
+
## 排查方法论
|
|
65
|
+
|
|
66
|
+
<!-- 待填 -->
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: iOS / Swift / SwiftUI 开发避坑清单(XcodeGen / 签名 / 真机部署 / SwiftUI 代码踩坑)
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.swift"
|
|
5
|
+
- "**/project.yml"
|
|
6
|
+
- "**/Package.swift"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# iOS 开发坑清单
|
|
10
|
+
|
|
11
|
+
> 动手前先对照本清单避开已知坑;踩到新坑随手按四栏格式(现象 / 根因 / 正确做法 / 适用)追加到对应分类,没有合适分类就新建一个。
|
|
12
|
+
|
|
13
|
+
## 分类索引
|
|
14
|
+
|
|
15
|
+
- 工程与构建(XcodeGen / SPM / Xcode 工程)
|
|
16
|
+
- 签名与真机部署
|
|
17
|
+
- 证书 / Keychain / mTLS
|
|
18
|
+
- 排查方法论
|
|
19
|
+
- Swift / SwiftUI 代码
|
|
20
|
+
- 其他分类遇到再加:StoreKit / 内购、Info.plist / 权限 / Capability、上架审核
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 工程与构建
|
|
25
|
+
|
|
26
|
+
### XcodeGen 工程要打开 .xcodeproj,不能直接打开项目文件夹
|
|
27
|
+
|
|
28
|
+
- **现象**:Xcode 里左侧根节点是文件夹名、右边只显示 `Package.swift`,点项目没有 General / Signing & Capabilities 标签页;选不到真机、装不上真机。
|
|
29
|
+
- **根因**:用 Xcode 直接打开了**项目文件夹**(或 `Package.swift`),Xcode 把它当 Swift Package(纯库)加载,没有 app target,自然没有签名页、也不能跑真机。
|
|
30
|
+
- **正确做法**:先 `xcodegen generate` 生成 `<项目>.xcodeproj`,再用 Xcode 打开这个 `.xcodeproj`(Finder 双击或 File → Open),**不要打开文件夹 / Package.swift**。打开对了,左侧根节点是蓝色 app 图标、底下有 TARGETS。
|
|
31
|
+
- **适用**:用 XcodeGen 且 `.xcodeproj` 不入库(被 .gitignore)的工程。SPM-only 的纯库项目本就没 app target,不适用。
|
|
32
|
+
|
|
33
|
+
### .xcodeproj 是生成物,Xcode 里手改设置会被 xcodegen 覆盖
|
|
34
|
+
|
|
35
|
+
- **现象**:在 Xcode 里改了签名 / Build Settings / bundle id,下次 `xcodegen generate` 之后改动全没了。
|
|
36
|
+
- **根因**:`.xcodeproj` 由 `project.yml` 生成、通常不入库;`xcodegen generate` 整体重建工程,覆盖一切手改。
|
|
37
|
+
- **正确做法**:需要持久化的工程设置(签名、bundle id、Build Settings、Info.plist 键)一律改 `project.yml` 再 `xcodegen generate`。Xcode 里手改只用于临时试验。
|
|
38
|
+
- **适用**:XcodeGen 工程。
|
|
39
|
+
|
|
40
|
+
### Package.swift 的 .macOS 平台声明,别为「只支持手机」删掉
|
|
41
|
+
|
|
42
|
+
- **现象**:想让 app 只支持 iPhone,看到 `Package.swift` 里 `platforms: [.iOS(.v17), .macOS(.v14)]` 就想删掉 `.macOS`。
|
|
43
|
+
- **根因**:这是**纯逻辑库**的平台声明,作用是让命令行 `swift test` 能在 mac(host)上跑核心逻辑测试,和 iOS app 支持哪些设备**无关**。app 的设备类型由 app target 的 `TARGETED_DEVICE_FAMILY` 控制。删掉 `.macOS` 会让 `swift test` 在 mac 上跑不起来。
|
|
44
|
+
- **正确做法**:保留 `.macOS(.v14)`。要 app 只支持手机,设 app target `TARGETED_DEVICE_FAMILY: "1"`(1=iPhone,2=iPad)。库的 platforms 是「最低支持平台」,不影响 app。
|
|
45
|
+
- **适用**:两层结构(SPM 纯逻辑库 + iOS app)、靠命令行 `swift test` 验证核心逻辑的工程。
|
|
46
|
+
|
|
47
|
+
### XcodeGen 加 String Catalog,要靠 CFBundleLocalizations 才生成多语言 .lproj
|
|
48
|
+
|
|
49
|
+
- **现象**:加了 `Localizable.xcstrings` 并翻译了多国语言,运行时却只显示开发语言(如中文),系统切语言 / app 内切语言都不生效;`xcodegen generate` 出来的 `.xcodeproj` 里 `knownRegions` 只有 `Base, en`。
|
|
50
|
+
- **根因**:XcodeGen 不会扫 `.xcstrings` 内容来推断本地化区域。pbxproj 的 `knownRegions` 决定 Xcode 编译哪些 `.lproj`;某语言不在其中,String Catalog 里该语言就不会编进 app bundle,自建的 `loc()` / 系统本地化都只能回退开发语言。纯 `.xcodeproj` 工程不暴露此坑,因为 `knownRegions` 是手维护的(参考项目 niannian 即如此)。
|
|
51
|
+
- **正确做法**:在 `project.yml` 的 app target `info.properties` 下显式声明 `CFBundleLocalizations` 数组,列出全部目标区域(如 `zh-Hans, zh-Hant, en, ja, ko, de, fr, es`)。XcodeGen 会据此把这些区域写进 pbxproj 的 `knownRegions`,Xcode 才会把 `.xcstrings` 编成各语言 `.lproj`。生成后用 `grep -A12 knownRegions TVRemote.xcodeproj/project.pbxproj` 核对区域齐全。
|
|
52
|
+
- **适用**:XcodeGen + String Catalog(`.xcstrings`)且 `.xcodeproj` 不入库的工程。
|
|
53
|
+
|
|
54
|
+
### Info.plist 文案本地化要用固定文件名 InfoPlist.xcstrings
|
|
55
|
+
|
|
56
|
+
- **现象**:`CFBundleDisplayName`(app 显示名)、各 `NS*UsageDescription`(权限弹窗文案)硬编码成一种语言,想随界面语言本地化,但放进 `Localizable.xcstrings` 不生效。
|
|
57
|
+
- **根因**:Info.plist 的面向用户字段不归 `Localizable.xcstrings` 管,Xcode 用一个专门的 String Catalog —— 文件名必须是 `InfoPlist.xcstrings`(大小写敏感),Xcode 自动识别它来本地化 Info.plist 各键。
|
|
58
|
+
- **正确做法**:在资源目录建 `InfoPlist.xcstrings`,key 用 Info.plist 的键名(`CFBundleDisplayName` / `NSLocalNetworkUsageDescription` / `NSPhotoLibraryAddUsageDescription`…),各 key 补齐目标语言。XcodeGen 工程把它放进 target 的 sources 目录即自动纳入 resources(`lastKnownFileType = text.json.xcstrings`);仍需配 `CFBundleLocalizations` 注册区域(见上一条)。`project.yml` 里 `info.properties` 的原值作为开发语言默认保留即可,xcstrings 提供各语言覆盖。
|
|
59
|
+
- **适用**:iOS app 本地化 Info.plist 文案,Xcode 15+ String Catalog。
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 签名与真机部署
|
|
64
|
+
|
|
65
|
+
### 命令行/CI 用的 CODE_SIGNING_ALLOWED=NO + 空 DEVELOPMENT_TEAM,装真机前要改
|
|
66
|
+
|
|
67
|
+
- **现象**:工程命令行编译、模拟器运行都正常,但装不上真机 / 没有可用签名。
|
|
68
|
+
- **根因**:`project.yml` 为命令行、CI、模拟器验证配了 `CODE_SIGNING_ALLOWED: "NO"` 和 `DEVELOPMENT_TEAM: ""`;真机安装必须代码签名,这套配置把签名直接关了。
|
|
69
|
+
- **正确做法**:真机部署在 `project.yml` 的 app target settings 配 `DEVELOPMENT_TEAM: "<teamID>"` + `CODE_SIGN_STYLE: Automatic`,删掉 `CODE_SIGNING_ALLOWED: "NO"`,再 `xcodegen generate`。命令行 / CI 仍可在调用时用 `xcodebuild ... CODE_SIGNING_ALLOWED=NO` 覆盖,不必把它写死进工程。teamID 在 Xcode → Settings → Accounts 看。
|
|
70
|
+
- **适用**:为命令行 / 模拟器验证而关签名的工程,首次要装真机时。
|
|
71
|
+
|
|
72
|
+
### personal team(免费账号)签名,bundle id 不能用 com.example 等保留前缀
|
|
73
|
+
|
|
74
|
+
- **现象**:用免费 personal team 自动签名失败 / 注册 App ID 被拒。
|
|
75
|
+
- **根因**:`com.example.*`(及 `com.apple.*` 等)是保留 / 示例域名,注册 App ID 会被拒。
|
|
76
|
+
- **正确做法**:换成自有反向域名,如 `com.<你的标识>.<app>`;同时改 `project.yml` 的 `bundleIdPrefix` 和 `PRODUCT_BUNDLE_IDENTIFIER`,再 `xcodegen generate`。
|
|
77
|
+
- **适用**:免费 personal team 真机调试。付费账号用已注册的 App ID。
|
|
78
|
+
|
|
79
|
+
### personal team 签的 app 7 天过期、首次需在设备「信任开发者」
|
|
80
|
+
|
|
81
|
+
- **现象**:免费账号装的 app 几天后打不开;或刚装上点击提示「不受信任的开发者」。
|
|
82
|
+
- **根因**:personal team 签名证书有效期仅 7 天;且首次安装的开发者证书需手动信任。
|
|
83
|
+
- **正确做法**:过期重新 `Cmd+R` 安装即可;首次安装后到 iPhone → 设置 → 通用 → VPN与设备管理 → 信任你的开发者证书。
|
|
84
|
+
- **适用**:免费 personal team。付费账号无 7 天限制。
|
|
85
|
+
|
|
86
|
+
### devicectl 的设备 UUID 和 xcodebuild 的 -destination id 不是同一套
|
|
87
|
+
|
|
88
|
+
- **现象**:`xcrun devicectl list devices` 拿到设备 Identifier(如 `5C5E6FAB-D288-559D-9425-3C2754AA401C`),直接拿去 `xcodebuild -destination 'id=...'`,报 `Unable to find a device matching the provided destination specifier` / `no available devices matched`。
|
|
89
|
+
- **根因**:两套工具用两套设备标识。`devicectl` 列的是它自己的 CoreDevice UUID;`xcodebuild` 的 `-destination id=` 要的是硬件 ECID 形式的 id(如 `00008101-001645E21406001E`)。同一台真机两个 id 完全不同,不能混用。
|
|
90
|
+
- **正确做法**:编译阶段从 `xcodebuild` 报错末尾的「Available destinations for the scheme」里抄那台机器的 `id=`(ECID 形式)传给 `-destination`;安装阶段 `xcrun devicectl device install app --device <UUID>` 用 `devicectl list devices` 的 UUID。即:build 用 xcodebuild 的 id,install 用 devicectl 的 UUID,按名字对上同一台机器。
|
|
91
|
+
- **适用**:命令行 `xcodebuild` 编译 + `xcrun devicectl device install` 安装真机的流程(Xcode 15+ / iOS 17+ CoreDevice)。
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 排查方法论
|
|
96
|
+
|
|
97
|
+
### 别把 deploymentTarget 下限误读成「不支持高版本」
|
|
98
|
+
|
|
99
|
+
- **现象**:看到工程部署目标 iOS 17.0,以为「不支持 iOS 26」,甚至想抬高部署目标来「支持新系统」。
|
|
100
|
+
- **根因**:deploymentTarget 是**最低**支持版本(下限),不是上限。iOS 17.0 表示 17 及以上(含 26)全部支持。真机装不上几乎从不是因为这个。
|
|
101
|
+
- **正确做法**:真机装不上,按「打开方式 → 签名配置 → Xcode 是否支持该 iOS 大版本」排查,别先动部署目标。另:iOS 新大版本真机需要配套新 Xcode(如 iOS 26 真机需 Xcode 26),但那报的是设备支持 / 架构类错误,也不是改部署目标能解决的。
|
|
102
|
+
- **适用**:通用。
|
|
103
|
+
|
|
104
|
+
### 真机装不上时,拿「同机能跑的另一个项目」做对照
|
|
105
|
+
|
|
106
|
+
- **现象**:一个项目装不上真机、报错含糊(如 unsupported architecture),方向难定。
|
|
107
|
+
- **根因**:同一台 Mac、同一个 Xcode 下,若另一个项目能装真机,说明 Xcode 版本、设备、账号都没问题,差异一定在**这个项目的工程配置**(打开方式 / 签名 / bundle id)。
|
|
108
|
+
- **正确做法**:找一个同机能装真机的项目当 working example,逐项对比 `project.yml`、打开方式、签名设置,差异处就是根因——比凭空猜快得多。
|
|
109
|
+
- **适用**:手头有可对照项目时的真机部署排查。
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 证书 / Keychain / mTLS
|
|
114
|
+
|
|
115
|
+
### SecIdentityCreateWithCertificate 在 iOS 不存在,组装 SecIdentity 要走 keychain 查询
|
|
116
|
+
|
|
117
|
+
- **现象**:用 swift-certificates 生成自签证书后想组装 `SecIdentity`,命令行 `swift test`(host 是 macOS)能过,但一编译 iOS(模拟器/真机)就报 `cannot find 'SecIdentityCreateWithCertificate' in scope`,阻断整个 app build。
|
|
118
|
+
- **根因**:`SecIdentityCreateWithCertificate` 是 **macOS-only** API,iOS SDK 里没有。只在 macOS 跑 `swift test` 不会暴露——因为测试 host 就是 macOS,能解析到该符号。
|
|
119
|
+
- **正确做法**:iOS 上把私钥(`SecItemAdd` + `kSecClassKey`)和证书(`SecItemAdd` + `kSecClassCertificate`)分别入 keychain,再用 `kSecClassIdentity` + `SecItemCopyMatching` 查询——系统会自动把同一公钥的 key+cert 组装成 `SecIdentity` 返回。此写法 iOS / macOS 通用。
|
|
120
|
+
- **适用**:iOS 客户端 mTLS / 自签证书 / SecIdentity 组装。两层工程(SPM 纯逻辑库 + iOS app)只靠 `swift test` 验证逻辑时,**务必再补一次 iOS 编译**,否则 macOS-only 的 Security API 会从 `swift test` 漏过去。
|
|
121
|
+
|
|
122
|
+
### swift-crypto 的 Crypto 产物在 Xcode 测试目标链接报「framework 缺二进制」
|
|
123
|
+
|
|
124
|
+
- **现象**:给 SPM Core 加了 swift-crypto / swift-certificates 后,app target 能 build,但单元测试目标 `build-for-testing` 报 `clang: error: no such file or directory: .../Crypto_<hash>_PackageProduct.framework/Crypto_<hash>_PackageProduct`。
|
|
125
|
+
- **根因**:swift-crypto 的 `Crypto` 产物在 Apple 平台会桥接系统 CryptoKit,其 `PackageProduct.framework` 没有可链接的二进制。测试目标若**同时**依赖 app target 和 `package: <Core>`,就会去直接链这个空 framework 而报缺文件;app target 自身的链接逻辑会跳过它,所以 app build 不受影响、只有测试目标炸。
|
|
126
|
+
- **正确做法**:单元测试目标**只依赖 app 宿主 target**(`- target: <App>`),删掉冗余的 `- package: <Core>`。Core 及其传递框架由 app 链接并嵌入,测试经宿主仍可 `import <Core>`(含 `@testable import <App>`)。改 `project.yml` 后 `xcodegen generate` 重生工程。
|
|
127
|
+
- **适用**:XcodeGen + SPM,且 unit-test 目标依赖了含 swift-crypto / swift-certificates 这类「Apple 平台会桥接系统库」的包时。
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Swift / SwiftUI 代码
|
|
132
|
+
|
|
133
|
+
### @AppStorage 驱动 .preferredColorScheme 切换后不即时生效(须重启)
|
|
134
|
+
|
|
135
|
+
- **现象**:设置页切「浅色/深色/跟随系统」,`.preferredColorScheme` 由 `@AppStorage("themeMode")` 驱动,改后界面不变,杀进程重启才生效。换 sheet→push、枚举存值改存 `String` rawValue、调整视图层级都**无效**——全是猜,方向错。
|
|
136
|
+
- **根因**:SwiftUI 框架级已知问题——`.preferredColorScheme` 由 `@AppStorage` 驱动时不即时刷新,尤其 `.system` 档对应的 `nil` 不触发更新(iOS 17/18 多人复现)。与「枚举 vs String 的 KVO」无关,换存储类型治不了本。
|
|
137
|
+
- **正确做法**:别用 `@AppStorage` 驱动 `.preferredColorScheme`。改用单一 `@MainActor @Observable` 单例(如 `ThemeManager.shared`,持久化自己写 UserDefaults),`@State private var theme = ThemeManager.shared` 在根视图持有,`.preferredColorScheme(theme.mode.colorScheme)` 读它;`@Observable` 属性变化可靠驱动重算,绕开框架 bug。这正是同项目语言切换 `LocalizationManager.shared` 已验证的免重启 pattern——设置类全局开关一律照此办,别走 `@AppStorage` + `App` struct。范例:`Sources/Foundation/DesignKit/ThemeManager.swift` + `TVRemote/App/RootView.swift`。仍命中 nil 不刷新时,system 档读 `@Environment(\.colorScheme)` 当前值兜底代替 nil。
|
|
138
|
+
- **适用**:iOS 17+ SwiftUI,运行时切换 `.preferredColorScheme` 需即时生效。推广:任何「设置改后须即时全局生效」的状态,优先 `@Observable` 单例 + `@State` 持有,而非 `@AppStorage` 跨视图/跨 App-struct 同步。
|
|
139
|
+
|
|
140
|
+
### 隐藏系统导航栏后边缘滑动返回手势失灵
|
|
141
|
+
|
|
142
|
+
- **现象**:用 NavigationStack 做主路由,页面自绘深色 UI 故 `.toolbar(.hidden, for: .navigationBar)` 隐藏系统导航栏。隐藏后 iOS 左缘右滑返回手势(interactivePopGestureRecognizer)不响应,只能点自绘返回按钮。
|
|
143
|
+
- **根因**:`interactivePopGestureRecognizer` 的 `delegate` 默认指向系统导航栏。导航栏被隐藏后,delegate 据「栏不可见」拒绝识别手势,于是边缘返回整体失效。这是 SwiftUI/UIKit 十年老坑,不是 NavigationStack 的 bug。
|
|
144
|
+
- **正确做法**:用 `UIViewControllerRepresentable` 在 `didMove(toParent:)` 拿 `navigationController?.interactivePopGestureRecognizer`,置 `isEnabled = true` 并把 delegate 换成**自定义带守卫的 delegate**(`gestureRecognizerShouldBegin` 仅在 `viewControllers.count > 1` 时返回 true)。封装成 `.swipeBackEnabled()` View 扩展。范例见 `TVRemote/App/SwipeBackEnabler.swift`。**切忌把 delegate 置 nil**——见下条。
|
|
145
|
+
- **适用**:iOS 16+ NavigationStack(或 UIKit push 栈)+ 隐藏系统导航栏自绘顶栏的场景。手感须真机验证。
|
|
146
|
+
|
|
147
|
+
### interactivePopGestureRecognizer.delegate 置 nil 致根页崩溃/卡死
|
|
148
|
+
|
|
149
|
+
- **现象**:为恢复边缘手势把 `interactivePopGestureRecognizer.delegate = nil`。结果:在首页(栈根)边缘右滑后首页卡死、点击崩溃;甚至点返回按钮程序化 pop 也崩溃,app 退到桌面。
|
|
150
|
+
- **根因**:UIKit 默认的手势 delegate 含「栈深 ≤ 1 不识别」的守卫,防止 pop 根控制器。置 nil 移除这道守卫,根页边缘滑动会试图 pop 根控制器,UINavigationController 内部状态损坏 → 卡死/崩溃;损坏后任何后续 pop(含程序化 `path = []`)都可能崩。
|
|
151
|
+
- **正确做法**:不要 `delegate = nil`。装自定义 `UIGestureRecognizerDelegate`,`gestureRecognizerShouldBegin` 返回 `navigationController.viewControllers.count > 1`,根页(count==1)不放行。`shouldRecognizeSimultaneouslyWith` 返回 false 避免与滚动冲突。delegate 弱引用 navigationController 防循环。
|
|
152
|
+
- **适用**:iOS 16+ 自管 interactivePopGestureRecognizer 的场景。本坑由上一条「隐藏 navbar 恢复手势」的错误修法引出,二者要一起记。
|
|
153
|
+
|
|
154
|
+
### TabView 切页保活视图态,别用 switch 渲染 tab 内容
|
|
155
|
+
|
|
156
|
+
- **现象**:底部 tab 用 `@ViewBuilder { switch tab { case .a: AViewView() ... } }` 渲染,切 tab 再切回,页面的滚动位置、已加载数据、嵌套导航栈全丢——「重复刷新」。
|
|
157
|
+
- **根因**:`switch` 在视图树里每次只保留当前 case 的子树,切 tab 时 SwiftUI **销毁**旧 tab 视图、**重建**新 tab 视图,`@State` 随之重置。它不是缓存容器。
|
|
158
|
+
- **正确做法**:用系统 `TabView(selection:)` + 每页 `.tag(...)`。TabView 保活所有 tab 页,切换只是显隐,状态天然保留。隐藏系统 tab bar(`.toolbar(.hidden, for: .tabBar)`)后用自绘底栏,`selection` 与自绘栏共用同一 `@State`。注意别用 `.tabViewStyle(.page)`(会引入左右滑动切页,与边缘返回手势冲突)。
|
|
159
|
+
- **适用**:iOS 16+ SwiftUI 多 tab 且需保留各 tab 页内状态时。
|
|
@@ -42,6 +42,32 @@ final class TaskListViewModel {}
|
|
|
42
42
|
- 全屏背景、遮罩、弹层可用 `ignoresSafeArea()`。
|
|
43
43
|
- 禁止使用固定状态栏高度常量。
|
|
44
44
|
|
|
45
|
+
## 软键盘
|
|
46
|
+
|
|
47
|
+
- 任何调起系统软键盘的输入控件,必须给用户提供主动关闭键盘的途径,禁止只能靠输入框自身失焦。
|
|
48
|
+
- 至少同时满足以下两种退出方式:
|
|
49
|
+
- 点击键盘外空白区域收起:根容器加 `.onTapGesture` 把 `@FocusState` 置 `false`。
|
|
50
|
+
- 滚动视图内滚动时收起:`ScrollView` 加 `.scrollDismissesKeyboard(.interactively)`。
|
|
51
|
+
- 多行输入或无明显空白可点的页面,额外在键盘上方放「完成」按钮收起。
|
|
52
|
+
- 焦点统一用 `@FocusState` 管理,不散落手写 UIKit `resignFirstResponder`。
|
|
53
|
+
|
|
54
|
+
```swift
|
|
55
|
+
@FocusState private var focused: Bool
|
|
56
|
+
|
|
57
|
+
ScrollView {
|
|
58
|
+
TextField(loc("input_placeholder"), text: $text)
|
|
59
|
+
.focused($focused)
|
|
60
|
+
}
|
|
61
|
+
.scrollDismissesKeyboard(.interactively)
|
|
62
|
+
.onTapGesture { focused = false }
|
|
63
|
+
.toolbar {
|
|
64
|
+
ToolbarItemGroup(placement: .keyboard) {
|
|
65
|
+
Spacer()
|
|
66
|
+
Button(loc("done")) { focused = false }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
45
71
|
## 动效
|
|
46
72
|
|
|
47
73
|
- 默认优先 `spring`,用同一组参数避免风格撕裂。
|
|
@@ -54,6 +80,7 @@ private let motionSpring = Animation.spring(response: 0.5, dampingFraction: 0.75
|
|
|
54
80
|
|
|
55
81
|
- 图标优先 `Image(systemName:)`,无符号则用成熟 SVG。
|
|
56
82
|
- iOS 应用图标设计稿只提供一张 `1024x1024` 的 PNG 源图,由系统生成各尺寸适配版本。
|
|
83
|
+
- App Icon 源图必须是直角完整方形,背景填满整张画布,禁止预裁圆角、圆角背景、透明圆角或模拟系统圆角遮罩。
|
|
57
84
|
- 图标主体必须预留至少 `10%` 安全边距,避免系统缩放、圆角遮罩或视觉放大时被切边。
|
|
58
85
|
- 文案禁止 emoji,禁止硬编码,统一 `loc("key")`。
|
|
59
86
|
- 禁止使用 emoji 作为功能说明、状态、提示。
|
|
@@ -75,3 +102,11 @@ Text(loc("home_title"))
|
|
|
75
102
|
|
|
76
103
|
- 颜色、字号走 token,不用裸值。
|
|
77
104
|
- 时间来源通过注入参数或测试可控入口,不直接散落 `Date()`。
|
|
105
|
+
|
|
106
|
+
## 打包与版本号
|
|
107
|
+
|
|
108
|
+
- **build 号用时间戳**:`CFBundleVersion` 每次打包设为 `date +%Y%m%d%H%M`(如 `202606231415`)。时间戳天然唯一递增,满足 TestFlight / App Store「build 号唯一递增」要求,免去人工记号撞号。
|
|
109
|
+
- marketing version `CFBundleShortVersionString`(如 `1.0`)按发布节奏手动改,不随每次打包变。
|
|
110
|
+
- 用 XcodeGen 的工程,`xcodegen generate` 会按 `project.yml` 重写 `Info.plist` 把 build 号覆盖回默认值。顺序必须 **先 `xcodegen generate`、再 `PlistBuddy -c "Set CFBundleVersion <时间戳>"` 设号、最后 archive**,别反。
|
|
111
|
+
- 归档产物放工程内固定目录(如 `build/`)并加 gitignore,不用 `$TMPDIR` 临时目录。
|
|
112
|
+
- changelog 维护在 iOS 端目录下(如 `ios/CHANGELOG.md`),发版前按「新增 / 变更 / 修复」记本次相对上版的用户可感变化。
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 依赖版本新鲜度通用规则(首次技术选型下载依赖时,发布 <7 天的版本不采用;跨 npm/uv/Docker/Android/iOS/鸿蒙/Flutter 等所有栈)
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 依赖版本新鲜度规则
|
|
6
|
+
|
|
7
|
+
> 首次技术选型下载依赖(引新包、选 base 镜像 / SDK 版本)时,刚发布的新版本常带回归 bug:构建失败、Docker 镜像缺层、API breaking change。本规则约束 AI 在装新依赖前主动查「版本发布时间」,避开 <7 天的版本。
|
|
8
|
+
>
|
|
9
|
+
> 适用**所有技术栈**:npm/pnpm、uv/pip、Docker、Android(Gradle/Maven)、iOS(CocoaPods/SPM)、鸿蒙(ohpm)、Flutter(pub) 以及未来出现的任何包管理器。下面给出总约束 + 各栈示例,未列出的栈按「查官方 registry 发布时间」通用流程自行判断。
|
|
10
|
+
|
|
11
|
+
## 一、适用边界
|
|
12
|
+
|
|
13
|
+
- **仅约束首次技术选型下载依赖**:新项目引入新包、新搭脚手架选 base 镜像 / SDK 版本、原项目引入此前没用过的依赖。
|
|
14
|
+
- **已安装的旧版本无需为此升级**。本规则只管「新装 / 新选」,不管「存量升级」。
|
|
15
|
+
- 7 天是**下限排除条件**,不是「必须选最近一个 ≥7 天的版本」。最终选哪个稳定版由 AI 综合判断(成熟度、维护活跃度、依赖体积、与项目栈兼容性)。
|
|
16
|
+
|
|
17
|
+
## 二、核心约束
|
|
18
|
+
|
|
19
|
+
- **必须**在首次安装 / 选定版本前,查候选版本的「首次发布时间」
|
|
20
|
+
- 用官方 registry 的发布时间,**不是**上传时间、**不是** `latest` / `stable` tag 时间
|
|
21
|
+
- **禁止**采用发布时间距今 **<7 天**的版本
|
|
22
|
+
- **禁止**无脑用 `@latest` / `latest` / `stable` / 浮动 tag / 不带版本号安装——这些随时可能落到刚发布的新版本上
|
|
23
|
+
- 已安装版本**无需**为此规则升级
|
|
24
|
+
|
|
25
|
+
## 三、各栈查发布时间示例
|
|
26
|
+
|
|
27
|
+
未列出的栈,按「查官方 registry 元数据 → 取发布时间」自行处理。
|
|
28
|
+
|
|
29
|
+
| 栈 | 查发布时间方法 |
|
|
30
|
+
|---|---|
|
|
31
|
+
| npm / pnpm | `npm view <pkg> time --json`(取目标版本对应的时间字段) |
|
|
32
|
+
| uv / pip | PyPI JSON API:`https://pypi.org/pypi/<pkg>/json` 的 `releases[<version>][0].upload_time` |
|
|
33
|
+
| Docker | Docker Hub 镜像页 / API 的 `last_updated`;优先看具体 tag 的 push 时间 |
|
|
34
|
+
| Android (Gradle/Maven) | Maven Central `maven-metadata.xml` 的 `<lastUpdated>`(格式 yyyyMMddHHmmss) |
|
|
35
|
+
| iOS (CocoaPods) | `https://github.com/CocoaPods/Specs` 或仓库 release / tag 时间 |
|
|
36
|
+
| iOS (SPM) | 源仓库 release / tag 时间 |
|
|
37
|
+
| 鸿蒙 (ohpm) | ohpm registry 包元数据 publishTime |
|
|
38
|
+
| Flutter (pub) | `https://pub.dev/packages/<pkg>/versions` 页 published 时间 |
|
|
39
|
+
|
|
40
|
+
## 四、正反例
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# 反例:直接装 latest,可能落到昨天才发的版本
|
|
44
|
+
pnpm add some-pkg@latest
|
|
45
|
+
uv add some-pkg # 不锁版本
|
|
46
|
+
FROM node:latest # 随时被新镜像覆盖
|
|
47
|
+
implementation 'com.example:lib:latest'
|
|
48
|
+
|
|
49
|
+
# 正例:先查发布时间,选发布 ≥7 天的稳定版
|
|
50
|
+
npm view some-pkg time --json # 看候选版本发布时间
|
|
51
|
+
pnpm add some-pkg@1.2.3 # 1.2.3 发布已满 7 天
|
|
52
|
+
FROM node:20.18.1-slim # 该 tag push 已满 7 天
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 五、AI 自判提示
|
|
56
|
+
|
|
57
|
+
- 栈未列全时,先找该栈的**官方 registry**(包管理器背后的中央仓库),查目标版本的发布时间字段。
|
|
58
|
+
- 拿不到精确发布时间时,保守处理:选发布更早、更稳定的版本,不要选「最新」。
|
|
59
|
+
- 7 天窗口内的新版本,**等待**而非采用,**无任何例外**(含安全补丁)。等满 7 天再用。
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: TDD 开发流程(新功能/bug 修复/重构的测试先行与回归策略)
|
|
2
|
+
description: TDD 开发流程(新功能/bug 修复/重构的测试先行与回归策略)与测试-源码分隔(测试工程独立 tests/ 目录、镜像源码结构、各栈目录约定)
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# TDD 开发规则
|
|
@@ -26,6 +26,42 @@ description: TDD 开发流程(新功能/bug 修复/重构的测试先行与回
|
|
|
26
26
|
2. 再重构 — 修改代码
|
|
27
27
|
3. 再跑测试 — 确认行为没变
|
|
28
28
|
|
|
29
|
+
## 测试与源码分隔(硬约束)
|
|
30
|
+
|
|
31
|
+
测试工程必须与源码分隔,不混在一起。
|
|
32
|
+
|
|
33
|
+
### 核心规则
|
|
34
|
+
|
|
35
|
+
- 测试代码**禁止**与源码同目录混放;测试集中在项目根 / 包根的独立 `tests/` 目录。
|
|
36
|
+
- `tests/` **镜像源码结构**,测试文件与被测源码一一对应、可反查:
|
|
37
|
+
- `src/foo/bar.ts` → `tests/foo/bar.test.ts`
|
|
38
|
+
- `src/services/user.py` → `tests/test_user.py`
|
|
39
|
+
- 源码目录(`src/`)保持纯净,**只放产品代码**;打包 / 发布产物天然不含测试,CI 用一个目录通配即可圈定全部测试。
|
|
40
|
+
- 测试用的 fixture / mock 数据 / 测试工具,放 `tests/` 内(如 `tests/fixtures/`、`tests/conftest.py`),不散落进源码目录。
|
|
41
|
+
|
|
42
|
+
### 为什么(约束的根因)
|
|
43
|
+
|
|
44
|
+
测试混入源码目录会引发一连串问题,分隔后一次性收口:
|
|
45
|
+
|
|
46
|
+
- **被打包进发布产物**:测试随源码一起发到 npm / PyPI,污染产物、撑大体积。
|
|
47
|
+
- **source map / 内部细节泄露**:测试常含内部实现假设、mock 的真实数据,泄露到产物有安全风险。
|
|
48
|
+
- **覆盖率 / 构建统计污染**:测试文件被计入源码统计,覆盖率、行数失真。
|
|
49
|
+
- **CI 难圈定范围**:源码与测试交织,CI 无法用一条目录规则排除测试。
|
|
50
|
+
- **AI / 工具难定位**:找不到「源码↔测试」对应关系,反复把测试误写进 `src/`。
|
|
51
|
+
|
|
52
|
+
### 各栈目录与命令速查表
|
|
53
|
+
|
|
54
|
+
| 栈 | 测试目录 | 命名约定 | 运行命令 |
|
|
55
|
+
|----|---------|---------|---------|
|
|
56
|
+
| Vue / Vitest | `tests/` 镜像 `src/` | `*.test.ts` | `pnpm vitest run [路径]` |
|
|
57
|
+
| Python / pytest | `tests/` | `test_*.py` | `pytest tests/ -v` / `uv run pytest` |
|
|
58
|
+
| Android | `src/test/`(单元)+ `src/androidTest/`(仪器) | `*Test.kt` | `./gradlew test` / `./gradlew connectedAndroidTest` |
|
|
59
|
+
| Node / CLI | `tests/` 镜像 `src/` | `*.test.ts` | 对应 runner(vitest / jest) |
|
|
60
|
+
|
|
61
|
+
注:Android 的 `src/test` / `src/androidTest` 是 Gradle 生态**强约定**的独立测试源集(test source set),与 `src/main` 源码完全分隔,符合本规范"测试与源码分隔"理念,按其生态默认即可,**属合规分隔**,不视为"测试写进 src"。
|
|
62
|
+
|
|
63
|
+
**通用准则(表中未列的栈)**:遵循「独立测试目录 + 镜像源码结构 + 该生态主流 runner 约定」三条原则自行判断,不硬套上表布局。
|
|
64
|
+
|
|
29
65
|
## 回归测试
|
|
30
66
|
|
|
31
67
|
### 开发中:只跑受影响的测试
|
|
@@ -55,7 +91,13 @@ pnpm vitest run tests/utils/format.test.ts # 改了单个工具函数
|
|
|
55
91
|
|
|
56
92
|
## TEST_CASES.md
|
|
57
93
|
|
|
58
|
-
每个包/项目维护一份 `TEST_CASES.md
|
|
94
|
+
每个包/项目维护一份 `TEST_CASES.md`,记录所有测试用例的清单。**`TEST_CASES.md` 跟测试目录走,不放项目根**:
|
|
95
|
+
|
|
96
|
+
- 有 `tests/` → 放 `tests/TEST_CASES.md`
|
|
97
|
+
- 测试源集分散(如 Android `src/test/`)→ 放对应测试根(`src/test/TEST_CASES.md`),与该模块测试同处
|
|
98
|
+
- 理念:测试清单是测试工程的一部分,归测试目录,与「测试与源码分隔」一致
|
|
99
|
+
|
|
100
|
+
内容要求:
|
|
59
101
|
|
|
60
102
|
- 人类可读,方便审查和手动编辑
|
|
61
103
|
- 新增/修改/删除功能时必须同步更新
|
|
@@ -68,3 +110,6 @@ pnpm vitest run tests/utils/format.test.ts # 改了单个工具函数
|
|
|
68
110
|
- **禁止**写完代码再补测试(先测试后代码)
|
|
69
111
|
- **禁止**测试不通过就提交
|
|
70
112
|
- **禁止** TEST_CASES.md 与实际测试不同步
|
|
113
|
+
- **禁止**测试文件写进 `src/` 源码目录(Android `src/test`、`src/androidTest` 例外,是 Gradle 标准测试源集)
|
|
114
|
+
- **禁止**测试目录结构与源码脱节(找不到「源码↔测试」对应关系)
|
|
115
|
+
- **禁止**把测试 fixture / mock 数据散落进源码目录
|