chore: stop tracking ignored directories (.claude, .cursor, .specify, .trae, .github, docs)
Showing
50 changed files
with
0 additions
and
3952 deletions
.claude/settings.local.json
deleted
100644 → 0
.cursor/commands/speckit.analyze.md
deleted
100644 → 0
| 1 | ---- | ||
| 2 | -description: 在生成 tasks.md 之后,对 spec.md、plan.md、tasks.md 做一次非破坏性的跨文件一致性与质量分析。 | ||
| 3 | ---- | ||
| 4 | - | ||
| 5 | -## 用户输入 | ||
| 6 | - | ||
| 7 | -```text | ||
| 8 | -$ARGUMENTS | ||
| 9 | -``` | ||
| 10 | - | ||
| 11 | -在继续之前,你**必须**先考虑用户输入(如果不为空)。 | ||
| 12 | - | ||
| 13 | -## 目标 | ||
| 14 | - | ||
| 15 | -在开始实现之前,识别三份核心产物(`spec.md`、`plan.md`、`tasks.md`)之间的不一致、重复、歧义和说明不足之处。本命令**必须**在 `/speckit.tasks` 成功生成完整的 `tasks.md` 之后才运行。 | ||
| 16 | - | ||
| 17 | -## 运行约束 | ||
| 18 | - | ||
| 19 | -**严格只读**:**不要**修改任何文件。输出结构化的分析报告。可以提供可选的修复方案(用户必须明确同意后,才可以人工触发后续的编辑命令)。 | ||
| 20 | - | ||
| 21 | -**宪法优先级**:在本分析范围内,项目宪法(`.specify/memory/constitution.md`)**不可协商**。任何与宪法冲突的问题都自动视为 CRITICAL,并要求调整 spec/plan/tasks,而不是淡化、重新解释或悄悄忽略原则。如果确实需要修改原则本身,必须在 `/speckit.analyze` 之外,通过单独、明确的宪法更新来完成。 | ||
| 22 | - | ||
| 23 | -## 执行步骤 | ||
| 24 | - | ||
| 25 | -### 1. 初始化分析上下文 | ||
| 26 | - | ||
| 27 | -在仓库根目录只运行一次 `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks`,并解析 JSON 中的 FEATURE_DIR 和 AVAILABLE_DOCS。推导出绝对路径: | ||
| 28 | - | ||
| 29 | -- SPEC = FEATURE_DIR/spec.md | ||
| 30 | -- PLAN = FEATURE_DIR/plan.md | ||
| 31 | -- TASKS = FEATURE_DIR/tasks.md | ||
| 32 | - | ||
| 33 | -如果缺少任何必需文件,直接报错并提示用户运行缺失的前置命令。 | ||
| 34 | -当参数里包含单引号(例如 "I'm Groot")时,使用转义写法:如 'I'\''m Groot'(或尽量用双引号:"I'm Groot")。 | ||
| 35 | - | ||
| 36 | -### 2. 加载产物(渐进式披露) | ||
| 37 | - | ||
| 38 | -每个产物只加载最少且必要的上下文: | ||
| 39 | - | ||
| 40 | -**来自 spec.md:** | ||
| 41 | - | ||
| 42 | -- 概览/背景 | ||
| 43 | -- 功能需求 | ||
| 44 | -- 非功能需求 | ||
| 45 | -- 用户故事 | ||
| 46 | -- 边界情况(如存在) | ||
| 47 | - | ||
| 48 | -**来自 plan.md:** | ||
| 49 | - | ||
| 50 | -- 架构/技术栈选择 | ||
| 51 | -- 数据模型引用 | ||
| 52 | -- 阶段划分 | ||
| 53 | -- 技术约束 | ||
| 54 | - | ||
| 55 | -**来自 tasks.md:** | ||
| 56 | - | ||
| 57 | -- 任务 ID | ||
| 58 | -- 任务描述 | ||
| 59 | -- 阶段分组 | ||
| 60 | -- 并行标记 [P] | ||
| 61 | -- 引用到的文件路径 | ||
| 62 | - | ||
| 63 | -**来自宪法:** | ||
| 64 | - | ||
| 65 | -- 加载 `.specify/memory/constitution.md` 用于原则校验 | ||
| 66 | - | ||
| 67 | -### 3. 构建语义模型 | ||
| 68 | - | ||
| 69 | -创建内部表示(输出中不要包含原始产物全文): | ||
| 70 | - | ||
| 71 | -- **需求清单**:每条功能/非功能需求都要有稳定的 key(根据祈使句短语生成 slug;例如 “用户可以上传文件” → `user-can-upload-file`) | ||
| 72 | -- **用户故事/动作清单**:离散的用户动作及其验收标准 | ||
| 73 | -- **任务覆盖映射**:将每个任务映射到一个或多个需求/故事(通过关键字推断或显式引用模式,比如 ID、关键短语) | ||
| 74 | -- **宪法规则集**:提取原则名称以及 MUST/SHOULD 等规范性语句 | ||
| 75 | - | ||
| 76 | -### 4. 检测遍历(Token 高效分析) | ||
| 77 | - | ||
| 78 | -聚焦高信号问题。总计最多输出 50 条发现;其余聚合到溢出摘要中。 | ||
| 79 | - | ||
| 80 | -#### A. 重复检测 | ||
| 81 | - | ||
| 82 | -- 识别近似重复的需求 | ||
| 83 | -- 标记表达质量较低的需求,建议合并收敛表述 | ||
| 84 | - | ||
| 85 | -#### B. 歧义检测 | ||
| 86 | - | ||
| 87 | -- 标记缺少可度量标准的模糊形容词(fast、scalable、secure、intuitive、robust 等) | ||
| 88 | -- 标记未解决的占位符(TODO、TKTK、???、`<placeholder>` 等) | ||
| 89 | - | ||
| 90 | -#### C. 说明不足 | ||
| 91 | - | ||
| 92 | -- 有动词但缺少对象或可度量结果的需求 | ||
| 93 | -- 用户故事缺少与验收标准对齐的说明 | ||
| 94 | -- 任务引用了 spec/plan 中未定义的文件或组件 | ||
| 95 | - | ||
| 96 | -#### D. 宪法对齐 | ||
| 97 | - | ||
| 98 | -- 任意与宪法 MUST 原则冲突的需求或计划要素 | ||
| 99 | -- 缺失宪法要求的必填章节或质量门禁 | ||
| 100 | - | ||
| 101 | -#### E. 覆盖缺口 | ||
| 102 | - | ||
| 103 | -- 没有关联任务的需求 | ||
| 104 | -- 未映射到任何需求/故事的任务 | ||
| 105 | -- 未在任务中体现的非功能需求(例如性能、安全) | ||
| 106 | - | ||
| 107 | -#### F. 不一致 | ||
| 108 | - | ||
| 109 | -- 术语漂移(同一概念在不同文件中用不同名称) | ||
| 110 | -- plan 引用的数据实体在 spec 中缺失(或反之) | ||
| 111 | -- 任务顺序自相矛盾(例如未注明依赖却在基础初始化之前安排集成任务) | ||
| 112 | -- 需求相互冲突(例如一处要求 Next.js,另一处指定 Vue) | ||
| 113 | - | ||
| 114 | -### 5. 严重级别判定 | ||
| 115 | - | ||
| 116 | -使用以下启发式规则为发现排序: | ||
| 117 | - | ||
| 118 | -- **CRITICAL**: 违反宪法 MUST、缺失核心产物,或关键需求零覆盖导致基线功能无法实现 | ||
| 119 | -- **HIGH**: 重复或冲突的需求、含糊的安全/性能属性、不可测试的验收标准 | ||
| 120 | -- **MEDIUM**: 术语漂移、缺少非功能需求的任务覆盖、边界情况说明不足 | ||
| 121 | -- **LOW**: 风格/措辞改进、不影响执行顺序的轻微冗余 | ||
| 122 | - | ||
| 123 | -### 6. 产出精简分析报告 | ||
| 124 | - | ||
| 125 | -按以下结构输出 Markdown 报告(不写文件): | ||
| 126 | - | ||
| 127 | -## 规格说明分析报告 | ||
| 128 | - | ||
| 129 | -| ID | 类别 | 严重级别 | 位置 | 摘要 | 建议 | | ||
| 130 | -|----|----------|----------|-------------|---------|----------------| | ||
| 131 | -| A1 | 重复 | HIGH | spec.md:L120-134 | 两条相似需求…… | 合并表述;保留更清晰的版本 | | ||
| 132 | - | ||
| 133 | -(每条发现一行;生成稳定 ID,并以类别首字母作为前缀。) | ||
| 134 | - | ||
| 135 | -**覆盖摘要表:** | ||
| 136 | - | ||
| 137 | -| 需求 Key | 是否有任务 | 任务 ID | 备注 | | ||
| 138 | -|-----------------|-----------|----------|-------| | ||
| 139 | - | ||
| 140 | -**宪法对齐问题:**(如有) | ||
| 141 | - | ||
| 142 | -**未映射任务:**(如有) | ||
| 143 | - | ||
| 144 | -**指标:** | ||
| 145 | - | ||
| 146 | -- 需求总数 | ||
| 147 | -- 任务总数 | ||
| 148 | -- 覆盖率(至少关联 1 个任务的需求占比) | ||
| 149 | -- 歧义数量 | ||
| 150 | -- 重复数量 | ||
| 151 | -- 严重问题数量 | ||
| 152 | - | ||
| 153 | -### 7. 给出下一步动作 | ||
| 154 | - | ||
| 155 | -在报告末尾输出精炼的“下一步动作”区块: | ||
| 156 | - | ||
| 157 | -- 若存在 CRITICAL:建议在进入 `/speckit.implement` 之前先解决 | ||
| 158 | -- 若只有 LOW/MEDIUM:可继续推进,但应给出改进建议 | ||
| 159 | -- 给出明确命令建议:例如“用更精炼描述重新运行 /speckit.specify”“运行 /speckit.plan 调整架构”“手动编辑 tasks.md 为 ‘performance-metrics’ 补齐覆盖” | ||
| 160 | - | ||
| 161 | -### 8. 提供修复建议 | ||
| 162 | - | ||
| 163 | -询问用户:“你希望我为前 N 个问题给出具体的修复编辑建议吗?”(不要自动应用。) | ||
| 164 | - | ||
| 165 | -## 运行原则 | ||
| 166 | - | ||
| 167 | -### 上下文效率 | ||
| 168 | - | ||
| 169 | -- **最小高信号 token**:聚焦可执行发现,不做穷举式文档复述 | ||
| 170 | -- **渐进式披露**:逐步加载产物,不把全文倾倒进分析 | ||
| 171 | -- **token 高效输出**:发现表最多 50 行,其余做溢出摘要 | ||
| 172 | -- **确定性结果**:在无变更前提下重复运行应得到一致的 ID 与统计 | ||
| 173 | - | ||
| 174 | -### 分析准则 | ||
| 175 | - | ||
| 176 | -- **禁止修改文件**(严格只读分析) | ||
| 177 | -- **禁止臆造缺失章节**(缺失就如实报告) | ||
| 178 | -- **优先处理宪法违规则**(一律 CRITICAL) | ||
| 179 | -- **用例子胜过穷举规则**(引用具体实例而不是泛泛而谈) | ||
| 180 | -- **零问题也要输出报告**(给出覆盖统计并明确通过) | ||
| 181 | - | ||
| 182 | -## 上下文 | ||
| 183 | - | ||
| 184 | -$ARGUMENTS |
This diff is collapsed. Click to expand it.
.cursor/commands/speckit.clarify.md
deleted
100644 → 0
This diff is collapsed. Click to expand it.
| 1 | ---- | ||
| 2 | -description: 通过交互式或提供的原则输入创建/更新项目宪法,并确保所有依赖模板保持同步。 | ||
| 3 | -handoffs: | ||
| 4 | - - label: 生成规格说明 | ||
| 5 | - agent: speckit.specify | ||
| 6 | - prompt: 基于更新后的宪法生成功能规格说明。我想要构建…… | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -## 用户输入 | ||
| 10 | - | ||
| 11 | -```text | ||
| 12 | -$ARGUMENTS | ||
| 13 | -``` | ||
| 14 | - | ||
| 15 | -在继续之前,你**必须**先考虑用户输入(如果不为空)。 | ||
| 16 | - | ||
| 17 | -## 概述 | ||
| 18 | - | ||
| 19 | -你正在更新 `.specify/memory/constitution.md` 中的项目宪法。该文件是一个模板(Template),包含方括号占位符(例如 `[PROJECT_NAME]`、`[PRINCIPLE_1_NAME]`)。你的工作是:(a)收集/推导具体值,(b)精准填充模板,(c)将所有修订同步传播到依赖产物中。 | ||
| 20 | - | ||
| 21 | -按以下流程执行: | ||
| 22 | - | ||
| 23 | -1. 加载 `.specify/memory/constitution.md` 中现有的宪法模板。 | ||
| 24 | - - 识别所有形如 `[ALL_CAPS_IDENTIFIER]` 的占位符 token。 | ||
| 25 | - **重要**:用户需要的原则数量可能少于或多于模板默认数量。如果用户指定了数量,必须尊重该数量,并在通用模板结构下调整文档内容。 | ||
| 26 | - | ||
| 27 | -2. 收集/推导占位符的具体值: | ||
| 28 | - - 若用户输入(对话中)已提供取值,直接使用。 | ||
| 29 | - - 否则从仓库上下文推断(README、文档、若存在则参考仓库内嵌的旧宪法内容)。 | ||
| 30 | - - 治理日期规则:`RATIFICATION_DATE` 为原始生效日期(未知则询问或标记 TODO);`LAST_AMENDED_DATE` 若本次有修改则为今天,否则保留原值。 | ||
| 31 | - - `CONSTITUTION_VERSION` 必须按语义化版本规则递增: | ||
| 32 | - - MAJOR:不向后兼容的治理/原则移除或重定义。 | ||
| 33 | - - MINOR:新增原则/章节,或对既有指导做实质性扩展。 | ||
| 34 | - - PATCH:澄清、措辞、错别字修正、非语义性的精炼。 | ||
| 35 | - - 若版本升级类型不明确,在最终落盘前先给出判断理由。 | ||
| 36 | - | ||
| 37 | -3. 起草更新后的宪法内容: | ||
| 38 | - - 用具体文本替换每个占位符(不应残留方括号 token;除非项目明确决定暂不定义,并对每个保留项给出理由)。 | ||
| 39 | - - 保持标题层级不变;占位符被替换后,注释可移除,除非该注释仍对理解有帮助。 | ||
| 40 | - - 确保每条原则包含:简短名称行、概括不可协商规则的段落(或要点列表),若理由不明显则补充明确的理由说明。 | ||
| 41 | - - 确保治理(Governance)章节包含:修订流程、版本策略、合规审查期望。 | ||
| 42 | - | ||
| 43 | -4. 一致性传播清单(把旧清单转为可执行校验项): | ||
| 44 | - - 阅读 `.specify/templates/plan-template.md`,确保其中的“宪法检查”或规则与更新后的原则一致。 | ||
| 45 | - - 阅读 `.specify/templates/spec-template.md`,校验范围/需求对齐;若宪法新增/移除必填章节或约束,则同步更新模板。 | ||
| 46 | - - 阅读 `.specify/templates/tasks-template.md`,确保任务分类能体现新增/移除的原则驱动任务类型(例如可观测性、版本策略、测试纪律)。 | ||
| 47 | - - 阅读 `.specify/templates/commands/*.md` 下的每个命令文件(包含本文件),确保通用指导中不残留过时的 agent 特定引用(例如仅写 CLAUDE)。 | ||
| 48 | - - 阅读运行/使用指导文档(例如 `README.md`、`docs/quickstart.md`,或存在的 agent 专属指导文件),同步更新与宪法变更相关的引用。 | ||
| 49 | - | ||
| 50 | -5. 生成“同步影响报告”(更新后,作为 HTML 注释插入到宪法文件顶部): | ||
| 51 | - - 版本变更:旧版本 → 新版本 | ||
| 52 | - - 修改过的原则列表(若重命名则写 旧标题 → 新标题) | ||
| 53 | - - 新增章节 | ||
| 54 | - - 移除章节 | ||
| 55 | - - 需要更新的模板列表(✅ 已更新 / ⚠ 待处理),并附文件路径 | ||
| 56 | - - 若有占位符被刻意延期,列出后续 TODO | ||
| 57 | - | ||
| 58 | -6. 最终输出前校验: | ||
| 59 | - - 不残留任何无法解释的方括号占位符 token。 | ||
| 60 | - - 版本行与同步影响报告一致。 | ||
| 61 | - - 日期使用 ISO 格式 YYYY-MM-DD。 | ||
| 62 | - - 原则陈述应可验证、可测试,避免含糊措辞(必要时将“should”替换为 MUST/SHOULD,并给出理由)。 | ||
| 63 | - | ||
| 64 | -7. 将完成的宪法内容写回 `.specify/memory/constitution.md`(覆盖写入)。 | ||
| 65 | - | ||
| 66 | -8. 输出给用户的最终总结应包含: | ||
| 67 | - - 新版本号与升级理由。 | ||
| 68 | - - 需要人工跟进的文件(如有)。 | ||
| 69 | - - 建议的提交信息(例如 `docs: amend constitution to vX.Y.Z (principle additions + governance update)`)。 | ||
| 70 | - | ||
| 71 | -格式与样式要求: | ||
| 72 | - | ||
| 73 | -- 标题层级必须与模板完全一致(不要升/降级标题)。 | ||
| 74 | -- 适度换行以保持可读性(理想情况下 <100 字符),但不要为了硬性限制造成生硬断行。 | ||
| 75 | -- 章节之间保持一个空行。 | ||
| 76 | -- 避免行尾空格。 | ||
| 77 | - | ||
| 78 | -如果用户只提供部分更新(例如只修改一条原则),也要照样执行校验与版本决策步骤。 | ||
| 79 | - | ||
| 80 | -如果缺少关键信息(例如确实不知道 ratification date),插入 `TODO(<FIELD_NAME>): explanation`,并在同步影响报告的延期项(deferred)中列出。 | ||
| 81 | - | ||
| 82 | -不要创建新的模板;始终在现有的 `.specify/memory/constitution.md` 文件上操作。 |
| 1 | ---- | ||
| 2 | -description: 通过处理并执行 tasks.md 中定义的所有任务,落实实现计划 | ||
| 3 | ---- | ||
| 4 | - | ||
| 5 | -## 用户输入 | ||
| 6 | - | ||
| 7 | -```text | ||
| 8 | -$ARGUMENTS | ||
| 9 | -``` | ||
| 10 | - | ||
| 11 | -在继续之前,你**必须**先考虑用户输入(如果不为空)。 | ||
| 12 | - | ||
| 13 | -## 概述 | ||
| 14 | - | ||
| 15 | -1. 在仓库根目录运行一次 `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks`,并解析 JSON 中的 FEATURE_DIR 与 AVAILABLE_DOCS 列表。所有路径必须是绝对路径。当参数里包含单引号(例如 "I'm Groot")时,使用转义写法:如 'I'\''m Groot'(或尽量用双引号:"I'm Groot")。 | ||
| 16 | - | ||
| 17 | -2. **检查检查清单状态**(如果存在 FEATURE_DIR/checklists/): | ||
| 18 | - - 扫描 checklists/ 目录下的所有清单文件 | ||
| 19 | - - 对每个清单统计: | ||
| 20 | - - 总条目:匹配 `- [ ]` 或 `- [X]` 或 `- [x]` 的所有行 | ||
| 21 | - - 已完成条目:匹配 `- [X]` 或 `- [x]` 的行 | ||
| 22 | - - 未完成条目:匹配 `- [ ]` 的行 | ||
| 23 | - - 生成状态表: | ||
| 24 | - | ||
| 25 | - ```text | ||
| 26 | - | Checklist | Total | Completed | Incomplete | Status | | ||
| 27 | - |-----------|-------|-----------|------------|--------| | ||
| 28 | - | ux.md | 12 | 12 | 0 | ✓ PASS | | ||
| 29 | - | test.md | 8 | 5 | 3 | ✗ FAIL | | ||
| 30 | - | security.md | 6 | 6 | 0 | ✓ PASS | | ||
| 31 | - ``` | ||
| 32 | - | ||
| 33 | - - 计算总体状态: | ||
| 34 | - - **PASS**:所有清单的未完成条目为 0 | ||
| 35 | - - **FAIL**:存在至少一个清单有未完成条目 | ||
| 36 | - | ||
| 37 | - - **如果存在未完成清单**: | ||
| 38 | - - 展示状态表与未完成数量 | ||
| 39 | - - **停止**并询问:“有些检查清单尚未完成。是否仍要继续实现?(yes/no)” | ||
| 40 | - - 等待用户回复后再继续 | ||
| 41 | - - 若用户回复 “no”“wait” 或 “stop”,则终止执行 | ||
| 42 | - - 若用户回复 “yes”“proceed” 或 “continue”,则进入第 3 步 | ||
| 43 | - | ||
| 44 | - - **如果所有清单都已完成**: | ||
| 45 | - - 展示所有清单通过的状态表 | ||
| 46 | - - 自动进入第 3 步 | ||
| 47 | - | ||
| 48 | -3. 加载并分析实现上下文: | ||
| 49 | - - **必须**:读取 tasks.md 获取完整任务列表与执行计划 | ||
| 50 | - - **必须**:读取 plan.md 获取技术栈、架构与文件结构 | ||
| 51 | - - **如果存在**:读取 data-model.md 获取实体与关系 | ||
| 52 | - - **如果存在**:读取 contracts/ 获取 API 规范与测试要求 | ||
| 53 | - - **如果存在**:读取 research.md 获取技术决策与约束 | ||
| 54 | - - **如果存在**:读取 quickstart.md 获取集成场景 | ||
| 55 | - | ||
| 56 | -4. **项目初始化校验**: | ||
| 57 | - - **必须**:基于实际项目情况创建/校验 ignore 文件: | ||
| 58 | - | ||
| 59 | - **检测与创建逻辑**: | ||
| 60 | - - 通过下列命令是否成功来判断仓库是否是 git 仓库(如果是,则创建/校验 .gitignore): | ||
| 61 | - | ||
| 62 | - ```sh | ||
| 63 | - git rev-parse --git-dir 2>/dev/null | ||
| 64 | - ``` | ||
| 65 | - | ||
| 66 | - - 检查是否存在 Dockerfile* 或 plan.md 中提到 Docker → 创建/校验 .dockerignore | ||
| 67 | - - 检查是否存在 .eslintrc* → 创建/校验 .eslintignore | ||
| 68 | - - 检查是否存在 eslint.config.* → 确保配置中的 `ignores` 覆盖所需模式 | ||
| 69 | - - 检查是否存在 .prettierrc* → 创建/校验 .prettierignore | ||
| 70 | - - 检查是否存在 .npmrc 或 package.json →(若要发布)创建/校验 .npmignore | ||
| 71 | - - 检查是否存在 terraform 文件(*.tf)→ 创建/校验 .terraformignore | ||
| 72 | - - 检查是否需要 .helmignore(存在 helm charts)→ 创建/校验 .helmignore | ||
| 73 | - | ||
| 74 | - **如果 ignore 文件已存在**:校验其包含关键模式,只追加缺失且重要的模式 | ||
| 75 | - **如果 ignore 文件不存在**:按检测到的技术栈创建包含完整模式集合的文件 | ||
| 76 | - | ||
| 77 | - **按技术栈的常见模式**(来自 plan.md 的技术栈): | ||
| 78 | - - **Node.js/JavaScript/TypeScript**: `node_modules/`, `dist/`, `build/`, `*.log`, `.env*` | ||
| 79 | - - **Python**: `__pycache__/`, `*.pyc`, `.venv/`, `venv/`, `dist/`, `*.egg-info/` | ||
| 80 | - - **Java**: `target/`, `*.class`, `*.jar`, `.gradle/`, `build/` | ||
| 81 | - - **C#/.NET**: `bin/`, `obj/`, `*.user`, `*.suo`, `packages/` | ||
| 82 | - - **Go**: `*.exe`, `*.test`, `vendor/`, `*.out` | ||
| 83 | - - **Ruby**: `.bundle/`, `log/`, `tmp/`, `*.gem`, `vendor/bundle/` | ||
| 84 | - - **PHP**: `vendor/`, `*.log`, `*.cache`, `*.env` | ||
| 85 | - - **Rust**: `target/`, `debug/`, `release/`, `*.rs.bk`, `*.rlib`, `*.prof*`, `.idea/`, `*.log`, `.env*` | ||
| 86 | - - **Kotlin**: `build/`, `out/`, `.gradle/`, `.idea/`, `*.class`, `*.jar`, `*.iml`, `*.log`, `.env*` | ||
| 87 | - - **C++**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.so`, `*.a`, `*.exe`, `*.dll`, `.idea/`, `*.log`, `.env*` | ||
| 88 | - - **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `Makefile`, `config.log`, `.idea/`, `*.log`, `.env*` | ||
| 89 | - - **Swift**: `.build/`, `DerivedData/`, `*.swiftpm/`, `Packages/` | ||
| 90 | - - **R**: `.Rproj.user/`, `.Rhistory`, `.RData`, `.Ruserdata`, `*.Rproj`, `packrat/`, `renv/` | ||
| 91 | - - **Universal**: `.DS_Store`, `Thumbs.db`, `*.tmp`, `*.swp`, `.vscode/`, `.idea/` | ||
| 92 | - | ||
| 93 | - **工具特定模式**: | ||
| 94 | - - **Docker**: `node_modules/`, `.git/`, `Dockerfile*`, `.dockerignore`, `*.log*`, `.env*`, `coverage/` | ||
| 95 | - - **ESLint**: `node_modules/`, `dist/`, `build/`, `coverage/`, `*.min.js` | ||
| 96 | - - **Prettier**: `node_modules/`, `dist/`, `build/`, `coverage/`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml` | ||
| 97 | - - **Terraform**: `.terraform/`, `*.tfstate*`, `*.tfvars`, `.terraform.lock.hcl` | ||
| 98 | - - **Kubernetes/k8s**: `*.secret.yaml`, `secrets/`, `.kube/`, `kubeconfig*`, `*.key`, `*.crt` | ||
| 99 | - | ||
| 100 | -5. 解析 tasks.md 结构并提取: | ||
| 101 | - - **任务阶段**:初始化、测试、核心、集成、打磨 | ||
| 102 | - - **任务依赖**:串行与并行的执行规则 | ||
| 103 | - - **任务细节**:ID、描述、文件路径、并行标记 [P] | ||
| 104 | - - **执行流程**:顺序与依赖要求 | ||
| 105 | - | ||
| 106 | -6. 按任务计划执行实现: | ||
| 107 | - - **按阶段执行**:完成当前阶段后再进入下一阶段 | ||
| 108 | - - **遵守依赖**:串行任务按顺序执行;并行任务 [P] 可同时执行 | ||
| 109 | - - **遵循 TDD**:若存在测试任务,先执行测试任务再执行对应实现任务 | ||
| 110 | - - **按文件协调**:影响同一文件的任务必须串行执行 | ||
| 111 | - - **阶段校验点**:进入下一阶段前先校验当前阶段是否完成 | ||
| 112 | - | ||
| 113 | -7. 实现执行规则: | ||
| 114 | - - **先做初始化**:初始化项目结构、依赖、配置 | ||
| 115 | - - **先写测试再写代码**:如果需要为契约、实体与集成场景编写测试 | ||
| 116 | - - **核心开发**:实现模型、服务、CLI 命令、端点 | ||
| 117 | - - **集成工作**:数据库连接、中间件、日志、外部服务 | ||
| 118 | - - **打磨与校验**:单元测试、性能优化、文档 | ||
| 119 | - | ||
| 120 | -8. 进度跟踪与错误处理: | ||
| 121 | - - 每完成一个任务都汇报进度 | ||
| 122 | - - 任意非并行任务失败时立即停止执行 | ||
| 123 | - - 并行任务 [P] 允许继续已成功任务,并汇报失败项 | ||
| 124 | - - 提供带上下文的清晰错误信息,便于定位 | ||
| 125 | - - 若实现无法继续,给出明确的下一步建议 | ||
| 126 | - - **重要**:任务完成后,务必在 tasks 文件中用 [X] 勾掉该任务 | ||
| 127 | - | ||
| 128 | -9. 完成校验: | ||
| 129 | - - 确认所有必需任务都已完成 | ||
| 130 | - - 检查实现的功能是否与原始 spec 一致 | ||
| 131 | - - 校验测试通过且覆盖率满足要求 | ||
| 132 | - - 确认实现遵循技术计划 | ||
| 133 | - - 汇报最终状态并总结已完成工作 | ||
| 134 | - | ||
| 135 | -注意:本命令假设 tasks.md 中已存在完整的任务拆分。如果任务不完整或缺失,应建议先运行 `/speckit.tasks` 重新生成任务列表。 |
.cursor/commands/speckit.plan.md
deleted
100644 → 0
| 1 | ---- | ||
| 2 | -description: 使用计划模板执行实现规划流程,并生成设计产物。 | ||
| 3 | -handoffs: | ||
| 4 | - - label: 生成任务列表 | ||
| 5 | - agent: speckit.tasks | ||
| 6 | - prompt: 将实现计划拆分为可执行任务 | ||
| 7 | - send: true | ||
| 8 | - - label: 生成检查清单 | ||
| 9 | - agent: speckit.checklist | ||
| 10 | - prompt: 为以下领域生成一份检查清单…… | ||
| 11 | ---- | ||
| 12 | - | ||
| 13 | -## 用户输入 | ||
| 14 | - | ||
| 15 | -```text | ||
| 16 | -$ARGUMENTS | ||
| 17 | -``` | ||
| 18 | - | ||
| 19 | -在继续之前,你**必须**先考虑用户输入(如果不为空)。 | ||
| 20 | - | ||
| 21 | -## 概述 | ||
| 22 | - | ||
| 23 | -1. **准备**:在仓库根目录运行 `.specify/scripts/bash/setup-plan.sh --json`,并解析 JSON 中的 FEATURE_SPEC、IMPL_PLAN、SPECS_DIR、BRANCH。当参数里包含单引号(例如 "I'm Groot")时,使用转义写法:如 'I'\''m Groot'(或尽量用双引号:"I'm Groot")。 | ||
| 24 | - | ||
| 25 | -2. **加载上下文**:读取 FEATURE_SPEC 与 `.specify/memory/constitution.md`。加载 IMPL_PLAN 模板(已复制到目标位置)。 | ||
| 26 | - | ||
| 27 | -3. **执行规划流程**:按 IMPL_PLAN 模板结构完成: | ||
| 28 | - - 填写技术上下文(未知项标记为 “NEEDS CLARIFICATION”) | ||
| 29 | - - 从宪法中填写“宪法检查”小节 | ||
| 30 | - - 评估门禁(若存在未经合理解释的违规则 ERROR) | ||
| 31 | - - Phase 0:生成 research.md(解决所有 NEEDS CLARIFICATION) | ||
| 32 | - - Phase 1:生成 data-model.md、contracts/、quickstart.md | ||
| 33 | - - Phase 1:通过运行 agent 脚本更新 agent 上下文 | ||
| 34 | - - 设计完成后重新评估“宪法检查” | ||
| 35 | - | ||
| 36 | -4. **停止并汇报**:命令在 Phase 2 规划后结束。汇报分支名、IMPL_PLAN 路径与生成的产物。 | ||
| 37 | - | ||
| 38 | -## 阶段 | ||
| 39 | - | ||
| 40 | -### Phase 0:大纲与调研 | ||
| 41 | - | ||
| 42 | -1. **从上面的技术上下文中提取未知项**: | ||
| 43 | - - 每个 NEEDS CLARIFICATION → 一条调研任务 | ||
| 44 | - - 每个依赖 → 一条最佳实践任务 | ||
| 45 | - - 每个集成点 → 一条模式/方案任务 | ||
| 46 | - | ||
| 47 | -2. **生成并分发调研 agent**: | ||
| 48 | - | ||
| 49 | - ```text | ||
| 50 | - 对于技术上下文中的每个未知项: | ||
| 51 | - 任务:"为 {feature context} 调研 {unknown}" | ||
| 52 | - 对于每个技术选型: | ||
| 53 | - 任务:"为 {domain} 场景寻找 {tech} 的最佳实践" | ||
| 54 | - ``` | ||
| 55 | - | ||
| 56 | -3. **在 `research.md` 中汇总结论**,格式为: | ||
| 57 | - - 决策(Decision):[选择了什么] | ||
| 58 | - - 理由(Rationale):[为什么这样选] | ||
| 59 | - - 备选方案(Alternatives):[还评估了什么] | ||
| 60 | - | ||
| 61 | -**输出**:research.md,且所有 NEEDS CLARIFICATION 已解决 | ||
| 62 | - | ||
| 63 | -### Phase 1:设计与契约 | ||
| 64 | - | ||
| 65 | -**前置条件:** `research.md` 已完成 | ||
| 66 | - | ||
| 67 | -1. **从功能 spec 中提取实体** → `data-model.md`: | ||
| 68 | - - 实体名称、字段、关系 | ||
| 69 | - - 来自需求的校验规则 | ||
| 70 | - - 若适用:状态流转 | ||
| 71 | - | ||
| 72 | -2. **从功能需求生成 API 契约**: | ||
| 73 | - - 每个用户动作 → 一个 endpoint | ||
| 74 | - - 使用标准 REST/GraphQL 模式 | ||
| 75 | - - 输出 OpenAPI/GraphQL schema 到 `/contracts/` | ||
| 76 | - | ||
| 77 | -3. **更新 agent 上下文**: | ||
| 78 | - - 运行 `.specify/scripts/bash/update-agent-context.sh cursor-agent` | ||
| 79 | - - 脚本会检测当前使用的是哪个 AI agent | ||
| 80 | - - 更新对应的 agent 专属上下文文件 | ||
| 81 | - - 只追加本次计划中新引入的技术 | ||
| 82 | - - 保留标记区间内的人工补充内容 | ||
| 83 | - | ||
| 84 | -**输出**:data-model.md、/contracts/*、quickstart.md、agent 专属上下文文件 | ||
| 85 | - | ||
| 86 | -## 关键规则 | ||
| 87 | - | ||
| 88 | -- 使用绝对路径 | ||
| 89 | -- 门禁失败或存在未解决澄清项时直接 ERROR |
.cursor/commands/speckit.specify.md
deleted
100644 → 0
This diff is collapsed. Click to expand it.
.cursor/commands/speckit.tasks.md
deleted
100644 → 0
| 1 | ---- | ||
| 2 | -description: 基于现有设计产物,为该功能生成可执行、按依赖排序的 tasks.md。 | ||
| 3 | -handoffs: | ||
| 4 | - - label: 一致性分析 | ||
| 5 | - agent: speckit.analyze | ||
| 6 | - prompt: 执行项目一致性分析 | ||
| 7 | - send: true | ||
| 8 | - - label: 分阶段实现 | ||
| 9 | - agent: speckit.implement | ||
| 10 | - prompt: 按阶段开始实现 | ||
| 11 | - send: true | ||
| 12 | ---- | ||
| 13 | - | ||
| 14 | -## 用户输入 | ||
| 15 | - | ||
| 16 | -```text | ||
| 17 | -$ARGUMENTS | ||
| 18 | -``` | ||
| 19 | - | ||
| 20 | -在继续之前,你**必须**先考虑用户输入(如果不为空)。 | ||
| 21 | - | ||
| 22 | -## 概述 | ||
| 23 | - | ||
| 24 | -1. **准备**:在仓库根目录运行 `.specify/scripts/bash/check-prerequisites.sh --json` 并解析 FEATURE_DIR 与 AVAILABLE_DOCS 列表。所有路径必须是绝对路径。当参数里包含单引号(例如 "I'm Groot")时,使用转义写法:如 'I'\''m Groot'(或尽量用双引号:"I'm Groot")。 | ||
| 25 | - | ||
| 26 | -2. **加载设计文档**:从 FEATURE_DIR 读取: | ||
| 27 | - - **必需**:plan.md(技术栈、依赖库、结构)、spec.md(带优先级的用户故事) | ||
| 28 | - - **可选**:data-model.md(实体)、contracts/(API 端点)、research.md(决策)、quickstart.md(测试场景) | ||
| 29 | - - 注意:并非所有项目都有全部文档。应基于当前可用文档生成任务。 | ||
| 30 | - | ||
| 31 | -3. **执行任务生成流程**: | ||
| 32 | - - 加载 plan.md,提取技术栈、依赖库与项目结构 | ||
| 33 | - - 加载 spec.md,提取带优先级的用户故事(P1、P2、P3 等) | ||
| 34 | - - 如果存在 data-model.md:提取实体并映射到用户故事 | ||
| 35 | - - 如果存在 contracts/:将端点映射到用户故事 | ||
| 36 | - - 如果存在 research.md:提取决策用于初始化相关任务 | ||
| 37 | - - 按用户故事组织生成任务(见下方“任务生成规则”) | ||
| 38 | - - 生成依赖图,展示用户故事的完成顺序 | ||
| 39 | - - 为每个用户故事提供并行执行示例 | ||
| 40 | - - 校验任务完整性(每个用户故事包含所需任务,且可独立测试) | ||
| 41 | - | ||
| 42 | -4. **生成 tasks.md**:以 `.specify/templates/tasks-template.md` 为结构,填充: | ||
| 43 | - - 从 plan.md 读取正确的功能名称 | ||
| 44 | - - Phase 1:初始化任务(项目初始化) | ||
| 45 | - - Phase 2:基础任务(所有用户故事的阻塞前置) | ||
| 46 | - - Phase 3+:按 spec.md 的优先级为每个用户故事建一个阶段 | ||
| 47 | - - 每个阶段包含:故事目标、独立测试标准、测试(如有要求)、实现任务 | ||
| 48 | - - 最终阶段:打磨与横切关注点 | ||
| 49 | - - 所有任务必须遵循严格的清单格式(见下方“任务生成规则”) | ||
| 50 | - - 每个任务都要写清楚文件路径 | ||
| 51 | - - Dependencies 章节展示故事完成顺序 | ||
| 52 | - - 每个故事提供并行执行示例 | ||
| 53 | - - Implementation strategy 章节说明实现策略(先 MVP,逐步交付) | ||
| 54 | - | ||
| 55 | -5. **报告(Report)**:输出生成的 tasks.md 路径与摘要: | ||
| 56 | - - 任务总数 | ||
| 57 | - - 每个用户故事的任务数 | ||
| 58 | - - 识别到的并行机会 | ||
| 59 | - - 每个故事的独立测试标准 | ||
| 60 | - - 建议的 MVP 范围(通常只包含用户故事 1) | ||
| 61 | - - 格式校验:确认所有任务都遵循清单格式(checkbox、ID、标签、文件路径) | ||
| 62 | - | ||
| 63 | -用于任务生成的上下文:$ARGUMENTS | ||
| 64 | - | ||
| 65 | -tasks.md 应当可以直接执行——每个任务都必须足够具体,使得 LLM 在不依赖额外上下文的情况下也能完成。 | ||
| 66 | - | ||
| 67 | -## 任务生成规则 | ||
| 68 | - | ||
| 69 | -**关键(CRITICAL)**:任务必须按用户故事组织,以支持独立实现与独立测试。 | ||
| 70 | - | ||
| 71 | -**测试是可选项**:只有在 spec 明确要求,或用户要求 TDD 时才生成测试任务。 | ||
| 72 | - | ||
| 73 | -### 清单格式(必须) | ||
| 74 | - | ||
| 75 | -每个任务都必须严格遵循以下格式: | ||
| 76 | - | ||
| 77 | -```text | ||
| 78 | -- [ ] [TaskID] [P?] [Story?] 包含文件路径的任务描述 | ||
| 79 | -``` | ||
| 80 | - | ||
| 81 | -**格式组成**: | ||
| 82 | - | ||
| 83 | -1. **勾选框(Checkbox)**:必须以 `- [ ]` 开头(Markdown checkbox) | ||
| 84 | -2. **任务 ID(Task ID)**:按执行顺序递增编号(T001、T002、T003...) | ||
| 85 | -3. **[P] 标记**:仅当任务可并行时才包含(不同文件,且不依赖未完成任务) | ||
| 86 | -4. **[Story] 标签**:仅用于“用户故事阶段”的任务,且必须包含 | ||
| 87 | - - 格式:[US1]、[US2]、[US3] 等(映射 spec.md 的用户故事) | ||
| 88 | - - 初始化阶段:不加 story 标签 | ||
| 89 | - - 基础阶段:不加 story 标签 | ||
| 90 | - - 用户故事阶段:必须有 story 标签 | ||
| 91 | - - 打磨阶段:不加 story 标签 | ||
| 92 | -5. **描述(Description)**:清晰描述动作,并包含准确文件路径 | ||
| 93 | - | ||
| 94 | -**示例**: | ||
| 95 | - | ||
| 96 | -- ✅ 正确:`- [ ] T001 按实现计划创建项目结构` | ||
| 97 | -- ✅ 正确:`- [ ] T005 [P] 在 src/middleware/auth.py 实现认证中间件` | ||
| 98 | -- ✅ 正确:`- [ ] T012 [P] [US1] 在 src/models/user.py 创建 User 模型` | ||
| 99 | -- ✅ 正确:`- [ ] T014 [US1] 在 src/services/user_service.py 实现 UserService` | ||
| 100 | -- ❌ 错误:`- [ ] 创建用户模型`(缺少 ID 与 Story 标签) | ||
| 101 | -- ❌ 错误:`T001 [US1] 创建模型`(缺少 checkbox) | ||
| 102 | -- ❌ 错误:`- [ ] [US1] 创建用户模型`(缺少 Task ID) | ||
| 103 | -- ❌ 错误:`- [ ] T001 [US1] 创建模型`(缺少文件路径) | ||
| 104 | - | ||
| 105 | -### 任务组织方式 | ||
| 106 | - | ||
| 107 | -1. **来自用户故事(spec.md)——主要组织方式**: | ||
| 108 | - - 每个用户故事(P1、P2、P3...)对应一个阶段 | ||
| 109 | - - 将所有相关组件映射到对应的故事: | ||
| 110 | - - 该故事需要的模型 | ||
| 111 | - - 该故事需要的服务 | ||
| 112 | - - 该故事需要的端点/UI | ||
| 113 | - - 若需要测试:该故事专属的测试任务 | ||
| 114 | - - 标记故事依赖(大多数故事应尽量独立) | ||
| 115 | - | ||
| 116 | -2. **来自 contracts**: | ||
| 117 | - - 将每个 contract/endpoint 映射到它服务的用户故事 | ||
| 118 | - - 若需要测试:每个 contract 在该故事阶段中,先生成 contract 测试任务 [P],再实现 | ||
| 119 | - | ||
| 120 | -3. **来自数据模型**: | ||
| 121 | - - 将每个实体映射到需要它的用户故事 | ||
| 122 | - - 若实体服务多个故事:放入最早的故事阶段或初始化阶段 | ||
| 123 | - - 关系处理 → 放到对应故事阶段的服务层任务中 | ||
| 124 | - | ||
| 125 | -4. **来自初始化/基础设施**: | ||
| 126 | - - 共享基础设施 → 初始化阶段(Phase 1) | ||
| 127 | - - 基础/阻塞任务 → 基础阶段(Phase 2) | ||
| 128 | - - 故事特定的初始化 → 放到该故事阶段内 | ||
| 129 | - | ||
| 130 | -### 阶段结构 | ||
| 131 | - | ||
| 132 | -- **Phase 1**:初始化(项目初始化) | ||
| 133 | -- **Phase 2**:基础(阻塞前置;必须在用户故事之前完成) | ||
| 134 | -- **Phase 3+**:按优先级顺序的用户故事(P1、P2、P3...) | ||
| 135 | - - 每个故事内:测试(如需)→ 模型 → 服务 → 端点 → 集成 | ||
| 136 | - - 每个阶段都应是一个完整且可独立测试的增量 | ||
| 137 | -- **最终阶段**:打磨与横切关注点 |
| 1 | ---- | ||
| 2 | -description: 基于现有设计产物,将已有任务转换为可执行、按依赖排序的 GitHub issue。 | ||
| 3 | -tools: ['github/github-mcp-server/issue_write'] | ||
| 4 | ---- | ||
| 5 | - | ||
| 6 | -## 用户输入 | ||
| 7 | - | ||
| 8 | -```text | ||
| 9 | -$ARGUMENTS | ||
| 10 | -``` | ||
| 11 | - | ||
| 12 | -在继续之前,你**必须**先考虑用户输入(如果不为空)。 | ||
| 13 | - | ||
| 14 | -## 概述 | ||
| 15 | - | ||
| 16 | -1. 在仓库根目录运行 `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` 并解析 FEATURE_DIR 与 AVAILABLE_DOCS 列表。所有路径必须是绝对路径。当参数里包含单引号(例如 "I'm Groot")时,使用转义写法:如 'I'\''m Groot'(或尽量用双引号:"I'm Groot")。 | ||
| 17 | -2. 从脚本输出中提取 **tasks** 文件路径。 | ||
| 18 | -3. 通过以下命令获取 Git 远端地址(remote): | ||
| 19 | - | ||
| 20 | -```bash | ||
| 21 | -git config --get remote.origin.url | ||
| 22 | -``` | ||
| 23 | - | ||
| 24 | -> [!CAUTION] | ||
| 25 | -> 只有当 remote 是 GitHub URL 时才允许继续后续步骤 | ||
| 26 | - | ||
| 27 | -4. 对任务列表中的每个任务,使用 GitHub MCP server 在与该 remote 相匹配的仓库中创建一个新 issue。 | ||
| 28 | - | ||
| 29 | -> [!CAUTION] | ||
| 30 | -> 任何情况下都不要在与 remote URL 不匹配的仓库里创建 issue |
| 1 | ---- | ||
| 2 | -description: "按任务实现并自检(implement)" | ||
| 3 | -agent: agent | ||
| 4 | -argument-hint: "feature_key(例如 001-xxx)" | ||
| 5 | ---- | ||
| 6 | - | ||
| 7 | -你需要按 specs/<feature_key>/tasks.md 逐条完成实现,并遵循: | ||
| 8 | - | ||
| 9 | -- 严格对照 specs/<feature_key>/spec.md 的验收标准 | ||
| 10 | -- 修改尽量小,避免无关重构 | ||
| 11 | -- 接口返回结构固定为 { code, data, msg },错误提示使用 msg | ||
| 12 | -- 图片域名为 cdn.ipadbiz.cn 时追加 ?imageMogr2/thumbnail/200x/strip/quality/70 | ||
| 13 | -- 完成后运行现有的测试/检查脚本(如果仓库提供) | ||
| 14 | -- 不要启动或重启任何服务;不要引导打开预览 |
| 1 | ---- | ||
| 2 | -description: "生成技术方案(plan)" | ||
| 3 | -agent: agent | ||
| 4 | -argument-hint: "feature_key(例如 001-xxx)" | ||
| 5 | ---- | ||
| 6 | - | ||
| 7 | -基于 specs/<feature_key>/spec.md,生成 specs/<feature_key>/plan.md。 | ||
| 8 | - | ||
| 9 | -约束: | ||
| 10 | -- 给出最小改动方案,优先复用现有组件、工具与目录结构 | ||
| 11 | -- 明确需要修改/新增的文件清单(按路径列出) | ||
| 12 | -- 明确数据流、状态管理方式(如需要 pinia/route query/组件本地状态) | ||
| 13 | -- 明确接口调用点与返回结构 { code, data, msg } 的处理策略 | ||
| 14 | -- 样式:优先 TailwindCSS 类名(项目已引入),Less 用于层级补充 | ||
| 15 | -- 不要启动或重启任何服务;不要引导打开预览 | ||
| 16 | - | ||
| 17 | -plan.md 输出结构: | ||
| 18 | -1) 总体方案 | ||
| 19 | -2) 页面与路由变更 | ||
| 20 | -3) 组件与复用点 | ||
| 21 | -4) 状态管理与数据流 | ||
| 22 | -5) 接口与错误处理 | ||
| 23 | -6) 样式与响应式策略 | ||
| 24 | -7) 影响范围与回归清单 |
| 1 | ---- | ||
| 2 | -description: "生成需求规格说明(spec)" | ||
| 3 | -agent: agent | ||
| 4 | -argument-hint: "feature_key 与需求描述(例如 001-xxx ...)" | ||
| 5 | ---- | ||
| 6 | - | ||
| 7 | -你需要把用户的需求整理成可评审的规格说明,输出到 specs/<feature_key>/spec.md。 | ||
| 8 | - | ||
| 9 | -约束: | ||
| 10 | -- 只写“做什么/为什么”,不要写实现细节 | ||
| 11 | -- 不确定的信息必须写成 [NEEDS CLARIFICATION],并给出具体要问的问题 | ||
| 12 | -- 规格说明使用中文,结构清晰,便于团队协作评审 | ||
| 13 | -- feature_key 使用数字前缀与短横线命名,例如 001-course-share-poster | ||
| 14 | - | ||
| 15 | -spec.md 模板: | ||
| 16 | - | ||
| 17 | -1) 背景与目标 | ||
| 18 | -- 背景: | ||
| 19 | -- 目标: | ||
| 20 | -- 非目标: | ||
| 21 | - | ||
| 22 | -2) 用户画像与使用场景 | ||
| 23 | - | ||
| 24 | -3) 需求范围 | ||
| 25 | -- 页面/入口: | ||
| 26 | -- 核心流程: | ||
| 27 | -- 状态与异常分支: | ||
| 28 | - | ||
| 29 | -4) 用户故事(User Stories) | ||
| 30 | -- 作为……我想……以便…… | ||
| 31 | - | ||
| 32 | -5) 验收标准(Acceptance Criteria) | ||
| 33 | -- 使用可验证的条件描述 | ||
| 34 | - | ||
| 35 | -6) 交互与视觉要点 | ||
| 36 | -- 移动端优先(如涉及 PC/移动差异需要说明) | ||
| 37 | - | ||
| 38 | -7) 数据与接口 | ||
| 39 | -- 需要的字段(仅描述数据含义,不给实现) | ||
| 40 | -- 返回结构固定为 { code, data, msg } | ||
| 41 | - | ||
| 42 | -8) 边界条件与风险 | ||
| 43 | - | ||
| 44 | -9) 待确认事项 | ||
| 45 | -- [NEEDS CLARIFICATION] ... |
| 1 | ---- | ||
| 2 | -description: "从方案拆分可执行任务(tasks)" | ||
| 3 | -agent: agent | ||
| 4 | -argument-hint: "feature_key(例如 001-xxx)" | ||
| 5 | ---- | ||
| 6 | - | ||
| 7 | -基于 specs/<feature_key>/plan.md,生成 specs/<feature_key>/tasks.md。 | ||
| 8 | - | ||
| 9 | -约束: | ||
| 10 | -- 任务要可执行、可验收,按顺序列出 | ||
| 11 | -- 每条任务不超过 14 个字 | ||
| 12 | -- 标注必要的回归点 | ||
| 13 | -- 不要包含“启动服务/打开预览”相关任务 | ||
| 14 | - | ||
| 15 | -tasks.md 格式: | ||
| 16 | -- [ ] 任务1 | ||
| 17 | -- [ ] 任务2 | ||
| 18 | - | ||
| 19 | -回归清单: | ||
| 20 | -- [ ] 回归项1 | ||
| 21 | -- [ ] 回归项2 |
.specify/memory/constitution.md
deleted
100644 → 0
| 1 | -# mlaj 宪法 | ||
| 2 | - | ||
| 3 | -## 核心原则 | ||
| 4 | - | ||
| 5 | -### 用户需求为中心 | ||
| 6 | - | ||
| 7 | -- 以用户需求为中心,优先实现最小可用闭环,避免过度设计 | ||
| 8 | - | ||
| 9 | -### 修改尽量小且可回滚 | ||
| 10 | - | ||
| 11 | -- 修改应尽量小且可回滚,避免大范围重构 | ||
| 12 | - | ||
| 13 | -### 风格一致与复用优先 | ||
| 14 | - | ||
| 15 | -- 保持现有代码风格与目录结构一致,优先复用已有工具与组件 | ||
| 16 | - | ||
| 17 | -### 生产代码完毕后不要重启服务 | ||
| 18 | - | ||
| 19 | -- 生产代码完毕后不要重启服务 | ||
| 20 | - | ||
| 21 | -### 操作完成后不要自动打开预览 | ||
| 22 | - | ||
| 23 | -- 操作完成后不要自动打开预览 | ||
| 24 | - | ||
| 25 | -## 技术与工程约束 | ||
| 26 | - | ||
| 27 | -- 依赖管理使用 pnpm | ||
| 28 | -- 构建工具使用 Vite | ||
| 29 | -- 前端使用 Vue3(优先 setup 语法糖) | ||
| 30 | -- 移动端 UI 优先使用 Vant | ||
| 31 | -- 样式使用 Less;如果项目已引入 TailwindCSS,布局优先用 Tailwind 类名,细节用 Less 补充 | ||
| 32 | -- 路由使用 vue-router | ||
| 33 | -- 状态管理使用 pinia(如项目已接入) | ||
| 34 | -- 不引入不必要的新库;如必须新增依赖,需要在方案中说明原因与替代方案 | ||
| 35 | - | ||
| 36 | -## 接口与数据约束 | ||
| 37 | - | ||
| 38 | -- 所有网络请求返回对象结构必须为 { code, data, msg } | ||
| 39 | -- code=1 表示成功,其他值表示失败;失败时保持 msg 可读 | ||
| 40 | - | ||
| 41 | -## 资源与链接约束 | ||
| 42 | - | ||
| 43 | -- 若图片域名为 cdn.ipadbiz.cn,需要自动追加 ?imageMogr2/thumbnail/200x/strip/quality/70 | ||
| 44 | - | ||
| 45 | -## 治理与管理 | ||
| 46 | - | ||
| 47 | -- 本宪法优先级高于临时约定与个人习惯;出现冲突时以本宪法为准 | ||
| 48 | -- 宪法变更应同步更新版本/日期信息,并确保对现有工程约束的兼容性 | ||
| 49 | - | ||
| 50 | -**版本**: [CONSTITUTION_VERSION] | **批准**: [RATIFICATION_DATE] | **最后修订**: [LAST_AMENDED_DATE] |
| 1 | -#!/usr/bin/env bash | ||
| 2 | - | ||
| 3 | -# 前置条件检查脚本(整合版) | ||
| 4 | -# | ||
| 5 | -# 本脚本为 Spec-Driven Development 工作流提供统一的前置条件检查, | ||
| 6 | -# 用于替代此前分散在多个脚本中的同类功能。 | ||
| 7 | -# | ||
| 8 | -# 用法:./check-prerequisites.sh [OPTIONS] | ||
| 9 | -# | ||
| 10 | -# OPTIONS: | ||
| 11 | -# --json 以 JSON 格式输出 | ||
| 12 | -# --require-tasks 要求 tasks.md 必须存在(实现阶段使用) | ||
| 13 | -# --include-tasks 在 AVAILABLE_DOCS 列表中包含 tasks.md | ||
| 14 | -# --paths-only 仅输出路径变量(不做校验) | ||
| 15 | -# --help, -h 显示帮助信息 | ||
| 16 | -# | ||
| 17 | -# 输出: | ||
| 18 | -# JSON 模式:{"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]} | ||
| 19 | -# 文本模式:FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md | ||
| 20 | -# 仅路径:REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... 等 | ||
| 21 | - | ||
| 22 | -set -e | ||
| 23 | - | ||
| 24 | -# 解析命令行参数 | ||
| 25 | -JSON_MODE=false | ||
| 26 | -REQUIRE_TASKS=false | ||
| 27 | -INCLUDE_TASKS=false | ||
| 28 | -PATHS_ONLY=false | ||
| 29 | - | ||
| 30 | -for arg in "$@"; do | ||
| 31 | - case "$arg" in | ||
| 32 | - --json) | ||
| 33 | - JSON_MODE=true | ||
| 34 | - ;; | ||
| 35 | - --require-tasks) | ||
| 36 | - REQUIRE_TASKS=true | ||
| 37 | - ;; | ||
| 38 | - --include-tasks) | ||
| 39 | - INCLUDE_TASKS=true | ||
| 40 | - ;; | ||
| 41 | - --paths-only) | ||
| 42 | - PATHS_ONLY=true | ||
| 43 | - ;; | ||
| 44 | - --help|-h) | ||
| 45 | - cat << 'EOF' | ||
| 46 | -用法:check-prerequisites.sh [OPTIONS] | ||
| 47 | - | ||
| 48 | -用于 Spec-Driven Development 工作流的前置条件检查(整合版)。 | ||
| 49 | - | ||
| 50 | -选项: | ||
| 51 | - --json 以 JSON 格式输出 | ||
| 52 | - --require-tasks 要求 tasks.md 必须存在(实现阶段使用) | ||
| 53 | - --include-tasks 在 AVAILABLE_DOCS 列表中包含 tasks.md | ||
| 54 | - --paths-only 仅输出路径变量(不做前置条件校验) | ||
| 55 | - --help, -h 显示帮助信息 | ||
| 56 | - | ||
| 57 | -示例: | ||
| 58 | - # 检查任务前置条件(需要 plan.md) | ||
| 59 | - ./check-prerequisites.sh --json | ||
| 60 | - | ||
| 61 | - # 检查实现前置条件(需要 plan.md + tasks.md) | ||
| 62 | - ./check-prerequisites.sh --json --require-tasks --include-tasks | ||
| 63 | - | ||
| 64 | - # 仅获取功能路径(不做校验) | ||
| 65 | - ./check-prerequisites.sh --paths-only | ||
| 66 | - | ||
| 67 | -EOF | ||
| 68 | - exit 0 | ||
| 69 | - ;; | ||
| 70 | - *) | ||
| 71 | - echo "错误:未知选项 '$arg'。使用 --help 查看用法。" >&2 | ||
| 72 | - exit 1 | ||
| 73 | - ;; | ||
| 74 | - esac | ||
| 75 | -done | ||
| 76 | - | ||
| 77 | -# 引入通用函数 | ||
| 78 | -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| 79 | -source "$SCRIPT_DIR/common.sh" | ||
| 80 | - | ||
| 81 | -# 获取功能路径并校验分支 | ||
| 82 | -eval $(get_feature_paths) | ||
| 83 | -check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 | ||
| 84 | - | ||
| 85 | -# 若为仅路径模式:输出路径后退出(支持 --json 与 --paths-only 组合) | ||
| 86 | -if $PATHS_ONLY; then | ||
| 87 | - if $JSON_MODE; then | ||
| 88 | - # 最小 JSON 路径负载(不做校验) | ||
| 89 | - printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ | ||
| 90 | - "$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS" | ||
| 91 | - else | ||
| 92 | - echo "REPO_ROOT: $REPO_ROOT" | ||
| 93 | - echo "BRANCH: $CURRENT_BRANCH" | ||
| 94 | - echo "FEATURE_DIR: $FEATURE_DIR" | ||
| 95 | - echo "FEATURE_SPEC: $FEATURE_SPEC" | ||
| 96 | - echo "IMPL_PLAN: $IMPL_PLAN" | ||
| 97 | - echo "TASKS: $TASKS" | ||
| 98 | - fi | ||
| 99 | - exit 0 | ||
| 100 | -fi | ||
| 101 | - | ||
| 102 | -# 校验必要的目录与文件 | ||
| 103 | -if [[ ! -d "$FEATURE_DIR" ]]; then | ||
| 104 | - echo "错误:未找到功能目录:$FEATURE_DIR" >&2 | ||
| 105 | - echo "请先运行 /speckit.specify 以创建功能目录结构。" >&2 | ||
| 106 | - exit 1 | ||
| 107 | -fi | ||
| 108 | - | ||
| 109 | -if [[ ! -f "$IMPL_PLAN" ]]; then | ||
| 110 | - echo "错误:在 $FEATURE_DIR 中未找到 plan.md" >&2 | ||
| 111 | - echo "请先运行 /speckit.plan 以创建实现计划。" >&2 | ||
| 112 | - exit 1 | ||
| 113 | -fi | ||
| 114 | - | ||
| 115 | -# 如果需要则检查 tasks.md | ||
| 116 | -if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then | ||
| 117 | - echo "错误:在 $FEATURE_DIR 中未找到 tasks.md" >&2 | ||
| 118 | - echo "请先运行 /speckit.tasks 以创建任务清单。" >&2 | ||
| 119 | - exit 1 | ||
| 120 | -fi | ||
| 121 | - | ||
| 122 | -# 构建可用文档列表 | ||
| 123 | -docs=() | ||
| 124 | - | ||
| 125 | -# 固定检查这些可选文档 | ||
| 126 | -[[ -f "$RESEARCH" ]] && docs+=("research.md") | ||
| 127 | -[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md") | ||
| 128 | - | ||
| 129 | -# 检查 contracts 目录(仅当存在且非空) | ||
| 130 | -if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then | ||
| 131 | - docs+=("contracts/") | ||
| 132 | -fi | ||
| 133 | - | ||
| 134 | -[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md") | ||
| 135 | - | ||
| 136 | -# 若要求包含 tasks.md,且文件存在,则加入列表 | ||
| 137 | -if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then | ||
| 138 | - docs+=("tasks.md") | ||
| 139 | -fi | ||
| 140 | - | ||
| 141 | -# 输出结果 | ||
| 142 | -if $JSON_MODE; then | ||
| 143 | - # 构建文档 JSON 数组 | ||
| 144 | - if [[ ${#docs[@]} -eq 0 ]]; then | ||
| 145 | - json_docs="[]" | ||
| 146 | - else | ||
| 147 | - json_docs=$(printf '"%s",' "${docs[@]}") | ||
| 148 | - json_docs="[${json_docs%,}]" | ||
| 149 | - fi | ||
| 150 | - | ||
| 151 | - printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs" | ||
| 152 | -else | ||
| 153 | - # 文本输出 | ||
| 154 | - echo "FEATURE_DIR:$FEATURE_DIR" | ||
| 155 | - echo "AVAILABLE_DOCS:" | ||
| 156 | - | ||
| 157 | - # 展示各文档的存在状态 | ||
| 158 | - check_file "$RESEARCH" "research.md" | ||
| 159 | - check_file "$DATA_MODEL" "data-model.md" | ||
| 160 | - check_dir "$CONTRACTS_DIR" "contracts/" | ||
| 161 | - check_file "$QUICKSTART" "quickstart.md" | ||
| 162 | - | ||
| 163 | - if $INCLUDE_TASKS; then | ||
| 164 | - check_file "$TASKS" "tasks.md" | ||
| 165 | - fi | ||
| 166 | -fi |
.specify/scripts/bash/common.sh
deleted
100755 → 0
| 1 | -#!/usr/bin/env bash | ||
| 2 | -# 所有脚本共用的函数与变量 | ||
| 3 | - | ||
| 4 | -# 获取仓库根目录(非 git 仓库时提供兜底) | ||
| 5 | -get_repo_root() { | ||
| 6 | - if git rev-parse --show-toplevel >/dev/null 2>&1; then | ||
| 7 | - git rev-parse --show-toplevel | ||
| 8 | - else | ||
| 9 | - # 非 git 仓库:回退为脚本所在目录向上查找 | ||
| 10 | - local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| 11 | - (cd "$script_dir/../../.." && pwd) | ||
| 12 | - fi | ||
| 13 | -} | ||
| 14 | - | ||
| 15 | -# 获取当前分支(非 git 仓库时提供兜底) | ||
| 16 | -get_current_branch() { | ||
| 17 | - # 优先使用 SPECIFY_FEATURE 环境变量 | ||
| 18 | - if [[ -n "${SPECIFY_FEATURE:-}" ]]; then | ||
| 19 | - echo "$SPECIFY_FEATURE" | ||
| 20 | - return | ||
| 21 | - fi | ||
| 22 | - | ||
| 23 | - # 其次尝试从 git 读取 | ||
| 24 | - if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then | ||
| 25 | - git rev-parse --abbrev-ref HEAD | ||
| 26 | - return | ||
| 27 | - fi | ||
| 28 | - | ||
| 29 | - # 非 git 仓库:尝试找到最新的功能目录 | ||
| 30 | - local repo_root=$(get_repo_root) | ||
| 31 | - local specs_dir="$repo_root/specs" | ||
| 32 | - | ||
| 33 | - if [[ -d "$specs_dir" ]]; then | ||
| 34 | - local latest_feature="" | ||
| 35 | - local highest=0 | ||
| 36 | - | ||
| 37 | - for dir in "$specs_dir"/*; do | ||
| 38 | - if [[ -d "$dir" ]]; then | ||
| 39 | - local dirname=$(basename "$dir") | ||
| 40 | - if [[ "$dirname" =~ ^([0-9]{3})- ]]; then | ||
| 41 | - local number=${BASH_REMATCH[1]} | ||
| 42 | - number=$((10#$number)) | ||
| 43 | - if [[ "$number" -gt "$highest" ]]; then | ||
| 44 | - highest=$number | ||
| 45 | - latest_feature=$dirname | ||
| 46 | - fi | ||
| 47 | - fi | ||
| 48 | - fi | ||
| 49 | - done | ||
| 50 | - | ||
| 51 | - if [[ -n "$latest_feature" ]]; then | ||
| 52 | - echo "$latest_feature" | ||
| 53 | - return | ||
| 54 | - fi | ||
| 55 | - fi | ||
| 56 | - | ||
| 57 | - echo "main" # 最终兜底 | ||
| 58 | -} | ||
| 59 | - | ||
| 60 | -# 检查是否为 git 仓库 | ||
| 61 | -has_git() { | ||
| 62 | - git rev-parse --show-toplevel >/dev/null 2>&1 | ||
| 63 | -} | ||
| 64 | - | ||
| 65 | -check_feature_branch() { | ||
| 66 | - local branch="$1" | ||
| 67 | - local has_git_repo="$2" | ||
| 68 | - | ||
| 69 | - # 非 git 仓库无法强制分支命名,但仍输出提示 | ||
| 70 | - if [[ "$has_git_repo" != "true" ]]; then | ||
| 71 | - echo "[specify] 警告:未检测到 Git 仓库;已跳过分支校验" >&2 | ||
| 72 | - return 0 | ||
| 73 | - fi | ||
| 74 | - | ||
| 75 | - if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then | ||
| 76 | - echo "错误:当前不在功能分支。当前分支:$branch" >&2 | ||
| 77 | - echo "功能分支命名应类似:001-feature-name" >&2 | ||
| 78 | - return 1 | ||
| 79 | - fi | ||
| 80 | - | ||
| 81 | - return 0 | ||
| 82 | -} | ||
| 83 | - | ||
| 84 | -get_feature_dir() { echo "$1/specs/$2"; } | ||
| 85 | - | ||
| 86 | -# 按数字前缀查找功能目录,而不是严格按分支名匹配 | ||
| 87 | -# 这样允许多个分支指向同一份规格(例如 004-fix-bug、004-add-feature) | ||
| 88 | -find_feature_dir_by_prefix() { | ||
| 89 | - local repo_root="$1" | ||
| 90 | - local branch_name="$2" | ||
| 91 | - local specs_dir="$repo_root/specs" | ||
| 92 | - | ||
| 93 | - # 从分支名提取数字前缀(例如从 "004-whatever" 提取 "004") | ||
| 94 | - if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then | ||
| 95 | - # 分支名不含数字前缀:回退为按分支名拼路径 | ||
| 96 | - echo "$specs_dir/$branch_name" | ||
| 97 | - return | ||
| 98 | - fi | ||
| 99 | - | ||
| 100 | - local prefix="${BASH_REMATCH[1]}" | ||
| 101 | - | ||
| 102 | - # 在 specs/ 下查找以该前缀开头的目录 | ||
| 103 | - local matches=() | ||
| 104 | - if [[ -d "$specs_dir" ]]; then | ||
| 105 | - for dir in "$specs_dir"/"$prefix"-*; do | ||
| 106 | - if [[ -d "$dir" ]]; then | ||
| 107 | - matches+=("$(basename "$dir")") | ||
| 108 | - fi | ||
| 109 | - done | ||
| 110 | - fi | ||
| 111 | - | ||
| 112 | - # 处理查找结果 | ||
| 113 | - if [[ ${#matches[@]} -eq 0 ]]; then | ||
| 114 | - # 未找到:返回按分支名拼的路径(后续会给出更明确的错误) | ||
| 115 | - echo "$specs_dir/$branch_name" | ||
| 116 | - elif [[ ${#matches[@]} -eq 1 ]]; then | ||
| 117 | - # 仅一个匹配:直接返回 | ||
| 118 | - echo "$specs_dir/${matches[0]}" | ||
| 119 | - else | ||
| 120 | - # 多个匹配:命名规范正确时不应发生 | ||
| 121 | - echo "错误:发现多个同前缀规格目录 '$prefix':${matches[*]}" >&2 | ||
| 122 | - echo "请确保每个数字前缀只对应一个规格目录。" >&2 | ||
| 123 | - echo "$specs_dir/$branch_name" # 返回一个路径以避免脚本中断 | ||
| 124 | - fi | ||
| 125 | -} | ||
| 126 | - | ||
| 127 | -get_feature_paths() { | ||
| 128 | - local repo_root=$(get_repo_root) | ||
| 129 | - local current_branch=$(get_current_branch) | ||
| 130 | - local has_git_repo="false" | ||
| 131 | - | ||
| 132 | - if has_git; then | ||
| 133 | - has_git_repo="true" | ||
| 134 | - fi | ||
| 135 | - | ||
| 136 | - # 使用前缀匹配以支持“多分支对应同一份规格”的场景 | ||
| 137 | - local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch") | ||
| 138 | - | ||
| 139 | - cat <<EOF | ||
| 140 | -REPO_ROOT='$repo_root' | ||
| 141 | -CURRENT_BRANCH='$current_branch' | ||
| 142 | -HAS_GIT='$has_git_repo' | ||
| 143 | -FEATURE_DIR='$feature_dir' | ||
| 144 | -FEATURE_SPEC='$feature_dir/spec.md' | ||
| 145 | -IMPL_PLAN='$feature_dir/plan.md' | ||
| 146 | -TASKS='$feature_dir/tasks.md' | ||
| 147 | -RESEARCH='$feature_dir/research.md' | ||
| 148 | -DATA_MODEL='$feature_dir/data-model.md' | ||
| 149 | -QUICKSTART='$feature_dir/quickstart.md' | ||
| 150 | -CONTRACTS_DIR='$feature_dir/contracts' | ||
| 151 | -EOF | ||
| 152 | -} | ||
| 153 | - | ||
| 154 | -check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; } | ||
| 155 | -check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; } |
| 1 | -#!/usr/bin/env bash | ||
| 2 | - | ||
| 3 | -set -e | ||
| 4 | - | ||
| 5 | -JSON_MODE=false | ||
| 6 | -SHORT_NAME="" | ||
| 7 | -BRANCH_NUMBER="" | ||
| 8 | -ARGS=() | ||
| 9 | -i=1 | ||
| 10 | -while [ $i -le $# ]; do | ||
| 11 | - arg="${!i}" | ||
| 12 | - case "$arg" in | ||
| 13 | - --json) | ||
| 14 | - JSON_MODE=true | ||
| 15 | - ;; | ||
| 16 | - --short-name) | ||
| 17 | - if [ $((i + 1)) -gt $# ]; then | ||
| 18 | - echo '错误:--short-name 需要提供值' >&2 | ||
| 19 | - exit 1 | ||
| 20 | - fi | ||
| 21 | - i=$((i + 1)) | ||
| 22 | - next_arg="${!i}" | ||
| 23 | - # 检查下一个参数是否为另一个选项(以 -- 开头) | ||
| 24 | - if [[ "$next_arg" == --* ]]; then | ||
| 25 | - echo '错误:--short-name 需要提供值' >&2 | ||
| 26 | - exit 1 | ||
| 27 | - fi | ||
| 28 | - SHORT_NAME="$next_arg" | ||
| 29 | - ;; | ||
| 30 | - --number) | ||
| 31 | - if [ $((i + 1)) -gt $# ]; then | ||
| 32 | - echo '错误:--number 需要提供值' >&2 | ||
| 33 | - exit 1 | ||
| 34 | - fi | ||
| 35 | - i=$((i + 1)) | ||
| 36 | - next_arg="${!i}" | ||
| 37 | - if [[ "$next_arg" == --* ]]; then | ||
| 38 | - echo '错误:--number 需要提供值' >&2 | ||
| 39 | - exit 1 | ||
| 40 | - fi | ||
| 41 | - BRANCH_NUMBER="$next_arg" | ||
| 42 | - ;; | ||
| 43 | - --help|-h) | ||
| 44 | - echo "用法:$0 [--json] [--short-name <短名>] [--number N] <功能描述>" | ||
| 45 | - echo "" | ||
| 46 | - echo "选项:" | ||
| 47 | - echo " --json 以 JSON 格式输出" | ||
| 48 | - echo " --short-name <短名> 为分支提供自定义短名(2-4 个单词)" | ||
| 49 | - echo " --number N 手动指定分支编号(覆盖自动检测)" | ||
| 50 | - echo " --help, -h 显示帮助信息" | ||
| 51 | - echo "" | ||
| 52 | - echo "示例:" | ||
| 53 | - echo " $0 '新增用户登录与鉴权能力' --short-name 'user-auth'" | ||
| 54 | - echo " $0 '为 API 接入 OAuth2 登录' --number 5" | ||
| 55 | - exit 0 | ||
| 56 | - ;; | ||
| 57 | - *) | ||
| 58 | - ARGS+=("$arg") | ||
| 59 | - ;; | ||
| 60 | - esac | ||
| 61 | - i=$((i + 1)) | ||
| 62 | -done | ||
| 63 | - | ||
| 64 | -FEATURE_DESCRIPTION="${ARGS[*]}" | ||
| 65 | -if [ -z "$FEATURE_DESCRIPTION" ]; then | ||
| 66 | - echo "用法:$0 [--json] [--short-name <短名>] [--number N] <功能描述>" >&2 | ||
| 67 | - exit 1 | ||
| 68 | -fi | ||
| 69 | - | ||
| 70 | -# 通过查找项目标记来定位仓库根目录 | ||
| 71 | -find_repo_root() { | ||
| 72 | - local dir="$1" | ||
| 73 | - while [ "$dir" != "/" ]; do | ||
| 74 | - if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then | ||
| 75 | - echo "$dir" | ||
| 76 | - return 0 | ||
| 77 | - fi | ||
| 78 | - dir="$(dirname "$dir")" | ||
| 79 | - done | ||
| 80 | - return 1 | ||
| 81 | -} | ||
| 82 | - | ||
| 83 | -# 从 specs 目录中获取最大编号 | ||
| 84 | -get_highest_from_specs() { | ||
| 85 | - local specs_dir="$1" | ||
| 86 | - local highest=0 | ||
| 87 | - | ||
| 88 | - if [ -d "$specs_dir" ]; then | ||
| 89 | - for dir in "$specs_dir"/*; do | ||
| 90 | - [ -d "$dir" ] || continue | ||
| 91 | - dirname=$(basename "$dir") | ||
| 92 | - number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0") | ||
| 93 | - number=$((10#$number)) | ||
| 94 | - if [ "$number" -gt "$highest" ]; then | ||
| 95 | - highest=$number | ||
| 96 | - fi | ||
| 97 | - done | ||
| 98 | - fi | ||
| 99 | - | ||
| 100 | - echo "$highest" | ||
| 101 | -} | ||
| 102 | - | ||
| 103 | -# 从 git 分支中获取最大编号 | ||
| 104 | -get_highest_from_branches() { | ||
| 105 | - local highest=0 | ||
| 106 | - | ||
| 107 | - # 获取所有分支(本地 + 远端) | ||
| 108 | - branches=$(git branch -a 2>/dev/null || echo "") | ||
| 109 | - | ||
| 110 | - if [ -n "$branches" ]; then | ||
| 111 | - while IFS= read -r branch; do | ||
| 112 | - # 清理分支名:移除前缀标记与远端前缀 | ||
| 113 | - clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||') | ||
| 114 | - | ||
| 115 | - # 若分支符合 ###-*,则提取功能编号 | ||
| 116 | - if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then | ||
| 117 | - number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0") | ||
| 118 | - number=$((10#$number)) | ||
| 119 | - if [ "$number" -gt "$highest" ]; then | ||
| 120 | - highest=$number | ||
| 121 | - fi | ||
| 122 | - fi | ||
| 123 | - done <<< "$branches" | ||
| 124 | - fi | ||
| 125 | - | ||
| 126 | - echo "$highest" | ||
| 127 | -} | ||
| 128 | - | ||
| 129 | -# 检查现有分支(本地 + 远端)并返回下一个可用编号 | ||
| 130 | -check_existing_branches() { | ||
| 131 | - local specs_dir="$1" | ||
| 132 | - | ||
| 133 | - # 拉取所有远端以获取最新分支信息(无远端时忽略错误) | ||
| 134 | - git fetch --all --prune 2>/dev/null || true | ||
| 135 | - | ||
| 136 | - # 获取所有分支中的最大编号(不局限于匹配 short name) | ||
| 137 | - local highest_branch=$(get_highest_from_branches) | ||
| 138 | - | ||
| 139 | - # 获取所有 specs 中的最大编号(不局限于匹配 short name) | ||
| 140 | - local highest_spec=$(get_highest_from_specs "$specs_dir") | ||
| 141 | - | ||
| 142 | - # 两者取最大值 | ||
| 143 | - local max_num=$highest_branch | ||
| 144 | - if [ "$highest_spec" -gt "$max_num" ]; then | ||
| 145 | - max_num=$highest_spec | ||
| 146 | - fi | ||
| 147 | - | ||
| 148 | - # 返回下一个编号 | ||
| 149 | - echo $((max_num + 1)) | ||
| 150 | -} | ||
| 151 | - | ||
| 152 | -# 清理并格式化分支名 | ||
| 153 | -clean_branch_name() { | ||
| 154 | - local name="$1" | ||
| 155 | - echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' | ||
| 156 | -} | ||
| 157 | - | ||
| 158 | -# 解析仓库根目录:优先使用 git 信息;如不可用则回退为查找仓库标记, | ||
| 159 | -# 以兼容使用 --no-git 初始化的仓库。 | ||
| 160 | -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| 161 | - | ||
| 162 | -if git rev-parse --show-toplevel >/dev/null 2>&1; then | ||
| 163 | - REPO_ROOT=$(git rev-parse --show-toplevel) | ||
| 164 | - HAS_GIT=true | ||
| 165 | -else | ||
| 166 | - REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")" | ||
| 167 | - if [ -z "$REPO_ROOT" ]; then | ||
| 168 | - echo "错误:无法确定仓库根目录。请在仓库内运行本脚本。" >&2 | ||
| 169 | - exit 1 | ||
| 170 | - fi | ||
| 171 | - HAS_GIT=false | ||
| 172 | -fi | ||
| 173 | - | ||
| 174 | -cd "$REPO_ROOT" | ||
| 175 | - | ||
| 176 | -SPECS_DIR="$REPO_ROOT/specs" | ||
| 177 | -mkdir -p "$SPECS_DIR" | ||
| 178 | - | ||
| 179 | -# 生成分支名:包含停用词过滤与长度过滤 | ||
| 180 | -generate_branch_name() { | ||
| 181 | - local description="$1" | ||
| 182 | - | ||
| 183 | - # 常见停用词(用于过滤) | ||
| 184 | - local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$" | ||
| 185 | - | ||
| 186 | - # 转小写并按单词拆分 | ||
| 187 | - local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g') | ||
| 188 | - | ||
| 189 | - # 过滤单词:移除停用词与长度 < 3 的词(除非在原描述中以大写出现,通常为缩写) | ||
| 190 | - local meaningful_words=() | ||
| 191 | - for word in $clean_name; do | ||
| 192 | - # 跳过空词 | ||
| 193 | - [ -z "$word" ] && continue | ||
| 194 | - | ||
| 195 | - # 保留:不属于停用词,并且(长度 >= 3 或为可能的缩写) | ||
| 196 | - if ! echo "$word" | grep -qiE "$stop_words"; then | ||
| 197 | - if [ ${#word} -ge 3 ]; then | ||
| 198 | - meaningful_words+=("$word") | ||
| 199 | - elif echo "$description" | grep -q "\b${word^^}\b"; then | ||
| 200 | - # 原描述中若以大写出现则保留(很可能是缩写) | ||
| 201 | - meaningful_words+=("$word") | ||
| 202 | - fi | ||
| 203 | - fi | ||
| 204 | - done | ||
| 205 | - | ||
| 206 | - # 若有有效词,则取前 3-4 个 | ||
| 207 | - if [ ${#meaningful_words[@]} -gt 0 ]; then | ||
| 208 | - local max_words=3 | ||
| 209 | - if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi | ||
| 210 | - | ||
| 211 | - local result="" | ||
| 212 | - local count=0 | ||
| 213 | - for word in "${meaningful_words[@]}"; do | ||
| 214 | - if [ $count -ge $max_words ]; then break; fi | ||
| 215 | - if [ -n "$result" ]; then result="$result-"; fi | ||
| 216 | - result="$result$word" | ||
| 217 | - count=$((count + 1)) | ||
| 218 | - done | ||
| 219 | - echo "$result" | ||
| 220 | - else | ||
| 221 | - # 若未找到有效词:回退为原始逻辑 | ||
| 222 | - local cleaned=$(clean_branch_name "$description") | ||
| 223 | - echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//' | ||
| 224 | - fi | ||
| 225 | -} | ||
| 226 | - | ||
| 227 | -# 生成分支名 | ||
| 228 | -if [ -n "$SHORT_NAME" ]; then | ||
| 229 | - # 使用用户提供的 short name,并做清理 | ||
| 230 | - BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME") | ||
| 231 | -else | ||
| 232 | - # 根据描述智能过滤生成 | ||
| 233 | - BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION") | ||
| 234 | -fi | ||
| 235 | - | ||
| 236 | -# 确定分支编号 | ||
| 237 | -if [ -z "$BRANCH_NUMBER" ]; then | ||
| 238 | - if [ "$HAS_GIT" = true ]; then | ||
| 239 | - # 检查远端已有分支 | ||
| 240 | - BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR") | ||
| 241 | - else | ||
| 242 | - # 回退为本地目录检查 | ||
| 243 | - HIGHEST=$(get_highest_from_specs "$SPECS_DIR") | ||
| 244 | - BRANCH_NUMBER=$((HIGHEST + 1)) | ||
| 245 | - fi | ||
| 246 | -fi | ||
| 247 | - | ||
| 248 | -# 强制按十进制解释,避免八进制转换(例如 010 在八进制会变为 8,但这里应是十进制 10) | ||
| 249 | -FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))") | ||
| 250 | -BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" | ||
| 251 | - | ||
| 252 | -# GitHub 对分支名有 244 字节限制:必要时校验并截断 | ||
| 253 | -MAX_BRANCH_LENGTH=244 | ||
| 254 | -if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then | ||
| 255 | - # 计算需要从后缀中截断的长度 | ||
| 256 | - # 分支名包含:功能编号(3)+ 连字符(1)= 4 个字符 | ||
| 257 | - MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4)) | ||
| 258 | - | ||
| 259 | - # 尽量按单词边界截断后缀 | ||
| 260 | - TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH) | ||
| 261 | - # 若截断产生了末尾连字符则移除 | ||
| 262 | - TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//') | ||
| 263 | - | ||
| 264 | - ORIGINAL_BRANCH_NAME="$BRANCH_NAME" | ||
| 265 | - BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}" | ||
| 266 | - | ||
| 267 | - >&2 echo "[specify] Warning: 分支名超过 GitHub 的 244 字节限制" | ||
| 268 | - >&2 echo "[specify] 原始:$ORIGINAL_BRANCH_NAME(${#ORIGINAL_BRANCH_NAME} 字节)" | ||
| 269 | - >&2 echo "[specify] 已截断为:$BRANCH_NAME(${#BRANCH_NAME} 字节)" | ||
| 270 | -fi | ||
| 271 | - | ||
| 272 | -if [ "$HAS_GIT" = true ]; then | ||
| 273 | - git checkout -b "$BRANCH_NAME" | ||
| 274 | -else | ||
| 275 | - >&2 echo "[specify] Warning: 未检测到 Git 仓库;已跳过创建分支 $BRANCH_NAME" | ||
| 276 | -fi | ||
| 277 | - | ||
| 278 | -FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" | ||
| 279 | -mkdir -p "$FEATURE_DIR" | ||
| 280 | - | ||
| 281 | -TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md" | ||
| 282 | -SPEC_FILE="$FEATURE_DIR/spec.md" | ||
| 283 | -if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi | ||
| 284 | - | ||
| 285 | -# 为当前会话设置 SPECIFY_FEATURE 环境变量 | ||
| 286 | -export SPECIFY_FEATURE="$BRANCH_NAME" | ||
| 287 | - | ||
| 288 | -if $JSON_MODE; then | ||
| 289 | - printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" | ||
| 290 | -else | ||
| 291 | - echo "BRANCH_NAME: $BRANCH_NAME" | ||
| 292 | - echo "SPEC_FILE: $SPEC_FILE" | ||
| 293 | - echo "FEATURE_NUM: $FEATURE_NUM" | ||
| 294 | - echo "已设置 SPECIFY_FEATURE 环境变量为:$BRANCH_NAME" | ||
| 295 | -fi |
.specify/scripts/bash/setup-plan.sh
deleted
100755 → 0
| 1 | -#!/usr/bin/env bash | ||
| 2 | - | ||
| 3 | -set -e | ||
| 4 | - | ||
| 5 | -# 解析命令行参数 | ||
| 6 | -JSON_MODE=false | ||
| 7 | -ARGS=() | ||
| 8 | - | ||
| 9 | -for arg in "$@"; do | ||
| 10 | - case "$arg" in | ||
| 11 | - --json) | ||
| 12 | - JSON_MODE=true | ||
| 13 | - ;; | ||
| 14 | - --help|-h) | ||
| 15 | - echo "用法:$0 [--json]" | ||
| 16 | - echo " --json 以 JSON 格式输出结果" | ||
| 17 | - echo " --help 显示帮助信息" | ||
| 18 | - exit 0 | ||
| 19 | - ;; | ||
| 20 | - *) | ||
| 21 | - ARGS+=("$arg") | ||
| 22 | - ;; | ||
| 23 | - esac | ||
| 24 | -done | ||
| 25 | - | ||
| 26 | -# 获取脚本目录并加载通用函数 | ||
| 27 | -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| 28 | -source "$SCRIPT_DIR/common.sh" | ||
| 29 | - | ||
| 30 | -# 从通用函数获取所有路径与变量 | ||
| 31 | -eval $(get_feature_paths) | ||
| 32 | - | ||
| 33 | -# 检查是否在合规的功能分支上(仅 git 仓库) | ||
| 34 | -check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 | ||
| 35 | - | ||
| 36 | -# 确保功能目录存在 | ||
| 37 | -mkdir -p "$FEATURE_DIR" | ||
| 38 | - | ||
| 39 | -# 如果存在计划模板,则复制 | ||
| 40 | -TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md" | ||
| 41 | -if [[ -f "$TEMPLATE" ]]; then | ||
| 42 | - cp "$TEMPLATE" "$IMPL_PLAN" | ||
| 43 | - echo "已复制 plan 模板到:$IMPL_PLAN" | ||
| 44 | -else | ||
| 45 | - echo "警告:未找到 plan 模板:$TEMPLATE" | ||
| 46 | - # 如果不存在模板,则创建空的 plan 文件 | ||
| 47 | - touch "$IMPL_PLAN" | ||
| 48 | -fi | ||
| 49 | - | ||
| 50 | -# 输出结果 | ||
| 51 | -if $JSON_MODE; then | ||
| 52 | - printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \ | ||
| 53 | - "$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT" | ||
| 54 | -else | ||
| 55 | - echo "FEATURE_SPEC: $FEATURE_SPEC" | ||
| 56 | - echo "IMPL_PLAN: $IMPL_PLAN" | ||
| 57 | - echo "SPECS_DIR: $FEATURE_DIR" | ||
| 58 | - echo "BRANCH: $CURRENT_BRANCH" | ||
| 59 | - echo "HAS_GIT: $HAS_GIT" | ||
| 60 | -fi |
This diff is collapsed. Click to expand it.
| 1 | -# [项目名称] 开发规范 | ||
| 2 | - | ||
| 3 | -从所有功能计划自动生成。最近更新:[DATE] | ||
| 4 | - | ||
| 5 | -## 当前技术栈 | ||
| 6 | - | ||
| 7 | -[从所有 plan.md 中提取] | ||
| 8 | - | ||
| 9 | -## 项目结构 | ||
| 10 | - | ||
| 11 | -```text | ||
| 12 | -[从计划中提取的真实结构] | ||
| 13 | -``` | ||
| 14 | - | ||
| 15 | -## 常用命令 | ||
| 16 | - | ||
| 17 | -[仅保留与当前技术栈相关的命令] | ||
| 18 | - | ||
| 19 | -## 代码风格 | ||
| 20 | - | ||
| 21 | -[语言相关规范(仅保留当前使用的语言)] | ||
| 22 | - | ||
| 23 | -## 最近变更 | ||
| 24 | - | ||
| 25 | -[最近 3 个功能及其变更] | ||
| 26 | - | ||
| 27 | -<!-- 手工补充开始 --> | ||
| 28 | -<!-- 手工补充结束 --> |
| 1 | -# [清单类型] 检查清单:[功能名称] | ||
| 2 | - | ||
| 3 | -**用途**:[本清单覆盖内容的简要说明] | ||
| 4 | -**创建时间**:[DATE] | ||
| 5 | -**关联功能**:[链接到 spec.md 或相关文档] | ||
| 6 | - | ||
| 7 | -**说明**:本清单由 `/speckit.checklist` 命令基于功能上下文与需求自动生成。 | ||
| 8 | - | ||
| 9 | -<!-- | ||
| 10 | - ============================================================================ | ||
| 11 | - 重要:以下检查项仅为示例,用于说明格式。 | ||
| 12 | - | ||
| 13 | - /speckit.checklist 命令必须基于以下信息,将这些示例替换为真实检查项: | ||
| 14 | - - 用户提出的具体检查清单诉求 | ||
| 15 | - - spec.md 中的功能需求 | ||
| 16 | - - plan.md 中的技术上下文 | ||
| 17 | - - tasks.md 中的实现细节 | ||
| 18 | - | ||
| 19 | - 生成的 checklist 文件中不要保留这些示例项。 | ||
| 20 | - ============================================================================ | ||
| 21 | ---> | ||
| 22 | - | ||
| 23 | -## [分类 1] | ||
| 24 | - | ||
| 25 | -- [ ] CHK001 第一条检查项(动作明确) | ||
| 26 | -- [ ] CHK002 第二条检查项 | ||
| 27 | -- [ ] CHK003 第三条检查项 | ||
| 28 | - | ||
| 29 | -## [分类 2] | ||
| 30 | - | ||
| 31 | -- [ ] CHK004 另一类检查项 | ||
| 32 | -- [ ] CHK005 带具体判定标准的检查项 | ||
| 33 | -- [ ] CHK006 本分类最后一条检查项 | ||
| 34 | - | ||
| 35 | -## 备注 | ||
| 36 | - | ||
| 37 | -- 完成后勾选:`[x]` | ||
| 38 | -- 发现问题可直接在行内补充说明 | ||
| 39 | -- 可添加链接指向相关资源或文档 | ||
| 40 | -- 条目按编号顺序排列,便于引用 |
.specify/templates/plan-template.md
deleted
100644 → 0
| 1 | -# 实现计划:[FEATURE] | ||
| 2 | - | ||
| 3 | -**分支**:`[###-feature-name]` | **日期**:[DATE] | **规格说明**:[link] | ||
| 4 | -**输入**:来自 `/specs/[###-feature-name]/spec.md` 的功能规格说明 | ||
| 5 | - | ||
| 6 | -**说明**:本模板由 `/speckit.plan` 命令填充。执行流程可参考 `.specify/templates/commands/plan.md`。 | ||
| 7 | - | ||
| 8 | -## 摘要 | ||
| 9 | - | ||
| 10 | -[从功能规格中提取:核心需求 +(结合 research 的)技术方案摘要] | ||
| 11 | - | ||
| 12 | -## 技术上下文 | ||
| 13 | - | ||
| 14 | -<!-- | ||
| 15 | - 必填:将本章节内容替换为项目的技术细节。 | ||
| 16 | - 这里的结构仅用于引导迭代过程,可按需调整。 | ||
| 17 | ---> | ||
| 18 | - | ||
| 19 | -**语言/版本**: [例如 Python 3.11、Swift 5.9、Rust 1.75 或 NEEDS CLARIFICATION] | ||
| 20 | -**主要依赖**: [例如 FastAPI、UIKit、LLVM 或 NEEDS CLARIFICATION] | ||
| 21 | -**存储**: [如适用,例如 PostgreSQL、CoreData、文件 或 N/A] | ||
| 22 | -**测试**: [例如 pytest、XCTest、cargo test 或 NEEDS CLARIFICATION] | ||
| 23 | -**目标平台**: [例如 Linux 服务器、iOS 15+、WASM 或 NEEDS CLARIFICATION] | ||
| 24 | -**工程类型**: [single/web/mobile - 决定源码结构] | ||
| 25 | -**性能目标**: [领域相关,例如 1000 req/s、10k lines/sec、60 fps 或 NEEDS CLARIFICATION] | ||
| 26 | -**约束**: [领域相关,例如 <200ms p95、<100MB 内存、支持离线 或 NEEDS CLARIFICATION] | ||
| 27 | -**规模/范围**: [领域相关,例如 10k 用户、1M LOC、50 个页面 或 NEEDS CLARIFICATION] | ||
| 28 | - | ||
| 29 | -## 宪法检查 | ||
| 30 | - | ||
| 31 | -*门禁:必须在阶段 0 调研前通过;在阶段 1 设计后需要复检。* | ||
| 32 | - | ||
| 33 | -[根据 constitution 文件确定门禁项] | ||
| 34 | - | ||
| 35 | - | ||
| 36 | -## 项目结构 | ||
| 37 | - | ||
| 38 | -### 文档(本功能) | ||
| 39 | - | ||
| 40 | -```text | ||
| 41 | -specs/[###-feature]/ | ||
| 42 | -├── plan.md # 本文件(/speckit.plan 输出) | ||
| 43 | -├── research.md # 阶段 0 输出(/speckit.plan) | ||
| 44 | -├── data-model.md # 阶段 1 输出(/speckit.plan) | ||
| 45 | -├── quickstart.md # 阶段 1 输出(/speckit.plan) | ||
| 46 | -├── contracts/ # 阶段 1 输出(/speckit.plan) | ||
| 47 | -└── tasks.md # 阶段 2 输出(/speckit.tasks - 不由 /speckit.plan 创建) | ||
| 48 | -``` | ||
| 49 | - | ||
| 50 | -### 源码(仓库根目录) | ||
| 51 | -<!-- | ||
| 52 | - 必填:将下方占位树替换为本功能对应的真实目录结构。 | ||
| 53 | - 删除未使用的选项,并将所选结构扩展为真实路径(例如 apps/admin、packages/xxx)。 | ||
| 54 | - 最终交付的 plan.md 不应包含 “选项” 字样。 | ||
| 55 | ---> | ||
| 56 | - | ||
| 57 | -```text | ||
| 58 | -# [不使用则删除] 选项 1:单体工程(默认) | ||
| 59 | -src/ | ||
| 60 | -├── models/ | ||
| 61 | -├── services/ | ||
| 62 | -├── cli/ | ||
| 63 | -└── lib/ | ||
| 64 | - | ||
| 65 | -tests/ | ||
| 66 | -├── contract/ | ||
| 67 | -├── integration/ | ||
| 68 | -└── unit/ | ||
| 69 | - | ||
| 70 | -# [不使用则删除] 选项 2:Web 应用(检测到 "frontend" + "backend" 时) | ||
| 71 | -backend/ | ||
| 72 | -├── src/ | ||
| 73 | -│ ├── models/ | ||
| 74 | -│ ├── services/ | ||
| 75 | -│ └── api/ | ||
| 76 | -└── tests/ | ||
| 77 | - | ||
| 78 | -frontend/ | ||
| 79 | -├── src/ | ||
| 80 | -│ ├── components/ | ||
| 81 | -│ ├── pages/ | ||
| 82 | -│ └── services/ | ||
| 83 | -└── tests/ | ||
| 84 | - | ||
| 85 | -# [不使用则删除] 选项 3:移动端 + API(检测到 "iOS/Android" 时) | ||
| 86 | -api/ | ||
| 87 | -└── [同 backend 的结构] | ||
| 88 | - | ||
| 89 | -ios/ 或 android/ | ||
| 90 | -└── [平台相关结构:功能模块、UI 流程、平台测试] | ||
| 91 | -``` | ||
| 92 | - | ||
| 93 | -**结构选择**: [说明最终选择的结构,并引用上面列出的真实目录] | ||
| 94 | - | ||
| 95 | -## 复杂度记录 | ||
| 96 | - | ||
| 97 | -> **仅在“宪法检查”存在需要解释的违规项时填写** | ||
| 98 | - | ||
| 99 | -| 违规项 | 必要性 | 为什么不能用更简单方案 | | ||
| 100 | -|--------|--------|--------------------------| | ||
| 101 | -| [例如:第 4 个子工程] | [当前需求] | [为何 3 个工程不够] | | ||
| 102 | -| [例如:Repository 模式] | [具体问题] | [为何不能直接访问数据库] | |
.specify/templates/spec-template.md
deleted
100644 → 0
| 1 | -# 功能规格说明:[功能名称] | ||
| 2 | - | ||
| 3 | -**功能分支**:`[###-feature-name]` | ||
| 4 | -**创建时间**:[DATE] | ||
| 5 | -**状态**:草稿 | ||
| 6 | -**输入**:用户描述:"$ARGUMENTS" | ||
| 7 | - | ||
| 8 | -## 用户场景与验收 *(必填)* | ||
| 9 | - | ||
| 10 | -<!-- | ||
| 11 | - 重要:用户故事必须按重要性排序,体现为“用户旅程”优先级。 | ||
| 12 | - 每个用户故事/旅程都必须可以独立验收——意味着即使只实现其中一个, | ||
| 13 | - 也应当形成可用的 MVP(最小可用产品)并能交付价值。 | ||
| 14 | - | ||
| 15 | - 为每个故事分配优先级(P1、P2、P3...),其中 P1 最关键。 | ||
| 16 | - 将每个故事视为一段可独立交付的功能切片,能够: | ||
| 17 | - - 独立开发 | ||
| 18 | - - 独立验收 | ||
| 19 | - - 独立部署 | ||
| 20 | - - 独立向用户演示 | ||
| 21 | ---> | ||
| 22 | - | ||
| 23 | -### 用户故事 1 - [简短标题](优先级:P1) | ||
| 24 | - | ||
| 25 | -[用通俗语言描述该用户旅程] | ||
| 26 | - | ||
| 27 | -**为什么是这个优先级**:[解释价值以及为何排在该优先级] | ||
| 28 | - | ||
| 29 | -**独立验收**:[描述如何独立验收——例如:“通过 [具体动作] 即可完整验收,并交付 [具体价值]”】【示例仅供参考】 | ||
| 30 | - | ||
| 31 | -**验收场景**: | ||
| 32 | - | ||
| 33 | -1. **假设** [初始状态],**当** [动作],**则** [期望结果] | ||
| 34 | -2. **假设** [初始状态],**当** [动作],**则** [期望结果] | ||
| 35 | - | ||
| 36 | ---- | ||
| 37 | - | ||
| 38 | -### 用户故事 2 - [简短标题](优先级:P2) | ||
| 39 | - | ||
| 40 | -[用通俗语言描述该用户旅程] | ||
| 41 | - | ||
| 42 | -**为什么是这个优先级**:[解释价值以及为何排在该优先级] | ||
| 43 | - | ||
| 44 | -**独立验收**:[描述如何独立验收] | ||
| 45 | - | ||
| 46 | -**验收场景**: | ||
| 47 | - | ||
| 48 | -1. **假设** [初始状态],**当** [动作],**则** [期望结果] | ||
| 49 | - | ||
| 50 | ---- | ||
| 51 | - | ||
| 52 | -### 用户故事 3 - [简短标题](优先级:P3) | ||
| 53 | - | ||
| 54 | -[用通俗语言描述该用户旅程] | ||
| 55 | - | ||
| 56 | -**为什么是这个优先级**:[解释价值以及为何排在该优先级] | ||
| 57 | - | ||
| 58 | -**独立验收**:[描述如何独立验收] | ||
| 59 | - | ||
| 60 | -**验收场景**: | ||
| 61 | - | ||
| 62 | -1. **假设** [初始状态],**当** [动作],**则** [期望结果] | ||
| 63 | - | ||
| 64 | ---- | ||
| 65 | - | ||
| 66 | -[按需补充更多用户故事,并为每个故事分配优先级] | ||
| 67 | - | ||
| 68 | -### 边界情况 | ||
| 69 | - | ||
| 70 | -<!-- | ||
| 71 | - 需要补全:本章节内容是占位符。 | ||
| 72 | - 请补充正确的边界情况。 | ||
| 73 | ---> | ||
| 74 | - | ||
| 75 | -- 当 [边界条件] 时会发生什么? | ||
| 76 | -- 系统如何处理 [错误场景]? | ||
| 77 | - | ||
| 78 | -## 需求 *(必填)* | ||
| 79 | - | ||
| 80 | -<!-- | ||
| 81 | - 需要补全:本章节内容是占位符。 | ||
| 82 | - 请补充正确的功能需求。 | ||
| 83 | ---> | ||
| 84 | - | ||
| 85 | -### 功能需求 | ||
| 86 | - | ||
| 87 | -- **FR-001**:系统必须 [具体能力,例如“允许用户创建账号”] | ||
| 88 | -- **FR-002**:系统必须 [具体能力,例如“校验邮箱地址”] | ||
| 89 | -- **FR-003**:用户必须能够 [关键交互,例如“重置密码”] | ||
| 90 | -- **FR-004**:系统必须 [数据要求,例如“持久化用户偏好”] | ||
| 91 | -- **FR-005**:系统必须 [行为要求,例如“记录所有安全事件”] | ||
| 92 | - | ||
| 93 | -*标记不清晰需求的示例:* | ||
| 94 | - | ||
| 95 | -- **FR-006**:系统必须通过 [NEEDS CLARIFICATION:认证方式未指定——邮箱/密码、SSO、OAuth?] 对用户进行认证 | ||
| 96 | -- **FR-007**:系统必须保留用户数据 [NEEDS CLARIFICATION:保留期限未指定] | ||
| 97 | - | ||
| 98 | -### 关键实体 *(当功能涉及数据时填写)* | ||
| 99 | - | ||
| 100 | -- **[实体 1]**:[表示什么;关键属性(不写实现细节)] | ||
| 101 | -- **[实体 2]**:[表示什么;与其他实体的关系] | ||
| 102 | - | ||
| 103 | -## 成功标准 *(必填)* | ||
| 104 | - | ||
| 105 | -<!-- | ||
| 106 | - 需要补全:定义可度量的成功标准。 | ||
| 107 | - 必须与技术实现无关,并且可度量。 | ||
| 108 | ---> | ||
| 109 | - | ||
| 110 | -### 可度量结果 | ||
| 111 | - | ||
| 112 | -- **SC-001**:[可度量指标,例如“用户可在 2 分钟内完成账号创建”】【示例仅供参考】 | ||
| 113 | -- **SC-002**:[可度量指标,例如“系统在 1000 并发用户下无明显劣化”】【示例仅供参考】 | ||
| 114 | -- **SC-003**:[用户满意度指标,例如“90% 的用户首次尝试即可完成主任务”】【示例仅供参考】 | ||
| 115 | -- **SC-004**:[业务指标,例如“将与 [X] 相关的支持工单减少 50%”】【示例仅供参考】 |
.specify/templates/tasks-template.md
deleted
100644 → 0
| 1 | ---- | ||
| 2 | - | ||
| 3 | -description: "功能实现的任务清单模板" | ||
| 4 | ---- | ||
| 5 | - | ||
| 6 | -# 任务清单:[功能名称] | ||
| 7 | - | ||
| 8 | -**输入**:来自 `/specs/[###-feature-name]/` 的设计与规格文档 | ||
| 9 | -**前置条件**:plan.md(必需)、spec.md(用户故事必需)、research.md、data-model.md、contracts/ | ||
| 10 | - | ||
| 11 | -**测试**:下方示例包含测试任务。测试是可选的——仅在功能规格中明确要求时才需要包含。 | ||
| 12 | - | ||
| 13 | -**组织方式**:任务按用户故事分组,保证每个故事都能独立实现与独立验证。 | ||
| 14 | - | ||
| 15 | -## 格式:`[ID] [P?] [Story] 描述` | ||
| 16 | - | ||
| 17 | -- **[P]**:可并行执行(不同文件、无依赖) | ||
| 18 | -- **[Story]**:任务所属用户故事(例如 US1、US2、US3) | ||
| 19 | -- 描述中包含准确的文件路径 | ||
| 20 | - | ||
| 21 | -## 路径约定 | ||
| 22 | - | ||
| 23 | -- **单体工程**:仓库根目录下 `src/`、`tests/` | ||
| 24 | -- **Web 应用**:`backend/src/`、`frontend/src/` | ||
| 25 | -- **移动端**:`api/src/`、`ios/src/` 或 `android/src/` | ||
| 26 | -- 下方展示默认按“单体工程”组织;请根据 plan.md 的结构调整 | ||
| 27 | - | ||
| 28 | -<!-- | ||
| 29 | - ============================================================================ | ||
| 30 | - 重要:以下任务仅为示例,用于说明结构与写法。 | ||
| 31 | - | ||
| 32 | - /speckit.tasks 命令必须基于以下信息,将这些示例替换为真实任务: | ||
| 33 | - - spec.md 的用户故事(含优先级 P1、P2、P3...) | ||
| 34 | - - plan.md 的功能与实现要求 | ||
| 35 | - - data-model.md 的实体 | ||
| 36 | - - contracts/ 的接口定义 | ||
| 37 | - | ||
| 38 | - 任务必须按用户故事组织,使每个故事都能: | ||
| 39 | - - 独立实现 | ||
| 40 | - - 独立验证 | ||
| 41 | - - 作为 MVP 增量交付 | ||
| 42 | - | ||
| 43 | - 生成的 tasks.md 文件中不要保留这些示例任务。 | ||
| 44 | - ============================================================================ | ||
| 45 | ---> | ||
| 46 | - | ||
| 47 | -## 阶段 1:准备(共享基础设施) | ||
| 48 | - | ||
| 49 | -**目标**:项目初始化与基础结构搭建 | ||
| 50 | - | ||
| 51 | -- [ ] T001 按实现计划创建项目结构 | ||
| 52 | -- [ ] T002 初始化 [language] 工程并安装 [framework] 依赖 | ||
| 53 | -- [ ] T003 [P] 配置 lint 与格式化工具 | ||
| 54 | - | ||
| 55 | ---- | ||
| 56 | - | ||
| 57 | -## 阶段 2:基础(阻塞性前置条件) | ||
| 58 | - | ||
| 59 | -**目标**:核心基础设施(任何用户故事开始前必须完成) | ||
| 60 | - | ||
| 61 | -**⚠️ 关键**:在本阶段完成前,不允许开始任何用户故事的工作 | ||
| 62 | - | ||
| 63 | -基础任务示例(请根据项目实际情况调整): | ||
| 64 | - | ||
| 65 | -- [ ] T004 建立数据库结构与迁移机制 | ||
| 66 | -- [ ] T005 [P] 实现认证/鉴权框架 | ||
| 67 | -- [ ] T006 [P] 建立 API 路由与中间件结构 | ||
| 68 | -- [ ] T007 创建所有故事依赖的基础模型/实体 | ||
| 69 | -- [ ] T008 配置错误处理与日志基础设施 | ||
| 70 | -- [ ] T009 建立环境配置管理 | ||
| 71 | - | ||
| 72 | -**检查点**:基础设施就绪——用户故事实现可以并行推进 | ||
| 73 | - | ||
| 74 | ---- | ||
| 75 | - | ||
| 76 | -## 阶段 3:用户故事 1 - [标题](优先级:P1)🎯 MVP | ||
| 77 | - | ||
| 78 | -**目标**:[简述该故事交付内容] | ||
| 79 | - | ||
| 80 | -**独立验证**:[如何单独验证该故事可用] | ||
| 81 | - | ||
| 82 | -### 用户故事 1 的测试(可选:仅在明确要求测试时)⚠️ | ||
| 83 | - | ||
| 84 | -> **注意:先写测试,并确保在实现前测试会失败** | ||
| 85 | - | ||
| 86 | -- [ ] T010 [P] [US1] 为 [endpoint] 编写契约测试:tests/contract/test_[name].py | ||
| 87 | -- [ ] T011 [P] [US1] 为 [用户旅程] 编写集成测试:tests/integration/test_[name].py | ||
| 88 | - | ||
| 89 | -### 用户故事 1 的实现 | ||
| 90 | - | ||
| 91 | -- [ ] T012 [P] [US1] 创建 [Entity1] 模型:src/models/[entity1].py | ||
| 92 | -- [ ] T013 [P] [US1] 创建 [Entity2] 模型:src/models/[entity2].py | ||
| 93 | -- [ ] T014 [US1] 实现 [Service]:src/services/[service].py(依赖 T012、T013) | ||
| 94 | -- [ ] T015 [US1] 实现 [endpoint/feature]:src/[location]/[file].py | ||
| 95 | -- [ ] T016 [US1] 增加校验与错误处理 | ||
| 96 | -- [ ] T017 [US1] 为用户故事 1 的关键操作补充日志 | ||
| 97 | - | ||
| 98 | -**检查点**:此时用户故事 1 应完整可用,且可独立验证 | ||
| 99 | - | ||
| 100 | ---- | ||
| 101 | - | ||
| 102 | -## 阶段 4:用户故事 2 - [标题](优先级:P2) | ||
| 103 | - | ||
| 104 | -**目标**:[简述该故事交付内容] | ||
| 105 | - | ||
| 106 | -**独立验证**:[如何单独验证该故事可用] | ||
| 107 | - | ||
| 108 | -### 用户故事 2 的测试(可选:仅在明确要求测试时)⚠️ | ||
| 109 | - | ||
| 110 | -- [ ] T018 [P] [US2] 为 [endpoint] 编写契约测试:tests/contract/test_[name].py | ||
| 111 | -- [ ] T019 [P] [US2] 为 [用户旅程] 编写集成测试:tests/integration/test_[name].py | ||
| 112 | - | ||
| 113 | -### 用户故事 2 的实现 | ||
| 114 | - | ||
| 115 | -- [ ] T020 [P] [US2] 创建 [Entity] 模型:src/models/[entity].py | ||
| 116 | -- [ ] T021 [US2] 实现 [Service]:src/services/[service].py | ||
| 117 | -- [ ] T022 [US2] 实现 [endpoint/feature]:src/[location]/[file].py | ||
| 118 | -- [ ] T023 [US2] 与用户故事 1 的组件集成(如需要) | ||
| 119 | - | ||
| 120 | -**检查点**:此时用户故事 1 和 2 应都能独立工作 | ||
| 121 | - | ||
| 122 | ---- | ||
| 123 | - | ||
| 124 | -## 阶段 5:用户故事 3 - [标题](优先级:P3) | ||
| 125 | - | ||
| 126 | -**目标**:[简述该故事交付内容] | ||
| 127 | - | ||
| 128 | -**独立验证**:[如何单独验证该故事可用] | ||
| 129 | - | ||
| 130 | -### 用户故事 3 的测试(可选:仅在明确要求测试时)⚠️ | ||
| 131 | - | ||
| 132 | -- [ ] T024 [P] [US3] 为 [endpoint] 编写契约测试:tests/contract/test_[name].py | ||
| 133 | -- [ ] T025 [P] [US3] 为 [用户旅程] 编写集成测试:tests/integration/test_[name].py | ||
| 134 | - | ||
| 135 | -### 用户故事 3 的实现 | ||
| 136 | - | ||
| 137 | -- [ ] T026 [P] [US3] 创建 [Entity] 模型:src/models/[entity].py | ||
| 138 | -- [ ] T027 [US3] 实现 [Service]:src/services/[service].py | ||
| 139 | -- [ ] T028 [US3] 实现 [endpoint/feature]:src/[location]/[file].py | ||
| 140 | - | ||
| 141 | -**检查点**:此时所有用户故事都应可独立运行 | ||
| 142 | - | ||
| 143 | ---- | ||
| 144 | - | ||
| 145 | -[如需要,可按同样格式继续添加更多用户故事阶段] | ||
| 146 | - | ||
| 147 | ---- | ||
| 148 | - | ||
| 149 | -## 阶段 N:打磨与横切关注点 | ||
| 150 | - | ||
| 151 | -**目标**:影响多个用户故事的改进项 | ||
| 152 | - | ||
| 153 | -- [ ] TXXX [P] 更新 docs/ 内的文档 | ||
| 154 | -- [ ] TXXX 代码清理与重构 | ||
| 155 | -- [ ] TXXX 跨故事的性能优化 | ||
| 156 | -- [ ] TXXX [P] 在 tests/unit/ 增加单测(如需) | ||
| 157 | -- [ ] TXXX 安全加固 | ||
| 158 | -- [ ] TXXX 验证 quickstart.md | ||
| 159 | - | ||
| 160 | ---- | ||
| 161 | - | ||
| 162 | -## 依赖与执行顺序 | ||
| 163 | - | ||
| 164 | -### 阶段依赖 | ||
| 165 | - | ||
| 166 | -- **准备(阶段 1)**:无依赖,可立即开始 | ||
| 167 | -- **基础(阶段 2)**:依赖准备阶段完成——阻塞所有用户故事 | ||
| 168 | -- **用户故事(阶段 3+)**:均依赖基础阶段完成 | ||
| 169 | - - 之后可并行推进(若有人力) | ||
| 170 | - - 或按优先级串行推进(P1 → P2 → P3) | ||
| 171 | -- **打磨(最终阶段)**:依赖所有目标用户故事完成 | ||
| 172 | - | ||
| 173 | -### 用户故事依赖 | ||
| 174 | - | ||
| 175 | -- **用户故事 1(P1)**:基础阶段后可开始——不依赖其他故事 | ||
| 176 | -- **用户故事 2(P2)**:基础阶段后可开始——可能与 US1 集成,但应保持可独立验证 | ||
| 177 | -- **用户故事 3(P3)**:基础阶段后可开始——可能与 US1/US2 集成,但应保持可独立验证 | ||
| 178 | - | ||
| 179 | -### 每个用户故事内部顺序 | ||
| 180 | - | ||
| 181 | -- 测试(如包含)必须先写,且在实现前应失败 | ||
| 182 | -- 先模型,再服务 | ||
| 183 | -- 先服务,再接口/端点 | ||
| 184 | -- 先核心实现,再集成 | ||
| 185 | -- 完成当前故事后再进入下一个优先级 | ||
| 186 | - | ||
| 187 | -### 并行机会 | ||
| 188 | - | ||
| 189 | -- 阶段 1 中标记 [P] 的任务可并行 | ||
| 190 | -- 阶段 2 中标记 [P] 的任务可并行(在阶段 2 内) | ||
| 191 | -- 基础阶段完成后,所有用户故事可并行开始(若团队容量允许) | ||
| 192 | -- 用户故事内标记 [P] 的测试可并行 | ||
| 193 | -- 用户故事内标记 [P] 的模型可并行 | ||
| 194 | -- 不同用户故事可由不同成员并行推进 | ||
| 195 | - | ||
| 196 | ---- | ||
| 197 | - | ||
| 198 | -## 并行示例:用户故事 1 | ||
| 199 | - | ||
| 200 | -```bash | ||
| 201 | -# 一次性启动用户故事 1 的所有测试(如需要测试): | ||
| 202 | -任务: "为 [endpoint] 编写契约测试:tests/contract/test_[name].py" | ||
| 203 | -任务: "为 [用户旅程] 编写集成测试:tests/integration/test_[name].py" | ||
| 204 | - | ||
| 205 | -# 一次性启动用户故事 1 的所有模型任务: | ||
| 206 | -任务: "创建 [Entity1] 模型:src/models/[entity1].py" | ||
| 207 | -任务: "创建 [Entity2] 模型:src/models/[entity2].py" | ||
| 208 | -``` | ||
| 209 | - | ||
| 210 | ---- | ||
| 211 | - | ||
| 212 | -## 实施策略 | ||
| 213 | - | ||
| 214 | -### MVP 优先(仅用户故事 1) | ||
| 215 | - | ||
| 216 | -1. 完成阶段 1:准备 | ||
| 217 | -2. 完成阶段 2:基础(关键:阻塞所有故事) | ||
| 218 | -3. 完成阶段 3:用户故事 1 | ||
| 219 | -4. **停止并验证**:独立验证用户故事 1 | ||
| 220 | -5. 如已就绪则部署/演示 | ||
| 221 | - | ||
| 222 | -### 增量交付 | ||
| 223 | - | ||
| 224 | -1. 完成准备 + 基础 → 基础设施就绪 | ||
| 225 | -2. 增加用户故事 1 → 独立验证 → 部署/演示(MVP) | ||
| 226 | -3. 增加用户故事 2 → 独立验证 → 部署/演示 | ||
| 227 | -4. 增加用户故事 3 → 独立验证 → 部署/演示 | ||
| 228 | -5. 每个故事都应在不破坏既有能力的前提下增量交付价值 | ||
| 229 | - | ||
| 230 | -### 多人并行策略 | ||
| 231 | - | ||
| 232 | -多人协作时: | ||
| 233 | - | ||
| 234 | -1. 团队协作完成准备 + 基础 | ||
| 235 | -2. 基础完成后: | ||
| 236 | - - 开发 A:用户故事 1 | ||
| 237 | - - 开发 B:用户故事 2 | ||
| 238 | - - 开发 C:用户故事 3 | ||
| 239 | -3. 各故事独立完成并独立集成 | ||
| 240 | - | ||
| 241 | ---- | ||
| 242 | - | ||
| 243 | -## 备注 | ||
| 244 | - | ||
| 245 | -- [P] 任务:不同文件、无依赖,可并行 | ||
| 246 | -- [Story] 标签:将任务映射到具体用户故事,便于追溯 | ||
| 247 | -- 每个用户故事都应可独立完成与独立验证 | ||
| 248 | -- 实现前先确认测试会失败(如有测试) | ||
| 249 | -- 每完成一个任务或合理的任务组再提交 | ||
| 250 | -- 在任意检查点都可以停下来做独立验证 | ||
| 251 | -- 避免:任务描述模糊、同文件冲突、跨故事强耦合导致无法独立交付 |
.trae/rules/update-changelog.md
deleted
100644 → 0
| 1 | -对话处理完成一个任务后,需要更新changelog.md文件,添加一个新的版本号,版本号的格式为YYYY-MM-DD,版本号的内容为任务的描述. |
| 1 | ---- | ||
| 2 | -name: vue-best-practices | ||
| 3 | -description: Vue 3 TypeScript, vue-tsc, Volar, Vite, component props, testing, composition API. | ||
| 4 | ---- | ||
| 5 | - | ||
| 6 | -# Vue Best Practices | ||
| 7 | - | ||
| 8 | -## Capability Rules | ||
| 9 | - | ||
| 10 | -| Rule | Keywords | Description | | ||
| 11 | -|------|----------|-------------| | ||
| 12 | -| [vue-tsc-strict-templates](rules/vue-tsc-strict-templates.md) | undefined component, template error, strictTemplates | Catch undefined components in templates | | ||
| 13 | -| [fallthrough-attributes](rules/fallthrough-attributes.md) | fallthrough, $attrs, wrapper component | Type-check fallthrough attributes | | ||
| 14 | -| [strict-css-modules](rules/strict-css-modules.md) | css modules, $style, typo | Catch CSS module class typos | | ||
| 15 | -| [data-attributes-config](rules/data-attributes-config.md) | data-*, strictTemplates, attribute | Allow data-* attributes | | ||
| 16 | -| [volar-3-breaking-changes](rules/volar-3-breaking-changes.md) | volar, vue-language-server, editor | Fix Volar 3.0 upgrade issues | | ||
| 17 | -| [module-resolution-bundler](rules/module-resolution-bundler.md) | cannot find module, @vue/tsconfig, moduleResolution | Fix module resolution errors | | ||
| 18 | -| [unplugin-auto-import-conflicts](rules/unplugin-auto-import-conflicts.md) | unplugin, auto-import, types any | Fix unplugin type conflicts | | ||
| 19 | -| [codeactions-save-performance](rules/codeactions-save-performance.md) | slow save, vscode, performance | Fix slow save in large projects | | ||
| 20 | -| [duplicate-plugin-detection](rules/duplicate-plugin-detection.md) | duplicate plugin, vite, vue plugin | Detect duplicate plugins | | ||
| 21 | -| [define-model-update-event](rules/define-model-update-event.md) | defineModel, update event, undefined | Fix model update errors | | ||
| 22 | -| [with-defaults-union-types](rules/with-defaults-union-types.md) | withDefaults, union type, default | Fix union type defaults | | ||
| 23 | -| [deep-watch-numeric](rules/deep-watch-numeric.md) | watch, deep, array, Vue 3.5 | Efficient array watching | | ||
| 24 | -| [vue-directive-comments](rules/vue-directive-comments.md) | @vue-ignore, @vue-skip, template | Control template type checking | | ||
| 25 | -| [script-setup-jsdoc](rules/script-setup-jsdoc.md) | jsdoc, script setup, documentation | Add JSDoc to script setup | | ||
| 26 | -| [vue-router-typed-params](rules/vue-router-typed-params.md) | route params, typed router, unplugin | Fix route params typing | | ||
| 27 | - | ||
| 28 | -## Efficiency Rules | ||
| 29 | - | ||
| 30 | -| Rule | Keywords | Description | | ||
| 31 | -|------|----------|-------------| | ||
| 32 | -| [hmr-vue-ssr](rules/hmr-vue-ssr.md) | hmr, ssr, hot reload | Fix HMR in SSR apps | | ||
| 33 | -| [pinia-store-mocking](rules/pinia-store-mocking.md) | pinia, mock, vitest, store | Mock Pinia stores | | ||
| 34 | - | ||
| 35 | -## Reference | ||
| 36 | - | ||
| 37 | -- [Vue Language Tools](https://github.com/vuejs/language-tools) | ||
| 38 | -- [Vue 3 Documentation](https://vuejs.org/) | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 1 | ---- | ||
| 2 | -title: Fix Slow Save Times with Code Actions Setting | ||
| 3 | -impact: HIGH | ||
| 4 | -impactDescription: fixes 30-60 second save delays in large Vue projects | ||
| 5 | -type: capability | ||
| 6 | -tags: performance, save-time, vscode, code-actions, volar | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# Fix Slow Save Times with Code Actions Setting | ||
| 10 | - | ||
| 11 | -**Impact: HIGH** - fixes 30-60 second save delays in large Vue projects | ||
| 12 | - | ||
| 13 | -In large Vue projects, saving files can take 30-60+ seconds due to VSCode's code actions triggering expensive TypeScript state synchronization. | ||
| 14 | - | ||
| 15 | -## Problem | ||
| 16 | - | ||
| 17 | -Symptoms: | ||
| 18 | -- Save operation takes 30+ seconds | ||
| 19 | -- Editor becomes unresponsive during save | ||
| 20 | -- CPU spikes when saving Vue files | ||
| 21 | -- Happens more in larger projects | ||
| 22 | - | ||
| 23 | -## Root Cause | ||
| 24 | - | ||
| 25 | -VSCode emits document change events multiple times during save cycles. Each event triggers Volar to synchronize with TypeScript, causing expensive re-computation. | ||
| 26 | - | ||
| 27 | -## Solution | ||
| 28 | - | ||
| 29 | -Disable code actions or limit their timeout: | ||
| 30 | - | ||
| 31 | -**Option 1: Disable code actions (fastest)** | ||
| 32 | -```json | ||
| 33 | -// .vscode/settings.json | ||
| 34 | -{ | ||
| 35 | - "vue.codeActions.enabled": false | ||
| 36 | -} | ||
| 37 | -``` | ||
| 38 | - | ||
| 39 | -**Option 2: Limit code action time** | ||
| 40 | -```json | ||
| 41 | -// .vscode/settings.json | ||
| 42 | -{ | ||
| 43 | - "vue.codeActions.savingTimeLimit": 1000 | ||
| 44 | -} | ||
| 45 | -``` | ||
| 46 | - | ||
| 47 | -**Option 3: Disable specific code actions** | ||
| 48 | -```json | ||
| 49 | -// .vscode/settings.json | ||
| 50 | -{ | ||
| 51 | - "vue.codeActions.enabled": true, | ||
| 52 | - "editor.codeActionsOnSave": { | ||
| 53 | - "source.organizeImports": "never" | ||
| 54 | - } | ||
| 55 | -} | ||
| 56 | -``` | ||
| 57 | - | ||
| 58 | -## VSCode Version Requirement | ||
| 59 | - | ||
| 60 | -VSCode 1.81.0+ includes fixes that reduce save time issues. Upgrade if using an older version. | ||
| 61 | - | ||
| 62 | -## Additional Optimizations | ||
| 63 | - | ||
| 64 | -```json | ||
| 65 | -// .vscode/settings.json | ||
| 66 | -{ | ||
| 67 | - "vue.codeActions.enabled": false, | ||
| 68 | - "editor.formatOnSave": true, | ||
| 69 | - "editor.codeActionsOnSave": {}, | ||
| 70 | - "[vue]": { | ||
| 71 | - "editor.formatOnSave": true, | ||
| 72 | - "editor.defaultFormatter": "Vue.volar" | ||
| 73 | - } | ||
| 74 | -} | ||
| 75 | -``` | ||
| 76 | - | ||
| 77 | -## Reference | ||
| 78 | - | ||
| 79 | -- [Vue Language Tools Discussion #2740](https://github.com/vuejs/language-tools/discussions/2740) |
| 1 | ---- | ||
| 2 | -title: Allow Data Attributes with Strict Templates | ||
| 3 | -impact: MEDIUM | ||
| 4 | -impactDescription: fixes data-testid and data-* attribute errors in strict mode | ||
| 5 | -type: capability | ||
| 6 | -tags: dataAttributes, vueCompilerOptions, strictTemplates, data-testid, testing | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# Allow Data Attributes with Strict Templates | ||
| 10 | - | ||
| 11 | -**Impact: MEDIUM** - fixes data-testid and data-* attribute errors in strict mode | ||
| 12 | - | ||
| 13 | -With `strictTemplates` enabled, `data-*` attributes on components cause type errors. Use the `dataAttributes` option to allow specific patterns. | ||
| 14 | - | ||
| 15 | -## Problem | ||
| 16 | - | ||
| 17 | -```vue | ||
| 18 | -<template> | ||
| 19 | - <!-- Error: Property 'data-testid' does not exist on type... --> | ||
| 20 | - <MyComponent data-testid="submit-button" /> | ||
| 21 | - | ||
| 22 | - <!-- Error: Property 'data-cy' does not exist on type... --> | ||
| 23 | - <MyComponent data-cy="login-form" /> | ||
| 24 | -</template> | ||
| 25 | -``` | ||
| 26 | - | ||
| 27 | -## Solution | ||
| 28 | - | ||
| 29 | -Configure `dataAttributes` to allow specific patterns: | ||
| 30 | - | ||
| 31 | -```json | ||
| 32 | -// tsconfig.json or tsconfig.app.json | ||
| 33 | -{ | ||
| 34 | - "vueCompilerOptions": { | ||
| 35 | - "strictTemplates": true, | ||
| 36 | - "dataAttributes": ["data-*"] | ||
| 37 | - } | ||
| 38 | -} | ||
| 39 | -``` | ||
| 40 | - | ||
| 41 | -Now all `data-*` attributes are allowed on any component. | ||
| 42 | - | ||
| 43 | -## Specific Patterns | ||
| 44 | - | ||
| 45 | -You can be more selective: | ||
| 46 | - | ||
| 47 | -```json | ||
| 48 | -{ | ||
| 49 | - "vueCompilerOptions": { | ||
| 50 | - "dataAttributes": [ | ||
| 51 | - "data-testid", | ||
| 52 | - "data-cy", | ||
| 53 | - "data-test-*" | ||
| 54 | - ] | ||
| 55 | - } | ||
| 56 | -} | ||
| 57 | -``` | ||
| 58 | - | ||
| 59 | -This only allows the specified patterns, not all data attributes. | ||
| 60 | - | ||
| 61 | -## Common Testing Attributes | ||
| 62 | - | ||
| 63 | -For testing libraries, allow their specific attributes: | ||
| 64 | - | ||
| 65 | -| Library | Attribute | Pattern | | ||
| 66 | -|---------|-----------|---------| | ||
| 67 | -| Testing Library | `data-testid` | `"data-testid"` | | ||
| 68 | -| Cypress | `data-cy` | `"data-cy"` | | ||
| 69 | -| Playwright | `data-testid` | `"data-testid"` | | ||
| 70 | -| Generic | All data attributes | `"data-*"` | | ||
| 71 | - | ||
| 72 | -## Reference | ||
| 73 | - | ||
| 74 | -- [Vue Language Tools Wiki - Vue Compiler Options](https://github.com/vuejs/language-tools/wiki/Vue-Compiler-Options) |
| 1 | ---- | ||
| 2 | -title: Vue 3.5+ Deep Watch Numeric Depth | ||
| 3 | -impact: MEDIUM | ||
| 4 | -impactDescription: enables efficient array mutation watching with numeric deep option | ||
| 5 | -type: capability | ||
| 6 | -tags: watch, deep, vue-3.5, array, mutation, performance | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# Vue 3.5+ Deep Watch Numeric Depth | ||
| 10 | - | ||
| 11 | -**Impact: MEDIUM** - enables efficient array mutation watching with numeric deep option | ||
| 12 | - | ||
| 13 | -Vue 3.5 introduced `deep: number` for watch depth control. This allows watching array mutations without the performance cost of deep traversal. | ||
| 14 | - | ||
| 15 | -## Symptoms | ||
| 16 | - | ||
| 17 | -- Array mutations not triggering watch callback | ||
| 18 | -- Deep watch causing performance issues on large nested objects | ||
| 19 | -- Unaware of new Vue 3.5 feature | ||
| 20 | - | ||
| 21 | -> **Note:** TypeScript error "Type 'number' is not assignable to type 'boolean'" no longer occurs with Vue 3.5+ and current TypeScript versions. The types now correctly support numeric `deep` values. | ||
| 22 | - | ||
| 23 | -## The Feature | ||
| 24 | - | ||
| 25 | -```typescript | ||
| 26 | -// Vue 3.5+ only | ||
| 27 | -watch(items, (newVal) => { | ||
| 28 | - // Triggered on array mutations (push, pop, splice, etc.) | ||
| 29 | -}, { deep: 1 }) | ||
| 30 | -``` | ||
| 31 | - | ||
| 32 | -| deep value | Behavior | | ||
| 33 | -|------------|----------| | ||
| 34 | -| `true` | Full recursive traversal (original behavior) | | ||
| 35 | -| `false` | Only reference changes | | ||
| 36 | -| `1` | One level deep - array mutations, not nested objects | | ||
| 37 | -| `2` | Two levels deep | | ||
| 38 | -| `n` | N levels deep | | ||
| 39 | - | ||
| 40 | -## Fix | ||
| 41 | - | ||
| 42 | -**Step 1: Ensure Vue 3.5+** | ||
| 43 | -```bash | ||
| 44 | -npm install vue@^3.5.0 | ||
| 45 | -``` | ||
| 46 | - | ||
| 47 | -**Step 2: Update @vue/runtime-core types** | ||
| 48 | -```bash | ||
| 49 | -npm install -D @vue/runtime-core@latest | ||
| 50 | -``` | ||
| 51 | - | ||
| 52 | -**Step 3: Use numeric depth** | ||
| 53 | -```typescript | ||
| 54 | -import { watch, ref } from 'vue' | ||
| 55 | - | ||
| 56 | -const items = ref([{ id: 1, data: { nested: 'value' } }]) | ||
| 57 | - | ||
| 58 | -// Watch array mutations only (push, pop, etc.) | ||
| 59 | -watch(items, (newItems) => { | ||
| 60 | - console.log('Array mutated') | ||
| 61 | -}, { deep: 1 }) | ||
| 62 | - | ||
| 63 | -// Won't trigger on: items.value[0].data.nested = 'new' | ||
| 64 | -// Will trigger on: items.value.push(newItem) | ||
| 65 | -``` | ||
| 66 | - | ||
| 67 | -## Performance Comparison | ||
| 68 | - | ||
| 69 | -```typescript | ||
| 70 | -const largeNestedData = ref({ /* deeply nested structure */ }) | ||
| 71 | - | ||
| 72 | -// SLOW - traverses entire structure | ||
| 73 | -watch(largeNestedData, handler, { deep: true }) | ||
| 74 | - | ||
| 75 | -// FAST - only watches top-level changes | ||
| 76 | -watch(largeNestedData, handler, { deep: 1 }) | ||
| 77 | - | ||
| 78 | -// FASTEST - only reference changes | ||
| 79 | -watch(largeNestedData, handler, { deep: false }) | ||
| 80 | -``` | ||
| 81 | - | ||
| 82 | -## Alternative: watchEffect for Selective Tracking | ||
| 83 | - | ||
| 84 | -```typescript | ||
| 85 | -// Only tracks properties actually accessed | ||
| 86 | -watchEffect(() => { | ||
| 87 | - // Only re-runs when items.value.length or first item changes | ||
| 88 | - console.log(items.value.length, items.value[0]?.id) | ||
| 89 | -}) | ||
| 90 | -``` | ||
| 91 | - | ||
| 92 | -## TypeScript Note | ||
| 93 | - | ||
| 94 | -If TypeScript complains about numeric deep, ensure: | ||
| 95 | -1. Vue version is 3.5+ | ||
| 96 | -2. `@vue/runtime-core` types are updated | ||
| 97 | -3. tsconfig targets correct node_modules types | ||
| 98 | - | ||
| 99 | -## Reference | ||
| 100 | - | ||
| 101 | -- [Vue Watchers Docs](https://vuejs.org/guide/essentials/watchers.html) | ||
| 102 | -- [Vue 3.5 Release Notes](https://blog.vuejs.org/posts/vue-3-5) |
| 1 | ---- | ||
| 2 | -title: defineModel Fires Update Event with Undefined | ||
| 3 | -impact: MEDIUM | ||
| 4 | -impactDescription: fixes runtime errors from unexpected undefined in model updates | ||
| 5 | -type: capability | ||
| 6 | -tags: defineModel, v-model, update-event, undefined, vue-3.5 | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# defineModel Fires Update Event with Undefined | ||
| 10 | - | ||
| 11 | -**Impact: MEDIUM** - fixes runtime errors from unexpected undefined in model updates | ||
| 12 | - | ||
| 13 | -> **Version Note (2025):** This issue may be resolved in Vue 3.5+. Testing with Vue 3.5.26 could not reproduce the double emission with `undefined`. If you're on Vue 3.5+, verify the issue exists in your specific scenario before applying workarounds. | ||
| 14 | - | ||
| 15 | -Components using `defineModel` may fire the `@update:model-value` event with `undefined` in certain edge cases. TypeScript types don't always reflect this behavior, potentially causing runtime errors when the parent expects a non-nullable value. | ||
| 16 | - | ||
| 17 | -## Symptoms | ||
| 18 | - | ||
| 19 | -- Parent component receives `undefined` unexpectedly | ||
| 20 | -- Runtime error: "Cannot read property of undefined" | ||
| 21 | -- Type mismatch between expected `T` and received `T | undefined` | ||
| 22 | -- Issue appears when clearing/resetting the model value | ||
| 23 | - | ||
| 24 | -## Root Cause | ||
| 25 | - | ||
| 26 | -`defineModel` returns `Ref<T | undefined>` by default, even when `T` is non-nullable. The update event can fire with `undefined` when: | ||
| 27 | -- Component unmounts | ||
| 28 | -- Model is explicitly cleared | ||
| 29 | -- Internal state resets | ||
| 30 | - | ||
| 31 | -## Fix | ||
| 32 | - | ||
| 33 | -**Option 1: Use required option (Vue 3.5+)** | ||
| 34 | -```typescript | ||
| 35 | -// Returns Ref<Item> instead of Ref<Item | undefined> | ||
| 36 | -const model = defineModel<Item>({ required: true }) | ||
| 37 | -``` | ||
| 38 | - | ||
| 39 | -**Option 2: Type parent handler to accept undefined** | ||
| 40 | -```vue | ||
| 41 | -<template> | ||
| 42 | - <MyComponent | ||
| 43 | - v-model="item" | ||
| 44 | - @update:model-value="handleUpdate" | ||
| 45 | - /> | ||
| 46 | -</template> | ||
| 47 | - | ||
| 48 | -<script setup lang="ts"> | ||
| 49 | -// Handle both value and undefined | ||
| 50 | -const handleUpdate = (value: Item | undefined) => { | ||
| 51 | - if (value !== undefined) { | ||
| 52 | - item.value = value | ||
| 53 | - } | ||
| 54 | -} | ||
| 55 | -</script> | ||
| 56 | -``` | ||
| 57 | - | ||
| 58 | -**Option 3: Use default value in defineModel** | ||
| 59 | -```typescript | ||
| 60 | -const model = defineModel<string>({ default: '' }) | ||
| 61 | -``` | ||
| 62 | - | ||
| 63 | -## Type Declaration Pattern | ||
| 64 | - | ||
| 65 | -```typescript | ||
| 66 | -// In child component | ||
| 67 | -interface Props { | ||
| 68 | - modelValue: Item | ||
| 69 | -} | ||
| 70 | -const model = defineModel<Item>({ required: true }) | ||
| 71 | - | ||
| 72 | -// Emits will be typed as (value: Item) not (value: Item | undefined) | ||
| 73 | -``` | ||
| 74 | - | ||
| 75 | -## Reference | ||
| 76 | - | ||
| 77 | -- [vuejs/core#12817](https://github.com/vuejs/core/issues/12817) | ||
| 78 | -- [vuejs/core#10103](https://github.com/vuejs/core/issues/10103) | ||
| 79 | -- [defineModel docs](https://vuejs.org/api/sfc-script-setup.html#definemodel) |
| 1 | ---- | ||
| 2 | -title: Duplicate Vue Plugin Detection | ||
| 3 | -impact: MEDIUM | ||
| 4 | -impactDescription: fixes cryptic build errors from Vue plugin registered twice | ||
| 5 | -type: capability | ||
| 6 | -tags: vite, plugin, vue, duplicate, config, inline | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# Duplicate Vue Plugin Detection | ||
| 10 | - | ||
| 11 | -**Impact: MEDIUM** - fixes cryptic build errors from Vue plugin registered twice | ||
| 12 | - | ||
| 13 | -When using Vite's JavaScript API, if the Vue plugin is loaded in `vite.config.js` and specified again in `inlineConfig`, it gets registered twice, causing cryptic build errors. | ||
| 14 | - | ||
| 15 | -## Symptoms | ||
| 16 | - | ||
| 17 | -- Build produces unexpected output or fails silently | ||
| 18 | -- "Cannot read property of undefined" during build | ||
| 19 | -- Different build behavior between CLI and JavaScript API | ||
| 20 | -- Vue components render incorrectly after build | ||
| 21 | - | ||
| 22 | -## Root Cause | ||
| 23 | - | ||
| 24 | -Vite doesn't deduplicate plugins by name when merging configs. The Vue plugin's internal state gets corrupted when registered twice. | ||
| 25 | - | ||
| 26 | -## Fix | ||
| 27 | - | ||
| 28 | -**Option 1: Use configFile: false with inline plugins** | ||
| 29 | -```typescript | ||
| 30 | -import { build } from 'vite' | ||
| 31 | -import vue from '@vitejs/plugin-vue' | ||
| 32 | - | ||
| 33 | -await build({ | ||
| 34 | - configFile: false, // Don't load vite.config.js | ||
| 35 | - plugins: [vue()], | ||
| 36 | - // ... rest of config | ||
| 37 | -}) | ||
| 38 | -``` | ||
| 39 | - | ||
| 40 | -**Option 2: Don't specify plugins in inlineConfig** | ||
| 41 | -```typescript | ||
| 42 | -// vite.config.js already has vue plugin | ||
| 43 | -import { build } from 'vite' | ||
| 44 | - | ||
| 45 | -await build({ | ||
| 46 | - // Don't add vue plugin here - it's in vite.config.js | ||
| 47 | - root: './src', | ||
| 48 | - build: { outDir: '../dist' } | ||
| 49 | -}) | ||
| 50 | -``` | ||
| 51 | - | ||
| 52 | -**Option 3: Filter out Vue plugin before merging** | ||
| 53 | -```typescript | ||
| 54 | -import { build, loadConfigFromFile } from 'vite' | ||
| 55 | -import vue from '@vitejs/plugin-vue' | ||
| 56 | - | ||
| 57 | -const { config } = await loadConfigFromFile({ command: 'build', mode: 'production' }) | ||
| 58 | - | ||
| 59 | -// Remove existing Vue plugin | ||
| 60 | -const filteredPlugins = config.plugins?.filter( | ||
| 61 | - p => !p || (Array.isArray(p) ? false : p.name !== 'vite:vue') | ||
| 62 | -) || [] | ||
| 63 | - | ||
| 64 | -await build({ | ||
| 65 | - ...config, | ||
| 66 | - plugins: [...filteredPlugins, vue({ /* your options */ })] | ||
| 67 | -}) | ||
| 68 | -``` | ||
| 69 | - | ||
| 70 | -## Detection Script | ||
| 71 | - | ||
| 72 | -Add this to debug plugin registration: | ||
| 73 | -```typescript | ||
| 74 | -// vite.config.ts | ||
| 75 | -export default defineConfig({ | ||
| 76 | - plugins: [ | ||
| 77 | - vue(), | ||
| 78 | - { | ||
| 79 | - name: 'debug-plugins', | ||
| 80 | - configResolved(config) { | ||
| 81 | - const vuePlugins = config.plugins.filter(p => p.name?.includes('vue')) | ||
| 82 | - if (vuePlugins.length > 1) { | ||
| 83 | - console.warn('WARNING: Multiple Vue plugins detected:', vuePlugins.map(p => p.name)) | ||
| 84 | - } | ||
| 85 | - } | ||
| 86 | - } | ||
| 87 | - ] | ||
| 88 | -}) | ||
| 89 | -``` | ||
| 90 | - | ||
| 91 | -## Common Scenarios | ||
| 92 | - | ||
| 93 | -| Scenario | Solution | | ||
| 94 | -|----------|----------| | ||
| 95 | -| Using `vite.createServer()` | Use `configFile: false` | | ||
| 96 | -| Build script with custom config | Don't duplicate plugins | | ||
| 97 | -| Monorepo with shared config | Check for plugin inheritance | | ||
| 98 | - | ||
| 99 | -## Reference | ||
| 100 | - | ||
| 101 | -- [Vite Issue #5335](https://github.com/vitejs/vite/issues/5335) | ||
| 102 | -- [Vite JavaScript API](https://vite.dev/guide/api-javascript.html) |
| 1 | ---- | ||
| 2 | -title: Enable Fallthrough Attributes Type Checking | ||
| 3 | -impact: HIGH | ||
| 4 | -impactDescription: enables type-safe fallthrough attributes in component libraries | ||
| 5 | -type: capability | ||
| 6 | -tags: fallthroughAttributes, vueCompilerOptions, component-library, wrapper-components | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# Enable Fallthrough Attributes Type Checking | ||
| 10 | - | ||
| 11 | -**Impact: MEDIUM** - enables type-aware attribute forwarding in component libraries | ||
| 12 | - | ||
| 13 | -When building component libraries with wrapper components, enable `fallthroughAttributes` to get IDE autocomplete for attributes that will be forwarded to child elements. | ||
| 14 | - | ||
| 15 | -## What It Does | ||
| 16 | - | ||
| 17 | -Wrapper components that pass attributes to child elements can benefit from type-aware completion: | ||
| 18 | - | ||
| 19 | -```vue | ||
| 20 | -<!-- MyButton.vue - wrapper around native button --> | ||
| 21 | -<template> | ||
| 22 | - <button v-bind="$attrs"><slot /></button> | ||
| 23 | -</template> | ||
| 24 | -``` | ||
| 25 | - | ||
| 26 | -## Solution | ||
| 27 | - | ||
| 28 | -Enable `fallthroughAttributes` in your tsconfig: | ||
| 29 | - | ||
| 30 | -```json | ||
| 31 | -// tsconfig.json or tsconfig.app.json | ||
| 32 | -{ | ||
| 33 | - "vueCompilerOptions": { | ||
| 34 | - "fallthroughAttributes": true | ||
| 35 | - } | ||
| 36 | -} | ||
| 37 | -``` | ||
| 38 | - | ||
| 39 | -## How It Works | ||
| 40 | - | ||
| 41 | -When `fallthroughAttributes: true`: | ||
| 42 | -- Vue Language Server analyzes which element receives `$attrs` | ||
| 43 | -- IDE autocomplete suggests valid attributes for the target element | ||
| 44 | -- Helps developers discover available attributes | ||
| 45 | - | ||
| 46 | -> **Note:** This primarily enables IDE autocomplete for valid fallthrough attributes. It does NOT reject invalid attributes as type errors - arbitrary attributes are still allowed. | ||
| 47 | - | ||
| 48 | -## Related Options | ||
| 49 | - | ||
| 50 | -Combine with `strictTemplates` for comprehensive checking: | ||
| 51 | - | ||
| 52 | -```json | ||
| 53 | -{ | ||
| 54 | - "vueCompilerOptions": { | ||
| 55 | - "strictTemplates": true, | ||
| 56 | - "fallthroughAttributes": true | ||
| 57 | - } | ||
| 58 | -} | ||
| 59 | -``` | ||
| 60 | - | ||
| 61 | -## Reference | ||
| 62 | - | ||
| 63 | -- [Vue Language Tools Wiki - Vue Compiler Options](https://github.com/vuejs/language-tools/wiki/Vue-Compiler-Options) |
| 1 | ---- | ||
| 2 | -title: HMR Debugging for Vue SSR | ||
| 3 | -impact: MEDIUM | ||
| 4 | -impactDescription: fixes Hot Module Replacement breaking in Vue SSR applications | ||
| 5 | -type: efficiency | ||
| 6 | -tags: vite, hmr, ssr, vue, hot-reload, server-side-rendering | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# HMR Debugging for Vue SSR | ||
| 10 | - | ||
| 11 | -**Impact: MEDIUM** - fixes Hot Module Replacement breaking in Vue SSR applications | ||
| 12 | - | ||
| 13 | -Hot Module Replacement breaks when modifying Vue component `<script setup>` sections in SSR applications. Changes cause errors instead of smooth updates, requiring full page reloads. | ||
| 14 | - | ||
| 15 | -## Symptoms | ||
| 16 | - | ||
| 17 | -- HMR works for `<template>` changes but breaks for `<script setup>` | ||
| 18 | -- "Cannot read property of undefined" after saving | ||
| 19 | -- Full page reload required after script changes | ||
| 20 | -- HMR works in dev:client but not dev:ssr | ||
| 21 | - | ||
| 22 | -## Root Cause | ||
| 23 | - | ||
| 24 | -SSR mode has a different transformation pipeline. The Vue plugin's HMR boundary detection doesn't handle SSR modules the same way as client modules. | ||
| 25 | - | ||
| 26 | -## Fix | ||
| 27 | - | ||
| 28 | -**Step 1: Ensure correct SSR plugin configuration** | ||
| 29 | -```typescript | ||
| 30 | -// vite.config.ts | ||
| 31 | -import { defineConfig } from 'vite' | ||
| 32 | -import vue from '@vitejs/plugin-vue' | ||
| 33 | - | ||
| 34 | -export default defineConfig({ | ||
| 35 | - plugins: [vue()], | ||
| 36 | - ssr: { | ||
| 37 | - // Don't externalize these for HMR to work | ||
| 38 | - noExternal: ['vue', '@vue/runtime-core', '@vue/runtime-dom'] | ||
| 39 | - } | ||
| 40 | -}) | ||
| 41 | -``` | ||
| 42 | - | ||
| 43 | -**Step 2: Configure dev server for SSR HMR** | ||
| 44 | -```typescript | ||
| 45 | -// server.ts | ||
| 46 | -import { createServer } from 'vite' | ||
| 47 | - | ||
| 48 | -const vite = await createServer({ | ||
| 49 | - server: { middlewareMode: true }, | ||
| 50 | - appType: 'custom' | ||
| 51 | -}) | ||
| 52 | - | ||
| 53 | -// Use vite.ssrLoadModule for server-side imports | ||
| 54 | -const { render } = await vite.ssrLoadModule('/src/entry-server.ts') | ||
| 55 | - | ||
| 56 | -// Handle HMR | ||
| 57 | -vite.watcher.on('change', async (file) => { | ||
| 58 | - if (file.endsWith('.vue')) { | ||
| 59 | - // Invalidate the module | ||
| 60 | - const mod = vite.moduleGraph.getModuleById(file) | ||
| 61 | - if (mod) { | ||
| 62 | - vite.moduleGraph.invalidateModule(mod) | ||
| 63 | - } | ||
| 64 | - } | ||
| 65 | -}) | ||
| 66 | -``` | ||
| 67 | - | ||
| 68 | -**Step 3: Add HMR acceptance in entry-server** | ||
| 69 | -```typescript | ||
| 70 | -// entry-server.ts | ||
| 71 | -import { createApp } from './main' | ||
| 72 | - | ||
| 73 | -export async function render(url: string) { | ||
| 74 | - const app = createApp() | ||
| 75 | - // ... render logic | ||
| 76 | -} | ||
| 77 | - | ||
| 78 | -// Accept HMR updates | ||
| 79 | -if (import.meta.hot) { | ||
| 80 | - import.meta.hot.accept() | ||
| 81 | -} | ||
| 82 | -``` | ||
| 83 | - | ||
| 84 | -## Framework-Specific Solutions | ||
| 85 | - | ||
| 86 | -### Nuxt 3 | ||
| 87 | -HMR should work out of the box. If not: | ||
| 88 | -```bash | ||
| 89 | -rm -rf .nuxt node_modules/.vite | ||
| 90 | -npm install | ||
| 91 | -npm run dev | ||
| 92 | -``` | ||
| 93 | - | ||
| 94 | -### Vite SSR Template | ||
| 95 | -Ensure you're using the latest `@vitejs/plugin-vue`: | ||
| 96 | -```bash | ||
| 97 | -npm install @vitejs/plugin-vue@latest | ||
| 98 | -``` | ||
| 99 | - | ||
| 100 | -## Debugging | ||
| 101 | - | ||
| 102 | -Enable verbose HMR logging: | ||
| 103 | -```typescript | ||
| 104 | -// vite.config.ts | ||
| 105 | -export default defineConfig({ | ||
| 106 | - server: { | ||
| 107 | - hmr: { | ||
| 108 | - overlay: true | ||
| 109 | - } | ||
| 110 | - }, | ||
| 111 | - logLevel: 'info' // Shows HMR updates | ||
| 112 | -}) | ||
| 113 | -``` | ||
| 114 | - | ||
| 115 | -## Known Limitations | ||
| 116 | - | ||
| 117 | -- HMR for `<script>` (not `<script setup>`) may require full reload | ||
| 118 | -- SSR components with external dependencies may not hot-reload | ||
| 119 | -- State is not preserved for SSR components (expected behavior) | ||
| 120 | - | ||
| 121 | -## Reference | ||
| 122 | - | ||
| 123 | -- [vite-plugin-vue#525](https://github.com/vitejs/vite-plugin-vue/issues/525) | ||
| 124 | -- [Vite SSR Guide](https://vite.dev/guide/ssr.html) |
| 1 | ---- | ||
| 2 | -title: moduleResolution Bundler Migration Issues | ||
| 3 | -impact: HIGH | ||
| 4 | -impactDescription: fixes "Cannot find module" errors after @vue/tsconfig upgrade | ||
| 5 | -type: capability | ||
| 6 | -tags: moduleResolution, bundler, tsconfig, vue-tsconfig, node, esm | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# moduleResolution Bundler Migration Issues | ||
| 10 | - | ||
| 11 | -**Impact: HIGH** - fixes "Cannot find module" errors after @vue/tsconfig upgrade | ||
| 12 | - | ||
| 13 | -Recent versions of `@vue/tsconfig` changed `moduleResolution` from `"node"` to `"bundler"`. This can break existing projects with errors like "Cannot find module 'vue'" or issues with `resolveJsonModule`. | ||
| 14 | - | ||
| 15 | -## Symptoms | ||
| 16 | - | ||
| 17 | -- `Cannot find module 'vue'` or other packages | ||
| 18 | -- `Option '--resolveJsonModule' cannot be specified without 'node' module resolution` | ||
| 19 | -- Errors appear after updating `@vue/tsconfig` | ||
| 20 | -- Some third-party packages no longer resolve | ||
| 21 | - | ||
| 22 | -## Root Cause | ||
| 23 | - | ||
| 24 | -`moduleResolution: "bundler"` requires: | ||
| 25 | -1. TypeScript 5.0+ | ||
| 26 | -2. Packages to have proper `exports` field in package.json | ||
| 27 | -3. Different resolution rules than Node.js classic resolution | ||
| 28 | - | ||
| 29 | -## Fix | ||
| 30 | - | ||
| 31 | -**Option 1: Ensure TypeScript 5.0+ everywhere** | ||
| 32 | -```bash | ||
| 33 | -npm install -D typescript@^5.0.0 | ||
| 34 | -``` | ||
| 35 | - | ||
| 36 | -In monorepos, ALL packages must use TypeScript 5.0+. | ||
| 37 | - | ||
| 38 | -**Option 2: Add compatibility workaround** | ||
| 39 | -```json | ||
| 40 | -{ | ||
| 41 | - "compilerOptions": { | ||
| 42 | - "module": "ESNext", | ||
| 43 | - "moduleResolution": "bundler", | ||
| 44 | - "resolvePackageJsonExports": false | ||
| 45 | - } | ||
| 46 | -} | ||
| 47 | -``` | ||
| 48 | - | ||
| 49 | -Setting `resolvePackageJsonExports: false` restores compatibility with packages that don't have proper exports. | ||
| 50 | - | ||
| 51 | -**Option 3: Revert to Node resolution** | ||
| 52 | -```json | ||
| 53 | -{ | ||
| 54 | - "compilerOptions": { | ||
| 55 | - "moduleResolution": "node" | ||
| 56 | - } | ||
| 57 | -} | ||
| 58 | -``` | ||
| 59 | - | ||
| 60 | -## Which Packages Break? | ||
| 61 | - | ||
| 62 | -Packages break if they: | ||
| 63 | -- Lack `exports` field in package.json | ||
| 64 | -- Have incorrect `exports` configuration | ||
| 65 | -- Rely on Node.js-specific resolution behavior | ||
| 66 | - | ||
| 67 | -## Diagnosis | ||
| 68 | - | ||
| 69 | -```bash | ||
| 70 | -# Check which resolution is being used | ||
| 71 | -cat tsconfig.json | grep moduleResolution | ||
| 72 | - | ||
| 73 | -# Test if a specific module resolves | ||
| 74 | -npx tsc --traceResolution 2>&1 | grep "module-name" | ||
| 75 | -``` | ||
| 76 | - | ||
| 77 | -## Reference | ||
| 78 | - | ||
| 79 | -- [vuejs/tsconfig#8](https://github.com/vuejs/tsconfig/issues/8) | ||
| 80 | -- [TypeScript moduleResolution docs](https://www.typescriptlang.org/tsconfig#moduleResolution) | ||
| 81 | -- [Vite discussion#14001](https://github.com/vitejs/vite/discussions/14001) |
| 1 | ---- | ||
| 2 | -title: Mocking Pinia Stores with Vitest | ||
| 3 | -impact: HIGH | ||
| 4 | -impactDescription: properly mocks Pinia stores in component tests | ||
| 5 | -type: efficiency | ||
| 6 | -tags: pinia, vitest, testing, mock, createTestingPinia, store | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# Mocking Pinia Stores with Vitest | ||
| 10 | - | ||
| 11 | -**Impact: HIGH** - properly mocks Pinia stores in component tests | ||
| 12 | - | ||
| 13 | -Developers struggle to properly mock Pinia stores: `createTestingPinia` requires explicit `createSpy` configuration, and "injection Symbol(pinia) not found" errors occur without proper setup. | ||
| 14 | - | ||
| 15 | -> **Important (@pinia/testing 1.0+):** The `createSpy` option is **REQUIRED**, not optional. Omitting it throws an error: "You must configure the `createSpy` option." | ||
| 16 | - | ||
| 17 | -## Symptoms | ||
| 18 | - | ||
| 19 | -- "injection Symbol(pinia) not found" error | ||
| 20 | -- "You must configure the `createSpy` option" error | ||
| 21 | -- Actions not properly mocked | ||
| 22 | -- Store state not reset between tests | ||
| 23 | - | ||
| 24 | -## Fix | ||
| 25 | - | ||
| 26 | -**Pattern 1: Basic setup with createTestingPinia** | ||
| 27 | -```typescript | ||
| 28 | -import { mount } from '@vue/test-utils' | ||
| 29 | -import { createTestingPinia } from '@pinia/testing' | ||
| 30 | -import { vi } from 'vitest' | ||
| 31 | -import MyComponent from './MyComponent.vue' | ||
| 32 | -import { useCounterStore } from '@/stores/counter' | ||
| 33 | - | ||
| 34 | -test('component uses store', async () => { | ||
| 35 | - const wrapper = mount(MyComponent, { | ||
| 36 | - global: { | ||
| 37 | - plugins: [ | ||
| 38 | - createTestingPinia({ | ||
| 39 | - createSpy: vi.fn, // REQUIRED in @pinia/testing 1.0+ | ||
| 40 | - initialState: { | ||
| 41 | - counter: { count: 10 } // Set initial state | ||
| 42 | - } | ||
| 43 | - }) | ||
| 44 | - ] | ||
| 45 | - } | ||
| 46 | - }) | ||
| 47 | - | ||
| 48 | - // Get the store instance AFTER mounting | ||
| 49 | - const store = useCounterStore() | ||
| 50 | - | ||
| 51 | - // Actions are automatically stubbed | ||
| 52 | - await wrapper.find('button').trigger('click') | ||
| 53 | - expect(store.increment).toHaveBeenCalled() | ||
| 54 | -}) | ||
| 55 | -``` | ||
| 56 | - | ||
| 57 | -**Pattern 2: Customize action behavior** | ||
| 58 | -```typescript | ||
| 59 | -test('component handles async action', async () => { | ||
| 60 | - const wrapper = mount(MyComponent, { | ||
| 61 | - global: { | ||
| 62 | - plugins: [ | ||
| 63 | - createTestingPinia({ | ||
| 64 | - createSpy: vi.fn, | ||
| 65 | - stubActions: false // Don't stub, use real actions | ||
| 66 | - }) | ||
| 67 | - ] | ||
| 68 | - } | ||
| 69 | - }) | ||
| 70 | - | ||
| 71 | - const store = useCounterStore() | ||
| 72 | - | ||
| 73 | - // Override specific action | ||
| 74 | - store.fetchData = vi.fn().mockResolvedValue({ items: [] }) | ||
| 75 | - | ||
| 76 | - await wrapper.find('.load-button').trigger('click') | ||
| 77 | - expect(store.fetchData).toHaveBeenCalled() | ||
| 78 | -}) | ||
| 79 | -``` | ||
| 80 | - | ||
| 81 | -**Pattern 3: Testing store directly** | ||
| 82 | -```typescript | ||
| 83 | -import { setActivePinia, createPinia } from 'pinia' | ||
| 84 | -import { useCounterStore } from '@/stores/counter' | ||
| 85 | - | ||
| 86 | -describe('Counter Store', () => { | ||
| 87 | - beforeEach(() => { | ||
| 88 | - setActivePinia(createPinia()) | ||
| 89 | - }) | ||
| 90 | - | ||
| 91 | - test('increments count', () => { | ||
| 92 | - const store = useCounterStore() | ||
| 93 | - expect(store.count).toBe(0) | ||
| 94 | - | ||
| 95 | - store.increment() | ||
| 96 | - expect(store.count).toBe(1) | ||
| 97 | - }) | ||
| 98 | -}) | ||
| 99 | -``` | ||
| 100 | - | ||
| 101 | -## Setup Store with Vitest | ||
| 102 | - | ||
| 103 | -```typescript | ||
| 104 | -// stores/counter.ts - Setup store syntax | ||
| 105 | -export const useCounterStore = defineStore('counter', () => { | ||
| 106 | - const count = ref(0) | ||
| 107 | - const doubleCount = computed(() => count.value * 2) | ||
| 108 | - | ||
| 109 | - function increment() { | ||
| 110 | - count.value++ | ||
| 111 | - } | ||
| 112 | - | ||
| 113 | - return { count, doubleCount, increment } | ||
| 114 | -}) | ||
| 115 | - | ||
| 116 | -// Test file | ||
| 117 | -test('setup store works', async () => { | ||
| 118 | - const pinia = createTestingPinia({ | ||
| 119 | - createSpy: vi.fn, | ||
| 120 | - initialState: { | ||
| 121 | - counter: { count: 5 } | ||
| 122 | - } | ||
| 123 | - }) | ||
| 124 | - | ||
| 125 | - const wrapper = mount(MyComponent, { | ||
| 126 | - global: { plugins: [pinia] } | ||
| 127 | - }) | ||
| 128 | - | ||
| 129 | - const store = useCounterStore() | ||
| 130 | - expect(store.count).toBe(5) | ||
| 131 | - expect(store.doubleCount).toBe(10) | ||
| 132 | -}) | ||
| 133 | -``` | ||
| 134 | - | ||
| 135 | -## Reset Between Tests | ||
| 136 | - | ||
| 137 | -```typescript | ||
| 138 | -describe('Store Tests', () => { | ||
| 139 | - let pinia: Pinia | ||
| 140 | - | ||
| 141 | - beforeEach(() => { | ||
| 142 | - pinia = createTestingPinia({ | ||
| 143 | - createSpy: vi.fn | ||
| 144 | - }) | ||
| 145 | - }) | ||
| 146 | - | ||
| 147 | - afterEach(() => { | ||
| 148 | - vi.clearAllMocks() | ||
| 149 | - }) | ||
| 150 | - | ||
| 151 | - test('test 1', () => { /* ... */ }) | ||
| 152 | - test('test 2', () => { /* ... */ }) | ||
| 153 | -}) | ||
| 154 | -``` | ||
| 155 | - | ||
| 156 | -## Reference | ||
| 157 | - | ||
| 158 | -- [Pinia Testing Guide](https://pinia.vuejs.org/cookbook/testing.html) | ||
| 159 | -- [Pinia Discussion #2092](https://github.com/vuejs/pinia/discussions/2092) |
| 1 | ---- | ||
| 2 | -title: JSDoc Documentation for Script Setup Components | ||
| 3 | -impact: MEDIUM | ||
| 4 | -impactDescription: enables proper documentation for composition API components | ||
| 5 | -type: capability | ||
| 6 | -tags: jsdoc, script-setup, documentation, composition-api, component | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# JSDoc Documentation for Script Setup Components | ||
| 10 | - | ||
| 11 | -**Impact: MEDIUM** - enables proper documentation for composition API components | ||
| 12 | - | ||
| 13 | -`<script setup>` doesn't have an obvious place to attach JSDoc comments for the component itself. Use a dual-script pattern. | ||
| 14 | - | ||
| 15 | -## Problem | ||
| 16 | - | ||
| 17 | -**Incorrect:** | ||
| 18 | -```vue | ||
| 19 | -<script setup lang="ts"> | ||
| 20 | -/** | ||
| 21 | - * This comment doesn't appear in IDE hover or docs | ||
| 22 | - * @component | ||
| 23 | - */ | ||
| 24 | -import { ref } from 'vue' | ||
| 25 | - | ||
| 26 | -const count = ref(0) | ||
| 27 | -</script> | ||
| 28 | -``` | ||
| 29 | - | ||
| 30 | -JSDoc comments inside `<script setup>` don't attach to the component export because there's no explicit export statement. | ||
| 31 | - | ||
| 32 | -## Solution | ||
| 33 | - | ||
| 34 | -Use both `<script>` and `<script setup>` blocks: | ||
| 35 | - | ||
| 36 | -**Correct:** | ||
| 37 | -```vue | ||
| 38 | -<script lang="ts"> | ||
| 39 | -/** | ||
| 40 | - * A counter component that displays and increments a value. | ||
| 41 | - * | ||
| 42 | - * @example | ||
| 43 | - * ```vue | ||
| 44 | - * <Counter :initial="5" @update="handleUpdate" /> | ||
| 45 | - * ``` | ||
| 46 | - * | ||
| 47 | - * @component | ||
| 48 | - */ | ||
| 49 | -export default {} | ||
| 50 | -</script> | ||
| 51 | - | ||
| 52 | -<script setup lang="ts"> | ||
| 53 | -import { ref } from 'vue' | ||
| 54 | - | ||
| 55 | -const props = defineProps<{ | ||
| 56 | - /** Starting value for the counter */ | ||
| 57 | - initial?: number | ||
| 58 | -}>() | ||
| 59 | - | ||
| 60 | -const emit = defineEmits<{ | ||
| 61 | - /** Emitted when counter value changes */ | ||
| 62 | - update: [value: number] | ||
| 63 | -}>() | ||
| 64 | - | ||
| 65 | -const count = ref(props.initial ?? 0) | ||
| 66 | -</script> | ||
| 67 | -``` | ||
| 68 | - | ||
| 69 | -## How It Works | ||
| 70 | - | ||
| 71 | -- The regular `<script>` block's default export is merged with `<script setup>` | ||
| 72 | -- JSDoc on `export default {}` attaches to the component | ||
| 73 | -- Props and emits JSDoc in `<script setup>` still work normally | ||
| 74 | - | ||
| 75 | -## What Gets Documented | ||
| 76 | - | ||
| 77 | -| Location | Shows In | | ||
| 78 | -|----------|----------| | ||
| 79 | -| `export default {}` JSDoc | Component import hover | | ||
| 80 | -| `defineProps` JSDoc | Prop hover in templates | | ||
| 81 | -| `defineEmits` JSDoc | Event handler hover | | ||
| 82 | - | ||
| 83 | -## Reference | ||
| 84 | - | ||
| 85 | -- [Vue Language Tools Discussion #5932](https://github.com/vuejs/language-tools/discussions/5932) |
| 1 | ---- | ||
| 2 | -title: Enable Strict CSS Modules Type Checking | ||
| 3 | -impact: MEDIUM | ||
| 4 | -impactDescription: catches typos in CSS module class names at compile time | ||
| 5 | -type: capability | ||
| 6 | -tags: strictCssModules, vueCompilerOptions, css-modules, style-module | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# Enable Strict CSS Modules Type Checking | ||
| 10 | - | ||
| 11 | -**Impact: MEDIUM** - catches typos in CSS module class names at compile time | ||
| 12 | - | ||
| 13 | -When using CSS modules with `<style module>`, Vue doesn't validate class names by default. Enable `strictCssModules` to catch typos and undefined classes. | ||
| 14 | - | ||
| 15 | -## Problem | ||
| 16 | - | ||
| 17 | -CSS module class name errors go undetected: | ||
| 18 | - | ||
| 19 | -```vue | ||
| 20 | -<script setup lang="ts"> | ||
| 21 | -// No error for typo in class name | ||
| 22 | -</script> | ||
| 23 | - | ||
| 24 | -<template> | ||
| 25 | - <div :class="$style.buttn">Click me</div> | ||
| 26 | -</template> | ||
| 27 | - | ||
| 28 | -<style module> | ||
| 29 | -.button { | ||
| 30 | - background: blue; | ||
| 31 | -} | ||
| 32 | -</style> | ||
| 33 | -``` | ||
| 34 | - | ||
| 35 | -The typo `buttn` instead of `button` silently fails at runtime. | ||
| 36 | - | ||
| 37 | -## Solution | ||
| 38 | - | ||
| 39 | -Enable `strictCssModules` in your tsconfig: | ||
| 40 | - | ||
| 41 | -```json | ||
| 42 | -// tsconfig.json or tsconfig.app.json | ||
| 43 | -{ | ||
| 44 | - "vueCompilerOptions": { | ||
| 45 | - "strictCssModules": true | ||
| 46 | - } | ||
| 47 | -} | ||
| 48 | -``` | ||
| 49 | - | ||
| 50 | -Now `$style.buttn` will show a type error because `buttn` doesn't exist in the CSS module. | ||
| 51 | - | ||
| 52 | -## What Gets Checked | ||
| 53 | - | ||
| 54 | -| Access | With strictCssModules | | ||
| 55 | -|--------|----------------------| | ||
| 56 | -| `$style.validClass` | OK | | ||
| 57 | -| `$style.typo` | Error: Property 'typo' does not exist | | ||
| 58 | -| `$style['dynamic']` | OK (dynamic access not checked) | | ||
| 59 | - | ||
| 60 | -## Limitations | ||
| 61 | - | ||
| 62 | -- Only checks static property access (`$style.className`) | ||
| 63 | -- Dynamic access (`$style[variable]`) is not validated | ||
| 64 | -- Only works with `<style module>`, not external CSS files | ||
| 65 | - | ||
| 66 | -## Reference | ||
| 67 | - | ||
| 68 | -- [Vue Language Tools Wiki - Vue Compiler Options](https://github.com/vuejs/language-tools/wiki/Vue-Compiler-Options) |
| 1 | ---- | ||
| 2 | -title: unplugin-vue-components and unplugin-auto-import Type Conflicts | ||
| 3 | -impact: HIGH | ||
| 4 | -impactDescription: fixes component types resolving as any when using both plugins | ||
| 5 | -type: capability | ||
| 6 | -tags: unplugin-vue-components, unplugin-auto-import, types, any, dts | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# unplugin-vue-components and unplugin-auto-import Type Conflicts | ||
| 10 | - | ||
| 11 | -**Impact: HIGH** - fixes component types resolving as any when using both plugins | ||
| 12 | - | ||
| 13 | -Installing both `unplugin-vue-components` and `unplugin-auto-import` can cause component types to resolve as `any`. The generated `.d.ts` files conflict with each other. | ||
| 14 | - | ||
| 15 | -## Symptoms | ||
| 16 | - | ||
| 17 | -- Components typed as `any` instead of proper component types | ||
| 18 | -- No autocomplete for component props | ||
| 19 | -- No type errors for invalid props | ||
| 20 | -- Types work when using only one plugin but break with both | ||
| 21 | - | ||
| 22 | -## Root Cause | ||
| 23 | - | ||
| 24 | -Both plugins generate declaration files (`components.d.ts` and `auto-imports.d.ts`) that can have conflicting declarations. TypeScript declaration merging fails silently. | ||
| 25 | - | ||
| 26 | -## Fix | ||
| 27 | - | ||
| 28 | -**Step 1: Ensure both .d.ts files are in tsconfig include** | ||
| 29 | -```json | ||
| 30 | -{ | ||
| 31 | - "include": [ | ||
| 32 | - "src/**/*.ts", | ||
| 33 | - "src/**/*.vue", | ||
| 34 | - "components.d.ts", | ||
| 35 | - "auto-imports.d.ts" | ||
| 36 | - ] | ||
| 37 | -} | ||
| 38 | -``` | ||
| 39 | - | ||
| 40 | -**Step 2: Set explicit, different dts paths** | ||
| 41 | -```typescript | ||
| 42 | -// vite.config.ts | ||
| 43 | -import Components from 'unplugin-vue-components/vite' | ||
| 44 | -import AutoImport from 'unplugin-auto-import/vite' | ||
| 45 | - | ||
| 46 | -export default defineConfig({ | ||
| 47 | - plugins: [ | ||
| 48 | - Components({ | ||
| 49 | - dts: 'src/types/components.d.ts' // Explicit unique path | ||
| 50 | - }), | ||
| 51 | - AutoImport({ | ||
| 52 | - dts: 'src/types/auto-imports.d.ts' // Explicit unique path | ||
| 53 | - }) | ||
| 54 | - ] | ||
| 55 | -}) | ||
| 56 | -``` | ||
| 57 | - | ||
| 58 | -**Step 3: Regenerate type files** | ||
| 59 | -```bash | ||
| 60 | -# Delete existing .d.ts files | ||
| 61 | -rm components.d.ts auto-imports.d.ts | ||
| 62 | - | ||
| 63 | -# Restart dev server to regenerate | ||
| 64 | -npm run dev | ||
| 65 | -``` | ||
| 66 | - | ||
| 67 | -**Step 4: Verify no duplicate declarations** | ||
| 68 | - | ||
| 69 | -Check that the same component isn't declared in both files. | ||
| 70 | - | ||
| 71 | -## Plugin Order Matters | ||
| 72 | - | ||
| 73 | -Configure Components plugin AFTER AutoImport: | ||
| 74 | -```typescript | ||
| 75 | -plugins: [ | ||
| 76 | - AutoImport({ /* ... */ }), | ||
| 77 | - Components({ /* ... */ }) // Must come after AutoImport | ||
| 78 | -] | ||
| 79 | -``` | ||
| 80 | - | ||
| 81 | -## Common Mistake: Duplicate Imports | ||
| 82 | - | ||
| 83 | -Don't configure the same import in both plugins: | ||
| 84 | -```typescript | ||
| 85 | -// Wrong - Vue imported in both | ||
| 86 | -AutoImport({ | ||
| 87 | - imports: ['vue'] | ||
| 88 | -}) | ||
| 89 | -Components({ | ||
| 90 | - resolvers: [/* includes Vue components */] | ||
| 91 | -}) | ||
| 92 | -``` | ||
| 93 | - | ||
| 94 | -## Reference | ||
| 95 | - | ||
| 96 | -- [unplugin-vue-components#640](https://github.com/unplugin/unplugin-vue-components/issues/640) | ||
| 97 | -- [unplugin-auto-import docs](https://github.com/unplugin/unplugin-auto-import) |
| 1 | ---- | ||
| 2 | -title: Volar 3.0 Breaking Changes | ||
| 3 | -impact: HIGH | ||
| 4 | -impactDescription: fixes editor integration after Volar/vue-language-server upgrade | ||
| 5 | -type: capability | ||
| 6 | -tags: volar, vue-language-server, neovim, vscode, ide, ts_ls, vtsls | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# Volar 3.0 Breaking Changes | ||
| 10 | - | ||
| 11 | -**Impact: HIGH** - fixes editor integration after Volar/vue-language-server upgrade | ||
| 12 | - | ||
| 13 | -Volar 3.0 (vue-language-server 3.x) introduced breaking changes to the language server protocol. Editors configured for Volar 2.x will break with errors like "vue_ls doesn't work with ts_ls.. it expects vtsls". | ||
| 14 | - | ||
| 15 | -## Symptoms | ||
| 16 | - | ||
| 17 | -- `vue_ls doesn't work with ts_ls` | ||
| 18 | -- TypeScript features stop working in Vue files | ||
| 19 | -- No autocomplete, type hints, or error highlighting | ||
| 20 | -- Editor shows "Language server initialization failed" | ||
| 21 | - | ||
| 22 | -## Fix by Editor | ||
| 23 | - | ||
| 24 | -### VSCode | ||
| 25 | - | ||
| 26 | -Update the "Vue - Official" extension to latest version. It manages the language server automatically. | ||
| 27 | - | ||
| 28 | -### NeoVim (nvim-lspconfig) | ||
| 29 | - | ||
| 30 | -**Option 1: Use vtsls instead of ts_ls** | ||
| 31 | -```lua | ||
| 32 | --- Replace ts_ls/tsserver with vtsls | ||
| 33 | -require('lspconfig').vtsls.setup({}) | ||
| 34 | -require('lspconfig').volar.setup({}) | ||
| 35 | -``` | ||
| 36 | - | ||
| 37 | -**Option 2: Downgrade vue-language-server** | ||
| 38 | -```bash | ||
| 39 | -npm install -g @vue/language-server@2.1.10 | ||
| 40 | -``` | ||
| 41 | - | ||
| 42 | -### JetBrains IDEs | ||
| 43 | - | ||
| 44 | -Update to latest Vue plugin. If issues persist, disable and re-enable the Vue plugin. | ||
| 45 | - | ||
| 46 | -## What Changed in 3.0 | ||
| 47 | - | ||
| 48 | -| Feature | Volar 2.x | Volar 3.0 | | ||
| 49 | -|---------|-----------|-----------| | ||
| 50 | -| Package name | volar | vue_ls | | ||
| 51 | -| TypeScript integration | ts_ls/tsserver | vtsls required | | ||
| 52 | -| Hybrid mode | Optional | Default | | ||
| 53 | - | ||
| 54 | -## Workaround: Stay on 2.x | ||
| 55 | - | ||
| 56 | -If upgrading is not possible: | ||
| 57 | -```bash | ||
| 58 | -npm install -g @vue/language-server@^2.0.0 | ||
| 59 | -``` | ||
| 60 | - | ||
| 61 | -Pin in your project's package.json to prevent accidental upgrades. | ||
| 62 | - | ||
| 63 | -## Reference | ||
| 64 | - | ||
| 65 | -- [vuejs/language-tools#5598](https://github.com/vuejs/language-tools/issues/5598) | ||
| 66 | -- [NeoVim Vue Setup Guide](https://dev.to/danwalsh/solved-vue-3-typescript-inlay-hint-support-in-neovim-53ej) |
| 1 | ---- | ||
| 2 | -title: Vue Template Directive Comments | ||
| 3 | -impact: HIGH | ||
| 4 | -impactDescription: enables fine-grained control over template type checking | ||
| 5 | -type: capability | ||
| 6 | -tags: vue-directive, vue-ignore, vue-expect-error, vue-skip, template, type-checking | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# Vue Template Directive Comments | ||
| 10 | - | ||
| 11 | -**Impact: HIGH** - enables fine-grained control over template type checking | ||
| 12 | - | ||
| 13 | -Vue Language Tools supports special directive comments to control type checking behavior in templates. | ||
| 14 | - | ||
| 15 | -## Available Directives | ||
| 16 | - | ||
| 17 | -### @vue-ignore | ||
| 18 | - | ||
| 19 | -Suppress type errors for the next line: | ||
| 20 | - | ||
| 21 | -```vue | ||
| 22 | -<template> | ||
| 23 | - <!-- @vue-ignore --> | ||
| 24 | - <Component :prop="valueWithTypeError" /> | ||
| 25 | -</template> | ||
| 26 | -``` | ||
| 27 | - | ||
| 28 | -### @vue-expect-error | ||
| 29 | - | ||
| 30 | -Assert that the next line should have a type error (useful for testing): | ||
| 31 | - | ||
| 32 | -```vue | ||
| 33 | -<template> | ||
| 34 | - <!-- @vue-expect-error --> | ||
| 35 | - <Component :invalid-prop="value" /> | ||
| 36 | -</template> | ||
| 37 | -``` | ||
| 38 | - | ||
| 39 | -### @vue-skip | ||
| 40 | - | ||
| 41 | -Skip type checking for an entire block: | ||
| 42 | - | ||
| 43 | -```vue | ||
| 44 | -<template> | ||
| 45 | - <!-- @vue-skip --> | ||
| 46 | - <div> | ||
| 47 | - <!-- Everything in here is not type-checked --> | ||
| 48 | - <LegacyComponent :any="props" :go="here" /> | ||
| 49 | - </div> | ||
| 50 | -</template> | ||
| 51 | -``` | ||
| 52 | - | ||
| 53 | -### @vue-generic | ||
| 54 | - | ||
| 55 | -Declare template-level generic types: | ||
| 56 | - | ||
| 57 | -```vue | ||
| 58 | -<template> | ||
| 59 | - <!-- @vue-generic {T extends string} --> | ||
| 60 | - <GenericList :items="items as T[]" /> | ||
| 61 | -</template> | ||
| 62 | -``` | ||
| 63 | - | ||
| 64 | -## Use Cases | ||
| 65 | - | ||
| 66 | -- Migrating legacy components with incomplete types | ||
| 67 | -- Working with third-party components that have incorrect type definitions | ||
| 68 | -- Temporarily suppressing errors during refactoring | ||
| 69 | -- Testing that certain patterns produce expected type errors | ||
| 70 | - | ||
| 71 | -## Reference | ||
| 72 | - | ||
| 73 | -- [Vue Language Tools Wiki - Directive Comments](https://github.com/vuejs/language-tools/wiki/Directive-Comments) |
| 1 | ---- | ||
| 2 | -title: Vue Router useRoute Params Union Type Narrowing | ||
| 3 | -impact: MEDIUM | ||
| 4 | -impactDescription: fixes "Property does not exist" errors with typed route params | ||
| 5 | -type: capability | ||
| 6 | -tags: vue-router, useRoute, unplugin-vue-router, typed-routes, params | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# Vue Router useRoute Params Union Type Narrowing | ||
| 10 | - | ||
| 11 | -**Impact: MEDIUM** - fixes "Property does not exist" errors with typed route params | ||
| 12 | - | ||
| 13 | -With `unplugin-vue-router` typed routes, `route.params` becomes a union of ALL page param types. TypeScript cannot narrow `Record<never, never> | { id: string }` properly, causing "Property 'id' does not exist" errors even on the correct page. | ||
| 14 | - | ||
| 15 | -## Symptoms | ||
| 16 | - | ||
| 17 | -- "Property 'id' does not exist on type 'RouteParams'" | ||
| 18 | -- `route.params.id` shows as `string | undefined` everywhere | ||
| 19 | -- Union type of all route params instead of specific route | ||
| 20 | -- Type narrowing with `if (route.name === 'users-id')` doesn't work | ||
| 21 | - | ||
| 22 | -## Root Cause | ||
| 23 | - | ||
| 24 | -`unplugin-vue-router` generates a union type of all possible route params. TypeScript's control flow analysis can't narrow this union based on route name checks. | ||
| 25 | - | ||
| 26 | -## Fix | ||
| 27 | - | ||
| 28 | -**Option 1: Pass route name to useRoute (recommended)** | ||
| 29 | -```typescript | ||
| 30 | -// pages/users/[id].vue | ||
| 31 | -import { useRoute } from 'vue-router/auto' | ||
| 32 | - | ||
| 33 | -// Specify the route path for proper typing | ||
| 34 | -const route = useRoute('/users/[id]') | ||
| 35 | - | ||
| 36 | -// Now properly typed as { id: string } | ||
| 37 | -console.log(route.params.id) // string, not string | undefined | ||
| 38 | -``` | ||
| 39 | - | ||
| 40 | -**Option 2: Type assertion with specific route** | ||
| 41 | -```typescript | ||
| 42 | -import { useRoute } from 'vue-router' | ||
| 43 | -import type { RouteLocationNormalized } from 'vue-router/auto-routes' | ||
| 44 | - | ||
| 45 | -const route = useRoute() as RouteLocationNormalized<'/users/[id]'> | ||
| 46 | -route.params.id // Properly typed | ||
| 47 | -``` | ||
| 48 | - | ||
| 49 | -**Option 3: Define route-specific param type** | ||
| 50 | -```typescript | ||
| 51 | -// In your page component | ||
| 52 | -interface UserRouteParams { | ||
| 53 | - id: string | ||
| 54 | -} | ||
| 55 | - | ||
| 56 | -const route = useRoute() | ||
| 57 | -const { id } = route.params as UserRouteParams | ||
| 58 | -``` | ||
| 59 | - | ||
| 60 | -## Required tsconfig Setting | ||
| 61 | - | ||
| 62 | -Ensure `moduleResolution: "bundler"` for unplugin-vue-router: | ||
| 63 | -```json | ||
| 64 | -{ | ||
| 65 | - "compilerOptions": { | ||
| 66 | - "moduleResolution": "bundler" | ||
| 67 | - } | ||
| 68 | -} | ||
| 69 | -``` | ||
| 70 | - | ||
| 71 | -## Caveat: Route Name Format | ||
| 72 | - | ||
| 73 | -The route name matches the file path pattern: | ||
| 74 | -- `pages/users/[id].vue` → `/users/[id]` | ||
| 75 | -- `pages/posts/[slug]/comments.vue` → `/posts/[slug]/comments` | ||
| 76 | - | ||
| 77 | -## Reference | ||
| 78 | - | ||
| 79 | -- [unplugin-vue-router#337](https://github.com/posva/unplugin-vue-router/issues/337) | ||
| 80 | -- [unplugin-vue-router#176](https://github.com/posva/unplugin-vue-router/discussions/176) | ||
| 81 | -- [unplugin-vue-router TypeScript docs](https://uvr.esm.is/guide/typescript.html) |
| 1 | ---- | ||
| 2 | -title: Enable Strict Template Checking | ||
| 3 | -impact: HIGH | ||
| 4 | -impactDescription: catches undefined components and props at compile time | ||
| 5 | -type: capability | ||
| 6 | -tags: vue-tsc, typescript, type-checking, templates, vueCompilerOptions | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# Enable Strict Template Checking | ||
| 10 | - | ||
| 11 | -**Impact: HIGH** - catches undefined components and props at compile time | ||
| 12 | - | ||
| 13 | -By default, vue-tsc does not report errors for undefined components in templates. Enable `strictTemplates` to catch these issues during type checking. | ||
| 14 | - | ||
| 15 | -## Which tsconfig? | ||
| 16 | - | ||
| 17 | -Add `vueCompilerOptions` to the tsconfig that includes Vue source files. In projects with multiple tsconfigs (like those created with `create-vue`), this is typically `tsconfig.app.json`, not the root `tsconfig.json` or `tsconfig.node.json`. | ||
| 18 | - | ||
| 19 | -**Incorrect (missing strict checking):** | ||
| 20 | - | ||
| 21 | -```json | ||
| 22 | -{ | ||
| 23 | - "compilerOptions": { | ||
| 24 | - "strict": true | ||
| 25 | - } | ||
| 26 | - // vueCompilerOptions not configured - undefined components won't error | ||
| 27 | -} | ||
| 28 | -``` | ||
| 29 | - | ||
| 30 | -**Correct (strict template checking enabled):** | ||
| 31 | - | ||
| 32 | -```json | ||
| 33 | -{ | ||
| 34 | - "compilerOptions": { | ||
| 35 | - "strict": true | ||
| 36 | - }, | ||
| 37 | - "vueCompilerOptions": { | ||
| 38 | - "strictTemplates": true | ||
| 39 | - } | ||
| 40 | -} | ||
| 41 | -``` | ||
| 42 | - | ||
| 43 | -## Available Options | ||
| 44 | - | ||
| 45 | -| Option | Default | Effect | | ||
| 46 | -|--------|---------|--------| | ||
| 47 | -| `strictTemplates` | `false` | Enables all checkUnknown* options below | | ||
| 48 | -| `checkUnknownComponents` | `false` | Error on undefined/unregistered components | | ||
| 49 | -| `checkUnknownProps` | `false` | Error on props not declared in component definition | | ||
| 50 | -| `checkUnknownEvents` | `false` | Error on events not declared via `defineEmits` | | ||
| 51 | -| `checkUnknownDirectives` | `false` | Error on unregistered custom directives | | ||
| 52 | - | ||
| 53 | -## Granular Control | ||
| 54 | - | ||
| 55 | -If `strictTemplates` is too strict, enable individual checks: | ||
| 56 | - | ||
| 57 | -```json | ||
| 58 | -{ | ||
| 59 | - "vueCompilerOptions": { | ||
| 60 | - "checkUnknownComponents": true, | ||
| 61 | - "checkUnknownProps": false | ||
| 62 | - } | ||
| 63 | -} | ||
| 64 | -``` | ||
| 65 | - | ||
| 66 | -## Reference | ||
| 67 | - | ||
| 68 | -- [Vue Compiler Options](https://github.com/vuejs/language-tools/wiki/Vue-Compiler-Options) | ||
| 69 | -- [Vite Vue+TS Template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-vue-ts) |
| 1 | ---- | ||
| 2 | -title: withDefaults Incorrect Default with Union Types | ||
| 3 | -impact: MEDIUM | ||
| 4 | -impactDescription: fixes incorrect default value behavior with union type props | ||
| 5 | -type: capability | ||
| 6 | -tags: withDefaults, defineProps, union-types, defaults, vue-3.5 | ||
| 7 | ---- | ||
| 8 | - | ||
| 9 | -# withDefaults Incorrect Default with Union Types | ||
| 10 | - | ||
| 11 | -**Impact: MEDIUM** - fixes spurious "Missing required prop" warning with union type props | ||
| 12 | - | ||
| 13 | -Using `withDefaults` with union types like `false | string` may produce a Vue runtime warning "Missing required prop" even when a default is provided. The runtime value IS applied correctly, but the warning can be confusing. | ||
| 14 | - | ||
| 15 | -## Symptoms | ||
| 16 | - | ||
| 17 | -- Vue warns "Missing required prop" despite default being set | ||
| 18 | -- Warning appears only with union types like `false | string` | ||
| 19 | -- TypeScript types are correct | ||
| 20 | -- Runtime value IS correct (the default is applied) | ||
| 21 | - | ||
| 22 | -## Problematic Pattern | ||
| 23 | - | ||
| 24 | -```typescript | ||
| 25 | -// This produces a spurious warning (but works at runtime) | ||
| 26 | -interface Props { | ||
| 27 | - value: false | string // Union type | ||
| 28 | -} | ||
| 29 | - | ||
| 30 | -const props = withDefaults(defineProps<Props>(), { | ||
| 31 | - value: 'default' // Runtime value IS correct, but Vue warns about missing prop | ||
| 32 | -}) | ||
| 33 | -``` | ||
| 34 | - | ||
| 35 | -## Fix | ||
| 36 | - | ||
| 37 | -**Option 1: Use Reactive Props Destructure (Vue 3.5+)** | ||
| 38 | -```vue | ||
| 39 | -<script setup lang="ts"> | ||
| 40 | -interface Props { | ||
| 41 | - value: false | string | ||
| 42 | -} | ||
| 43 | - | ||
| 44 | -// Preferred in Vue 3.5+ | ||
| 45 | -const { value = 'default' } = defineProps<Props>() | ||
| 46 | -</script> | ||
| 47 | -``` | ||
| 48 | - | ||
| 49 | -**Option 2: Use runtime declaration** | ||
| 50 | -```vue | ||
| 51 | -<script setup lang="ts"> | ||
| 52 | -const props = defineProps({ | ||
| 53 | - value: { | ||
| 54 | - type: [Boolean, String] as PropType<false | string>, | ||
| 55 | - default: 'default' | ||
| 56 | - } | ||
| 57 | -}) | ||
| 58 | -</script> | ||
| 59 | -``` | ||
| 60 | - | ||
| 61 | -**Option 3: Split into separate props** | ||
| 62 | -```typescript | ||
| 63 | -interface Props { | ||
| 64 | - enabled: boolean | ||
| 65 | - customValue?: string | ||
| 66 | -} | ||
| 67 | - | ||
| 68 | -const props = withDefaults(defineProps<Props>(), { | ||
| 69 | - enabled: false, | ||
| 70 | - customValue: 'default' | ||
| 71 | -}) | ||
| 72 | -``` | ||
| 73 | - | ||
| 74 | -## Why Reactive Props Destructure Works | ||
| 75 | - | ||
| 76 | -Vue 3.5's Reactive Props Destructure handles default values at the destructuring level, bypassing the type inference issues with `withDefaults`. | ||
| 77 | - | ||
| 78 | -```typescript | ||
| 79 | -// The default is applied during destructuring, not type inference | ||
| 80 | -const { prop = 'default' } = defineProps<{ prop?: string }>() | ||
| 81 | -``` | ||
| 82 | - | ||
| 83 | -## Enable Reactive Props Destructure | ||
| 84 | - | ||
| 85 | -This is enabled by default in Vue 3.5+. For older versions: | ||
| 86 | -```javascript | ||
| 87 | -// vite.config.js | ||
| 88 | -export default { | ||
| 89 | - plugins: [ | ||
| 90 | - vue({ | ||
| 91 | - script: { | ||
| 92 | - propsDestructure: true | ||
| 93 | - } | ||
| 94 | - }) | ||
| 95 | - ] | ||
| 96 | -} | ||
| 97 | -``` | ||
| 98 | - | ||
| 99 | -## Reference | ||
| 100 | - | ||
| 101 | -- [vuejs/core#12897](https://github.com/vuejs/core/issues/12897) | ||
| 102 | -- [Reactive Props Destructure RFC](https://github.com/vuejs/rfcs/discussions/502) |
docs/ARCHITECTURE.md
deleted
100644 → 0
| 1 | -# 架构实现与工程配置 | ||
| 2 | - | ||
| 3 | -## 入口与初始化 | ||
| 4 | - | ||
| 5 | -- 应用入口:[/src/main.js](file:///Users/huyirui/program/itomix/git/mlaj/src/main.js) | ||
| 6 | - - 创建 App、注册全局 Icon 组件、挂载路由 | ||
| 7 | - - 全局注入 axios 到 app.config.globalProperties.$http | ||
| 8 | -- 根组件:[/src/App.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/App.vue) | ||
| 9 | - - 初始化全局认证与购物车:provideAuth / provideCart | ||
| 10 | - - 生产环境 + 微信环境:初始化微信 JSSDK(配置签名 URL) | ||
| 11 | - - 生产环境:版本更新探测(弹窗提示刷新) | ||
| 12 | - | ||
| 13 | -## 路由与权限 | ||
| 14 | - | ||
| 15 | -- 路由入口:[/src/router/index.js](file:///Users/huyirui/program/itomix/git/mlaj/src/router/index.js) | ||
| 16 | - - Hash 路由:createWebHashHistory(import.meta.env.VITE_BASE || '/') | ||
| 17 | - - beforeEach:统一登录页回跳处理,并在必要时探测“是否已登录” | ||
| 18 | -- 鉴权策略:[/src/router/guards.js](file:///Users/huyirui/program/itomix/git/mlaj/src/router/guards.js) | ||
| 19 | - - 白名单 + meta.requiresAuth 双策略判断 | ||
| 20 | - - 未登录时重定向 /login 并带 redirect | ||
| 21 | -- 微信授权策略:[/src/router/guards.js](file:///Users/huyirui/program/itomix/git/mlaj/src/router/guards.js) | ||
| 22 | - - 不在路由守卫自动触发授权,避免循环 | ||
| 23 | - - 仅在用户触发(如点击微信图标/购买流程探测)时调用 startWxAuth | ||
| 24 | - | ||
| 25 | -## 请求与登录态注入 | ||
| 26 | - | ||
| 27 | -- Axios 封装:[/src/utils/axios.js](file:///Users/huyirui/program/itomix/git/mlaj/src/utils/axios.js) | ||
| 28 | - - 请求拦截:动态读取本地 user_info 并注入 User-Id / User-Token | ||
| 29 | - - 响应拦截:code=401 时,仅当当前路由确实需要登录才跳转登录(公开页面不强制跳转) | ||
| 30 | -- 登录态管理:[/src/contexts/auth.js](file:///Users/huyirui/program/itomix/git/mlaj/src/contexts/auth.js) | ||
| 31 | - - provide/inject 维护 currentUser/loading/login/logout | ||
| 32 | - - localStorage 持久化 currentUser | ||
| 33 | - - 初始化流程中会探测授权/登录态并拉取用户信息 | ||
| 34 | - | ||
| 35 | -## 购物车与结算 | ||
| 36 | - | ||
| 37 | -- 购物车上下文:[/src/contexts/cart.js](file:///Users/huyirui/program/itomix/git/mlaj/src/contexts/cart.js) | ||
| 38 | - - 单品/多品两种模式(App.vue 默认使用单品模式) | ||
| 39 | - - localStorage 带时间戳的过期策略(默认一天过期) | ||
| 40 | - - handleCheckout 负责构建订单数据并提交订单 | ||
| 41 | - | ||
| 42 | -## 上传与预览 | ||
| 43 | - | ||
| 44 | -- 上传工具:[/src/utils/upload.js](file:///Users/huyirui/program/itomix/git/mlaj/src/utils/upload.js)、[/src/utils/qiniuFileHash.js](file:///Users/huyirui/program/itomix/git/mlaj/src/utils/qiniuFileHash.js) | ||
| 45 | - - 前端计算文件 Hash,支持“秒传检测 + 直传对象存储” | ||
| 46 | -- 关键业务复用:[/src/composables/useCheckin.js](file:///Users/huyirui/program/itomix/git/mlaj/src/composables/useCheckin.js) | ||
| 47 | - - 打卡/作业提交流程:校验、上传、提交、编辑回填等 | ||
| 48 | -- 预览组件:[/src/components/media](file:///Users/huyirui/program/itomix/git/mlaj/src/components/media) | ||
| 49 | - - 视频/音频播放器、PDF/Office 预览等 | ||
| 50 | - | ||
| 51 | -## Vite 配置与环境变量 | ||
| 52 | - | ||
| 53 | -- Vite 配置:[/vite.config.js](file:///Users/huyirui/program/itomix/git/mlaj/vite.config.js) | ||
| 54 | - - 自动按需引入 Vant 组件(unplugin-auto-import / unplugin-vue-components) | ||
| 55 | - - 别名:@ / @components / @utils / @api 等 | ||
| 56 | - - 本地代理:createProxy(viteEnv.VITE_PROXY_PREFIX, viteEnv.VITE_PROXY_TARGET) | ||
| 57 | -- 环境变量示例:[.env](file:///Users/huyirui/program/itomix/git/mlaj/.env) | ||
| 58 | - - VITE_PORT:开发端口 | ||
| 59 | - - VITE_PROXY_TARGET / VITE_PROXY_PREFIX:接口代理目标与前缀 | ||
| 60 | - - VITE_OUTDIR:构建输出目录 | ||
| 61 | - - VITE_CONSOLE:调试开关 | ||
| 62 | - | ||
| 63 | -## 目录结构(详细) | ||
| 64 | - | ||
| 65 | -``` | ||
| 66 | -mlaj/ | ||
| 67 | -├── build/ # Vite 代理封装 | ||
| 68 | -├── docs/ # 项目文档(本目录) | ||
| 69 | -├── public/ # 静态资源 | ||
| 70 | -├── src/ | ||
| 71 | -│ ├── api/ # 按业务域拆分的接口封装(auth/course/checkin/teacher/...) | ||
| 72 | -│ ├── assets/ # 图片等资源 | ||
| 73 | -│ ├── common/ # 常量 | ||
| 74 | -│ ├── components/ # 组件(按业务域归类) | ||
| 75 | -│ ├── composables/ # 组合式函数(逻辑复用,含单测) | ||
| 76 | -│ ├── contexts/ # 全局状态(provide/inject:auth/cart) | ||
| 77 | -│ ├── router/ # 路由定义与守卫 | ||
| 78 | -│ ├── utils/ # 工具层(axios、上传、鉴权存储、版本更新等) | ||
| 79 | -│ └── views/ # 页面(按业务域归类) | ||
| 80 | -├── tailwind.config.js # Tailwind 配置 | ||
| 81 | -└── vite.config.js # Vite 配置 | ||
| 82 | -``` |
docs/CHANGELOG.md
deleted
100644 → 0
| 1 | -# 功能更新记录(Recent Changes) | ||
| 2 | - | ||
| 3 | -说明:该章节从 README 迁移到本文件,避免 README 过长。后续新增变更建议追加在文件顶部。 | ||
| 4 | - | ||
| 5 | -## 2026-01-25 | ||
| 6 | - | ||
| 7 | -- 新增「暂存用户打卡信息」开发规划:[/docs/plan/暂存用户打卡信息.md](file:///Users/huyirui/program/itomix/git/mlaj/docs/plan/%E6%9A%82%E5%AD%98%E7%94%A8%E6%88%B7%E6%89%93%E5%8D%A1%E4%BF%A1%E6%81%AF.md) | ||
| 8 | -- 完成「暂存用户打卡信息」功能开发 | ||
| 9 | - - 核心逻辑:[/src/composables/useCheckinDraft.js](file:///Users/huyirui/program/itomix/git/mlaj/src/composables/useCheckinDraft.js) | ||
| 10 | - - 页面集成:[/src/views/checkin/CheckinDetailPage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/checkin/CheckinDetailPage.vue) | ||
| 11 | - - 支持自动保存、过期清理、恢复提示 | ||
| 12 | -- 修复打卡详情页 `countValue` 初始化顺序导致的 ReferenceError 报错 | ||
| 13 | -- 修复附件上传成功后未保存 URL 导致草稿恢复后无法预览的问题 | ||
| 14 | -- 优化文件 URL 获取逻辑:移除硬编码默认域名,优先使用接口返回的 URL 或 src,仅在有域名信息时拼接 URL | ||
| 15 | - | ||
| 16 | -## 打卡详情页重构(/checkin/detail) | ||
| 17 | - | ||
| 18 | -- 统一了文本、媒体上传和计数打卡的入口 | ||
| 19 | -- 实现了基于 composables 的通用提交流程:[/src/composables/useCheckin.js](file:///Users/huyirui/program/itomix/git/mlaj/src/composables/useCheckin.js) | ||
| 20 | -- 页面入口:[/src/views/checkin/CheckinDetailPage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/checkin/CheckinDetailPage.vue) | ||
| 21 | -- 优化了附件预览与编辑回填逻辑(音频/视频/图片预览能力) | ||
| 22 | - | ||
| 23 | -## 教师端功能完善(/teacher) | ||
| 24 | - | ||
| 25 | -- 新增作业管理页面:/teacher/tasks(列表展示:名称、开始/截止时间) | ||
| 26 | -- 新增作业主页:/teacher/tasks/:id(统计 + 日历视图) | ||
| 27 | -- 新增学员作业记录页:/teacher/student-record(作业帖子 + 点赞/点评) | ||
| 28 | - | ||
| 29 | -## 基础体验优化 | ||
| 30 | - | ||
| 31 | -- 登录逻辑调整:仅在登录页点击微信图标时触发授权(避免路由守卫自动授权导致的循环) | ||
| 32 | - - 关键文件:[/src/router/guards.js](file:///Users/huyirui/program/itomix/git/mlaj/src/router/guards.js)、[/src/router/index.js](file:///Users/huyirui/program/itomix/git/mlaj/src/router/index.js)、[/src/views/auth/LoginPage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/auth/LoginPage.vue) | ||
| 33 | -- 搜索栏优化:提升 iOS 软键盘“搜索”键触发稳定性 | ||
| 34 | -- 课程详情页:增加动态 Open Graph 标签,优化分享体验 | ||
| 35 | - | ||
| 36 | -## 课程详情页动态 Open Graph 元标签 | ||
| 37 | - | ||
| 38 | -- 行为:进入课程详情页时,在 head 中插入 og:title / og:description / og:image / og:url;离开页面时移除 | ||
| 39 | -- CDN 规则:图片域名为 cdn.ipadbiz.cn 时,追加 ?imageMogr2/thumbnail/200x/strip/quality/70 | ||
| 40 | -- 位置:[/src/views/courses/CourseDetailPage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/courses/CourseDetailPage.vue) | ||
| 41 | - | ||
| 42 | -## 购买流程环境校验与微信授权探测 | ||
| 43 | - | ||
| 44 | -- 行为:仅对非免费课程在详情页点击“购买”时进行校验;生产环境必须为微信内置浏览器 | ||
| 45 | -- 微信环境内:若未完成微信授权(openid_has=false),会自动发起一次微信授权并中止本次购买,授权后再次点击进入结算 | ||
| 46 | -- 位置:[/src/views/courses/CourseDetailPage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/courses/CourseDetailPage.vue) | ||
| 47 | - | ||
| 48 | -## 401 拦截策略优化(公开页面不再跳登录) | ||
| 49 | - | ||
| 50 | -- 行为:接口返回 code=401 时,仅当当前路由确实需要登录时才重定向登录 | ||
| 51 | -- 位置:[/src/utils/axios.js](file:///Users/huyirui/program/itomix/git/mlaj/src/utils/axios.js) | ||
| 52 | - | ||
| 53 | -## 搜索栏回车搜索兼容性提升 | ||
| 54 | - | ||
| 55 | -- 行为:输入框类型改为 search,并可选开启 form submit 机制,同时保留 keyup.enter | ||
| 56 | -- 位置:[/src/components/common/SearchBar.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/components/common/SearchBar.vue) | ||
| 57 | - | ||
| 58 | -## 分享海报弹窗(可复用) | ||
| 59 | - | ||
| 60 | -- 入口:课程详情页底部操作栏“分享”按钮 | ||
| 61 | -- 组件:[/src/components/poster/SharePoster.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/components/poster/SharePoster.vue) | ||
| 62 | -- 能力:弹窗打开时通过 Canvas 合成海报(封面、二维码、文案),生成 dataURL 展示,用户长按保存 | ||
| 63 | - | ||
| 64 | -## 打卡弹窗与列表组件(可复用) | ||
| 65 | - | ||
| 66 | -- 打卡弹窗:[/src/components/checkin/CheckInDialog.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/components/checkin/CheckInDialog.vue) | ||
| 67 | -- 打卡列表:[/src/components/checkin/CheckInList.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/components/checkin/CheckInList.vue) |
docs/COMPONENTS.md
deleted
100644 → 0
| 1 | -# /src/components 组件目录索引 | ||
| 2 | - | ||
| 3 | -## 目录划分 | ||
| 4 | - | ||
| 5 | -| 目录 | 代表组件 | 说明 | | ||
| 6 | -| --- | --- | --- | | ||
| 7 | -| activity/ | ActivityCard.vue、ActivityApplyHistoryPopup.vue | 活动卡片、报名/历史相关弹窗 | | ||
| 8 | -| calendar/ | CollapsibleCalendar.vue、TaskCalendar.vue | 日历与任务日历组件 | | ||
| 9 | -| checkin/ | CheckInDialog.vue、CheckInList.vue、CheckinCard.vue、UploadVideoPopup.vue | 打卡/作业相关组件(弹窗、列表、卡片、上传) | | ||
| 10 | -| common/ | ConfirmDialog.vue、GradientHeader.vue、SearchBar.vue、UserAgreement.vue | 通用基础组件(确认、头部、搜索、协议) | | ||
| 11 | -| count/ | AddTargetDialog.vue、CheckinTargetList.vue、postCountModel.vue | 计数型打卡相关组件 | | ||
| 12 | -| courses/ | CourseCard.vue、CourseList.vue、LiveStreamCard.vue、ReviewPopup.vue | 课程展示与列表、直播卡片、评价弹窗等 | | ||
| 13 | -| effects/ | FrostedGlass.vue、StarryBackground.vue | 视觉效果组件 | | ||
| 14 | -| homePage/ | FeaturedCoursesSection.vue、LatestActivitiesSection.vue | 首页区块组件(精选/活动/推荐等) | | ||
| 15 | -| infoEntry/ | formPage.vue | 信息录入相关组件 | | ||
| 16 | -| layout/ | AppLayout.vue、BottomNav.vue | 页面布局与底部导航 | | ||
| 17 | -| media/ | AudioPlayer.vue、VideoPlayer.vue、PdfPreview.vue、OfficeViewer.vue | 音视频播放器与文档预览 | | ||
| 18 | -| payment/ | WechatPayment.vue | 微信支付相关组件 | | ||
| 19 | -| poster/ | RecallPoster.vue、SharePoster.vue | 海报生成与分享相关组件 | | ||
| 20 | -| studyDetail/ | StudyCatalogPopup.vue、StudyCommentsSection.vue、StudyMaterialsPopup.vue | 学习详情页的弹窗与评论区 | | ||
| 21 | -| teacher/ | TaskFilter.vue、TaskCascaderFilter.vue | 教师端筛选与任务相关组件 | | ||
| 22 | - | ||
| 23 | -## 备注 | ||
| 24 | - | ||
| 25 | -- 布局目录已归一:统一使用 [/src/components/layout](file:///Users/huyirui/program/itomix/git/mlaj/src/components/layout),已移除 /src/layouts |
docs/TODO.md
deleted
100644 → 0
| 1 | -用户反馈: |
docs/plan/暂存用户打卡信息.md
deleted
100644 → 0
| 1 | -# 暂存用户打卡信息 | ||
| 2 | - | ||
| 3 | -## 背景 | ||
| 4 | - | ||
| 5 | -用户在“提交作业/打卡”页面输入了较长文字并上传了媒体,但在未点击提交时被中断(误触返回、微信进程被系统回收、来电/切后台等),再次进入页面内容丢失,导致体验断裂。 | ||
| 6 | - | ||
| 7 | -本规划目标是在不改动后端接口的前提下,在前端提供“草稿暂存(文本 + 已上传媒体信息)”能力,支持一周内自动过期清理,并在用户再次进入时提示恢复或删除。 | ||
| 8 | - | ||
| 9 | -涉及页面与核心逻辑参考: | ||
| 10 | - | ||
| 11 | -- 打卡提交页:[CheckinDetailPage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/checkin/CheckinDetailPage.vue) | ||
| 12 | -- 打卡核心逻辑:[useCheckin.js](file:///Users/huyirui/program/itomix/git/mlaj/src/composables/useCheckin.js) | ||
| 13 | -- 环境变量约定:[.env](file:///Users/huyirui/program/itomix/git/mlaj/.env) | ||
| 14 | - | ||
| 15 | -## 需求拆解(逐条对齐) | ||
| 16 | - | ||
| 17 | -1. 缓存最多保存一周:写入时记录 saved_at,读写时执行过期清理(>7天删除)。 | ||
| 18 | -2. 用户提交后清空缓存:提交成功(code===1)后删除对应草稿。 | ||
| 19 | -3. 进入页面若存在未完成信息:弹框提示“继续/删除”,继续则回填,删除则清空。 | ||
| 20 | -4. 功能开关放到 .env:新增 VITE_CHECKIN_DRAFT_CACHE(0/1),默认建议 1(也可先默认 0,灰度开启)。 | ||
| 21 | - | ||
| 22 | -## 范围与不做项(第一版) | ||
| 23 | - | ||
| 24 | -- 覆盖:文字内容、已上传成功的媒体(含 url/meta_id/file_type/name)。 | ||
| 25 | -- 不覆盖:未上传完成的 File/Blob(localStorage 无法可靠持久化;要支持需 IndexedDB 存 Blob,复杂度与风险较高)。 | ||
| 26 | -- 编辑模式(route.query.status===edit):第一版建议默认不启用草稿恢复,避免与“编辑回显(来自后端)”冲突;若要覆盖编辑场景,采用独立 key(见“扩展”)。 | ||
| 27 | - | ||
| 28 | -## 关键设计 | ||
| 29 | - | ||
| 30 | -### 1) 存储介质 | ||
| 31 | - | ||
| 32 | -- 使用 localStorage:实现成本低,满足“一周”与“断网/切后台后仍可恢复”。 | ||
| 33 | -- 数据量控制:只存“已上传成功”的附件元数据;不存 File 本体。 | ||
| 34 | - | ||
| 35 | -### 2) 草稿 Key 设计(避免串号) | ||
| 36 | - | ||
| 37 | -建议 key 包含用户与作业上下文,确保不同用户/不同作业互不影响: | ||
| 38 | - | ||
| 39 | -- 前缀:CHECKIN_DRAFT_V1 | ||
| 40 | -- 维度:user_id、task_id、date、task_type、status | ||
| 41 | - | ||
| 42 | -示例: | ||
| 43 | - | ||
| 44 | -- CHECKIN_DRAFT_V1:{user_id}:{task_id}:{date}:{task_type}:{status} | ||
| 45 | - | ||
| 46 | -其中: | ||
| 47 | - | ||
| 48 | -- user_id:来自 currentUser(contexts/auth.js 本地持久化) | ||
| 49 | -- task_id/date/task_type/status:来自路由 query(CheckinDetailPage 已使用 route.query.task_id/date/task_type/status) | ||
| 50 | - | ||
| 51 | -### 3) 数据结构(建议) | ||
| 52 | - | ||
| 53 | -```json | ||
| 54 | -{ | ||
| 55 | - "version": 1, | ||
| 56 | - "saved_at": 1730000000000, | ||
| 57 | - "expires_at": 1730000000000, | ||
| 58 | - "context": { | ||
| 59 | - "user_id": "123", | ||
| 60 | - "task_id": "456", | ||
| 61 | - "date": "2026-01-25", | ||
| 62 | - "task_type": "upload", | ||
| 63 | - "status": "create" | ||
| 64 | - }, | ||
| 65 | - "payload": { | ||
| 66 | - "message": "...", | ||
| 67 | - "active_type": "image", | ||
| 68 | - "subtask_id": "789", | ||
| 69 | - "file_list": [ | ||
| 70 | - { | ||
| 71 | - "meta_id": "xxx", | ||
| 72 | - "url": "https://...", | ||
| 73 | - "name": "a.jpg", | ||
| 74 | - "file_type": "image" | ||
| 75 | - } | ||
| 76 | - ], | ||
| 77 | - "count": { | ||
| 78 | - "gratitude_count": 1, | ||
| 79 | - "gratitude_form_list": [] | ||
| 80 | - } | ||
| 81 | - } | ||
| 82 | -} | ||
| 83 | -``` | ||
| 84 | - | ||
| 85 | -说明: | ||
| 86 | - | ||
| 87 | -- file_list:仅保存 useCheckin.afterRead 上传成功后写入的字段(item.status===done 且 meta_id 存在)。 | ||
| 88 | -- count:来源于 CheckinDetailPage 的 selectedTargets/countValue(第一版可以先不存,或存但不影响非 count 类型)。 | ||
| 89 | - | ||
| 90 | -### 4) 触发保存的时机(自动暂存) | ||
| 91 | - | ||
| 92 | -- 文本变化:watch(message) debounce 500ms 保存。 | ||
| 93 | -- 附件变化:watch(fileList) 深度监听 debounce 500ms 保存(仅保存 done 项)。 | ||
| 94 | -- 作业选择变化:watch(selectedTaskValue) debounce 200ms 保存。 | ||
| 95 | -- 页面离开兜底:beforeRouteLeave 或 window.pagehide/visibilitychange 时强制保存一次(避免最后一次变更没落盘)。 | ||
| 96 | - | ||
| 97 | -落盘时机要遵循开关:VITE_CHECKIN_DRAFT_CACHE === '1' 才启用。 | ||
| 98 | - | ||
| 99 | -### 5) 弹框提示与回填流程 | ||
| 100 | - | ||
| 101 | -进入 [CheckinDetailPage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/checkin/CheckinDetailPage.vue)(且非 edit 模式)时: | ||
| 102 | - | ||
| 103 | -1. 读取 key 对应草稿;若不存在或已过期,直接进入正常流程。 | ||
| 104 | -2. 若存在草稿且 payload 有实际内容(message 有非空或 file_list 非空):弹框提示。 | ||
| 105 | -3. 用户选择: | ||
| 106 | - - 继续:将草稿回填到 message / activeType / fileList / selectedTaskValue(以及 count 数据如启用),并立刻再保存一次(避免回填后又丢)。 | ||
| 107 | - - 删除:删除草稿并保持空表单。 | ||
| 108 | - | ||
| 109 | -弹框建议用 showConfirmDialog(Vant 4),取消分支需要 catch,避免控制台出现 Uncaught (in promise) cancel(Vant 文档:showConfirmDialog.then/catch)[3](https://develop365.gitlab.io/vant/zh-CN/dialog/)。 | ||
| 110 | - | ||
| 111 | -### 6) 清理策略(“定期清除一周前”) | ||
| 112 | - | ||
| 113 | -采用“惰性清理 + 低频全量清理”组合: | ||
| 114 | - | ||
| 115 | -- 惰性清理:每次读/写草稿时,如果 expires_at < now 则删除。 | ||
| 116 | -- 低频全量:进入打卡页时,扫描 localStorage 中以 CHECKIN_DRAFT_V1: 开头的 key,删除所有过期项。 | ||
| 117 | - | ||
| 118 | -说明:localStorage 没有内建 TTL,必须业务侧维护 expires_at。 | ||
| 119 | - | ||
| 120 | -### 7) 提交成功后清空 | ||
| 121 | - | ||
| 122 | -清空动作必须绑定到“真正提交成功”之后: | ||
| 123 | - | ||
| 124 | -- 建议在 useCheckin.onSubmit 中,当 add/edit API 返回 code===1 且后续逻辑准备 router.back 前,删除对应 key。 | ||
| 125 | - | ||
| 126 | -这样可覆盖“不同入口页复用 onSubmit”以及“提交后立即返回上一页”的场景。 | ||
| 127 | - | ||
| 128 | -## 开发步骤(可落地的实现顺序) | ||
| 129 | - | ||
| 130 | -### 第 0 步:验证手段先行(TDD) | ||
| 131 | - | ||
| 132 | -新增 Vitest 用例,先定义以下可验证点: | ||
| 133 | - | ||
| 134 | -- 写入后能读取同一 key 的草稿;过期后读取返回空且自动删除。 | ||
| 135 | -- 仅保存 status===done 且含 meta_id 的附件。 | ||
| 136 | -- 清理函数能删除所有过期 key,不误删其他业务 localStorage。 | ||
| 137 | -- 提交成功时会调用清理(可通过 mock API 返回 code===1 验证)。 | ||
| 138 | - | ||
| 139 | -### 第 1 步:抽离草稿存储模块 | ||
| 140 | - | ||
| 141 | -位置建议:src/utils/checkinDraftCache.js(纯函数、无 UI 依赖)。 | ||
| 142 | - | ||
| 143 | -对外 API(示例): | ||
| 144 | - | ||
| 145 | -- is_enabled(): boolean(读取 env + 可选 query override) | ||
| 146 | -- build_key(context): string | ||
| 147 | -- save_draft(key, draft) | ||
| 148 | -- read_draft(key): draft|null(含 TTL 处理) | ||
| 149 | -- clear_draft(key) | ||
| 150 | -- cleanup_expired(prefix) | ||
| 151 | - | ||
| 152 | -### 第 2 步:在 CheckinDetailPage 接入“检测 + 弹框 + 回填” | ||
| 153 | - | ||
| 154 | -- onMounted:初始化后读取草稿并弹框。 | ||
| 155 | -- 回填时机:建议在任务详情/子任务列表加载完成后再回填 selectedTaskValue,避免 option 未加载导致显示异常。 | ||
| 156 | - | ||
| 157 | -### 第 3 步:在 CheckinDetailPage 接入“自动保存” | ||
| 158 | - | ||
| 159 | -- 对 message/fileList/selectedTaskValue/countValue/selectedTargets 建立 watch + debounce。 | ||
| 160 | -- 页面离开事件兜底(pagehide/visibilitychange)。 | ||
| 161 | - | ||
| 162 | -### 第 4 步:在 useCheckin.onSubmit 接入“成功清理” | ||
| 163 | - | ||
| 164 | -- onSubmit 成功分支清除草稿。 | ||
| 165 | -- 失败分支不清除,保留草稿以便重试。 | ||
| 166 | - | ||
| 167 | -## 边界条件与遗漏点梳理(建议补齐) | ||
| 168 | - | ||
| 169 | -1. 多用户切换:key 必须含 user_id,否则会串草稿。 | ||
| 170 | -2. 多任务并存:key 必须含 task_id/date/task_type,否则会在不同作业之间误恢复。 | ||
| 171 | -3. 附件未上传完成: | ||
| 172 | - - 仅保存已上传成功的项;如果用户退出时仍有 uploading 项,恢复后无法找回该 File。 | ||
| 173 | - - 可在保存时统计未保存数量,并在恢复弹框里追加提示“有 X 个附件上传未完成未被暂存”。 | ||
| 174 | -4. 关闭开关后的行为: | ||
| 175 | - - 关闭后不再读/写;建议仍执行一次 cleanup_expired,避免历史堆积。 | ||
| 176 | -5. 版本升级/数据结构变更:draft.version 不匹配时丢弃并清除,避免解析异常。 | ||
| 177 | -6. localStorage 配额:图片多但只存 url/meta_id 一般不会超;仍需 try/catch JSON 与 setItem 异常。 | ||
| 178 | -7. 编辑模式: | ||
| 179 | - - 要支持“编辑中断恢复”,建议 key 加 post_id 维度,并在 initEditData 回显后再弹框询问是否覆盖当前表单。 | ||
| 180 | - | ||
| 181 | -## 环境变量(规划) | ||
| 182 | - | ||
| 183 | -在 [.env](file:///Users/huyirui/program/itomix/git/mlaj/.env) 增加: | ||
| 184 | - | ||
| 185 | -- VITE_CHECKIN_DRAFT_CACHE = 1 | ||
| 186 | - | ||
| 187 | -约定: | ||
| 188 | - | ||
| 189 | -- '1' 开启,'0' 关闭 | ||
| 190 | -- 可选增加 URL 覆盖用于灰度测试:?enable_draft=1 / ?enable_draft=0(模式同 VITE_CHECKIN_MULTI_ATTACHMENT) |
-
Please register or login to post a comment