feat: 添加 Speckit 相关配置与实现文件
新增 Speckit 工具链相关文件,包括: - 添加 specs/.gitkeep 文件 - 在 package.json 中添加 mcp:speckit 脚本 - 添加 .github/prompts 下的多个 prompt 文件 - 添加 .specify/memory/constitution.md 项目原则文件 - 添加 src/docs 下的 Speckit 测试文档和硬核模板 - 实现 mcp/speckit_stdio_server.js 服务端逻辑
Showing
12 changed files
with
851 additions
and
0 deletions
.github/prompts/speckit.implement.prompt.md
0 → 100644
| 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 | +- 不要启动或重启任何服务;不要引导打开预览 |
.github/prompts/speckit.plan.prompt.md
0 → 100644
| 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) 影响范围与回归清单 |
.github/prompts/speckit.specify.prompt.md
0 → 100644
| 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] ... |
.github/prompts/speckit.tasks.prompt.md
0 → 100644
| 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
0 → 100644
| 1 | +## 项目原则 | ||
| 2 | + | ||
| 3 | +- 以用户需求为中心,优先实现最小可用闭环,避免过度设计 | ||
| 4 | +- 修改应尽量小且可回滚,避免大范围重构 | ||
| 5 | +- 保持现有代码风格与目录结构一致,优先复用已有工具与组件 | ||
| 6 | +- 生产代码完毕后不要重启服务 | ||
| 7 | +- 操作完成后不要自动打开预览 | ||
| 8 | + | ||
| 9 | +## 技术与工程约束 | ||
| 10 | + | ||
| 11 | +- 依赖管理使用 pnpm | ||
| 12 | +- 构建工具使用 Vite | ||
| 13 | +- 前端使用 Vue3(优先 setup 语法糖) | ||
| 14 | +- 移动端 UI 优先使用 Vant | ||
| 15 | +- 样式使用 Less;如果项目已引入 TailwindCSS,布局优先用 Tailwind 类名,细节用 Less 补充 | ||
| 16 | +- 路由使用 vue-router | ||
| 17 | +- 状态管理使用 pinia(如项目已接入) | ||
| 18 | +- 不引入不必要的新库;如必须新增依赖,需要在方案中说明原因与替代方案 | ||
| 19 | + | ||
| 20 | +## 接口与数据约束 | ||
| 21 | + | ||
| 22 | +- 所有网络请求返回对象结构必须为 { code, data, msg } | ||
| 23 | +- code=1 表示成功,其他值表示失败;失败时保持 msg 可读 | ||
| 24 | + | ||
| 25 | +## 资源与链接约束 | ||
| 26 | + | ||
| 27 | +- 若图片域名为 cdn.ipadbiz.cn,需要自动追加 ?imageMogr2/thumbnail/200x/strip/quality/70 | ||
| 28 | + |
mcp/speckit_stdio_server.js
0 → 100644
| 1 | +import { spawn } from 'node:child_process' | ||
| 2 | +import path from 'node:path' | ||
| 3 | +import { fileURLToPath } from 'node:url' | ||
| 4 | + | ||
| 5 | +const PROTOCOL_VERSIONS = ['2024-11-05', '2025-06-18'] | ||
| 6 | + | ||
| 7 | +const server_info = { | ||
| 8 | + name: 'speckit-cli-mcp', | ||
| 9 | + title: 'Speckit CLI MCP Server', | ||
| 10 | + version: '0.1.0', | ||
| 11 | +} | ||
| 12 | + | ||
| 13 | +const module_dirname = path.dirname(fileURLToPath(import.meta.url)) | ||
| 14 | +const workspace_root = path.resolve(module_dirname, '..') | ||
| 15 | + | ||
| 16 | +process.stderr.write(`[speckit-cli-mcp] cwd=${process.cwd()} workspace_root=${workspace_root}\n`) | ||
| 17 | + | ||
| 18 | +const send_json = (payload) => { | ||
| 19 | + process.stdout.write(`${JSON.stringify(payload)}\n`) | ||
| 20 | +} | ||
| 21 | + | ||
| 22 | +const send_result = (id, result) => { | ||
| 23 | + send_json({ jsonrpc: '2.0', id, result }) | ||
| 24 | +} | ||
| 25 | + | ||
| 26 | +const send_error = (id, code, message, data) => { | ||
| 27 | + const error = { code, message } | ||
| 28 | + if (data !== undefined) { | ||
| 29 | + error.data = data | ||
| 30 | + } | ||
| 31 | + send_json({ jsonrpc: '2.0', id, error }) | ||
| 32 | +} | ||
| 33 | + | ||
| 34 | +const as_text_content = (obj) => { | ||
| 35 | + return { | ||
| 36 | + content: [ | ||
| 37 | + { | ||
| 38 | + type: 'text', | ||
| 39 | + text: JSON.stringify(obj, null, 4), | ||
| 40 | + }, | ||
| 41 | + ], | ||
| 42 | + isError: obj.code !== 1, | ||
| 43 | + } | ||
| 44 | +} | ||
| 45 | + | ||
| 46 | +const normalize_subcommand = (subcommand) => { | ||
| 47 | + if (typeof subcommand !== 'string') { | ||
| 48 | + return null | ||
| 49 | + } | ||
| 50 | + const trimmed = subcommand.trim() | ||
| 51 | + if (!trimmed) { | ||
| 52 | + return null | ||
| 53 | + } | ||
| 54 | + return trimmed | ||
| 55 | +} | ||
| 56 | + | ||
| 57 | +const validate_args = (args) => { | ||
| 58 | + if (args === undefined) { | ||
| 59 | + return { ok: true, value: [] } | ||
| 60 | + } | ||
| 61 | + if (!Array.isArray(args)) { | ||
| 62 | + return { ok: false, message: 'args 必须为字符串数组' } | ||
| 63 | + } | ||
| 64 | + for (const item of args) { | ||
| 65 | + if (typeof item !== 'string') { | ||
| 66 | + return { ok: false, message: 'args 必须为字符串数组' } | ||
| 67 | + } | ||
| 68 | + if (item.length > 200) { | ||
| 69 | + return { ok: false, message: 'args 单项长度不能超过 200' } | ||
| 70 | + } | ||
| 71 | + } | ||
| 72 | + return { ok: true, value: args } | ||
| 73 | +} | ||
| 74 | + | ||
| 75 | +const resolve_cwd = (cwd_input) => { | ||
| 76 | + if (!cwd_input) { | ||
| 77 | + return workspace_root | ||
| 78 | + } | ||
| 79 | + if (typeof cwd_input !== 'string') { | ||
| 80 | + return null | ||
| 81 | + } | ||
| 82 | + const resolved = path.resolve(workspace_root, cwd_input) | ||
| 83 | + if (!resolved.startsWith(workspace_root)) { | ||
| 84 | + return null | ||
| 85 | + } | ||
| 86 | + return resolved | ||
| 87 | +} | ||
| 88 | + | ||
| 89 | +const run_specify = async ({ cli, subcommand, args, cwd, timeout_ms }) => { | ||
| 90 | + const cli_bin = cli === 'speckit' ? 'speckit' : 'specify' | ||
| 91 | + const allowed_subcommands = new Set(['check', 'init']) | ||
| 92 | + const normalized_subcommand = normalize_subcommand(subcommand) | ||
| 93 | + if (!normalized_subcommand || !allowed_subcommands.has(normalized_subcommand)) { | ||
| 94 | + return { code: 0, data: null, msg: `不支持的子命令:${subcommand}` } | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + const validated_args = validate_args(args) | ||
| 98 | + if (!validated_args.ok) { | ||
| 99 | + return { code: 0, data: null, msg: validated_args.message } | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + const resolved_cwd = resolve_cwd(cwd) | ||
| 103 | + if (!resolved_cwd) { | ||
| 104 | + return { code: 0, data: null, msg: 'cwd 非法,必须为工作区内的相对路径' } | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + const final_timeout_ms = Number.isFinite(timeout_ms) ? Math.max(1000, timeout_ms) : 10 * 60 * 1000 | ||
| 108 | + const final_args = [normalized_subcommand, ...validated_args.value] | ||
| 109 | + | ||
| 110 | + return await new Promise((resolve) => { | ||
| 111 | + const child = spawn(cli_bin, final_args, { | ||
| 112 | + cwd: resolved_cwd, | ||
| 113 | + env: process.env, | ||
| 114 | + }) | ||
| 115 | + | ||
| 116 | + let stdout = '' | ||
| 117 | + let stderr = '' | ||
| 118 | + let timed_out = false | ||
| 119 | + | ||
| 120 | + const timer = setTimeout(() => { | ||
| 121 | + timed_out = true | ||
| 122 | + child.kill('SIGTERM') | ||
| 123 | + }, final_timeout_ms) | ||
| 124 | + | ||
| 125 | + child.stdout.on('data', (chunk) => { | ||
| 126 | + stdout += chunk.toString() | ||
| 127 | + if (stdout.length > 2_000_000) { | ||
| 128 | + stdout = `${stdout.slice(0, 2_000_000)}\n...[stdout 截断]` | ||
| 129 | + } | ||
| 130 | + }) | ||
| 131 | + | ||
| 132 | + child.stderr.on('data', (chunk) => { | ||
| 133 | + stderr += chunk.toString() | ||
| 134 | + if (stderr.length > 2_000_000) { | ||
| 135 | + stderr = `${stderr.slice(0, 2_000_000)}\n...[stderr 截断]` | ||
| 136 | + } | ||
| 137 | + }) | ||
| 138 | + | ||
| 139 | + child.on('error', (err) => { | ||
| 140 | + clearTimeout(timer) | ||
| 141 | + resolve({ | ||
| 142 | + code: 0, | ||
| 143 | + data: { | ||
| 144 | + cwd: resolved_cwd, | ||
| 145 | + cli: cli_bin, | ||
| 146 | + args: final_args, | ||
| 147 | + stdout, | ||
| 148 | + stderr, | ||
| 149 | + error: String(err?.message || err), | ||
| 150 | + }, | ||
| 151 | + msg: '命令启动失败(请确认已安装 specify/speckit)', | ||
| 152 | + }) | ||
| 153 | + }) | ||
| 154 | + | ||
| 155 | + child.on('close', (exit_code, signal) => { | ||
| 156 | + clearTimeout(timer) | ||
| 157 | + resolve({ | ||
| 158 | + code: exit_code === 0 && !timed_out ? 1 : 0, | ||
| 159 | + data: { | ||
| 160 | + cwd: resolved_cwd, | ||
| 161 | + cli: cli_bin, | ||
| 162 | + args: final_args, | ||
| 163 | + exit_code, | ||
| 164 | + signal, | ||
| 165 | + timed_out, | ||
| 166 | + stdout, | ||
| 167 | + stderr, | ||
| 168 | + }, | ||
| 169 | + msg: exit_code === 0 && !timed_out ? 'ok' : '命令执行失败', | ||
| 170 | + }) | ||
| 171 | + }) | ||
| 172 | + }) | ||
| 173 | +} | ||
| 174 | + | ||
| 175 | +const list_tools = () => { | ||
| 176 | + return [ | ||
| 177 | + { | ||
| 178 | + name: 'speckit_check', | ||
| 179 | + description: '运行 specify check(检查工具链是否齐全)', | ||
| 180 | + inputSchema: { | ||
| 181 | + type: 'object', | ||
| 182 | + properties: { | ||
| 183 | + cli: { type: 'string', enum: ['specify', 'speckit'], default: 'specify' }, | ||
| 184 | + cwd: { type: 'string', description: '相对工作区路径(可选)' }, | ||
| 185 | + timeout_ms: { type: 'number', description: '超时时间(毫秒,可选)' }, | ||
| 186 | + }, | ||
| 187 | + required: [], | ||
| 188 | + }, | ||
| 189 | + }, | ||
| 190 | + { | ||
| 191 | + name: 'speckit_init_here', | ||
| 192 | + description: '运行 specify init --here(初始化/更新 Spec Kit 文件)', | ||
| 193 | + inputSchema: { | ||
| 194 | + type: 'object', | ||
| 195 | + properties: { | ||
| 196 | + cli: { type: 'string', enum: ['specify', 'speckit'], default: 'specify' }, | ||
| 197 | + ai: { type: 'string', default: 'copilot' }, | ||
| 198 | + force: { type: 'boolean', default: false }, | ||
| 199 | + cwd: { type: 'string', description: '相对工作区路径(可选)' }, | ||
| 200 | + timeout_ms: { type: 'number', description: '超时时间(毫秒,可选)' }, | ||
| 201 | + }, | ||
| 202 | + required: [], | ||
| 203 | + }, | ||
| 204 | + }, | ||
| 205 | + { | ||
| 206 | + name: 'speckit_run', | ||
| 207 | + description: '运行 allowlist 内的 specify 子命令(check/init)', | ||
| 208 | + inputSchema: { | ||
| 209 | + type: 'object', | ||
| 210 | + properties: { | ||
| 211 | + cli: { type: 'string', enum: ['specify', 'speckit'], default: 'specify' }, | ||
| 212 | + subcommand: { type: 'string', enum: ['check', 'init'] }, | ||
| 213 | + args: { type: 'array', items: { type: 'string' } }, | ||
| 214 | + cwd: { type: 'string', description: '相对工作区路径(可选)' }, | ||
| 215 | + timeout_ms: { type: 'number', description: '超时时间(毫秒,可选)' }, | ||
| 216 | + }, | ||
| 217 | + required: ['subcommand'], | ||
| 218 | + }, | ||
| 219 | + }, | ||
| 220 | + ] | ||
| 221 | +} | ||
| 222 | + | ||
| 223 | +const handle_request = async (msg) => { | ||
| 224 | + const { id, method, params } = msg || {} | ||
| 225 | + | ||
| 226 | + if (method === 'initialize') { | ||
| 227 | + const requested = params?.protocolVersion | ||
| 228 | + const negotiated = PROTOCOL_VERSIONS.includes(requested) ? requested : PROTOCOL_VERSIONS[0] | ||
| 229 | + send_result(id, { | ||
| 230 | + protocolVersion: negotiated, | ||
| 231 | + capabilities: { | ||
| 232 | + tools: { listChanged: false }, | ||
| 233 | + }, | ||
| 234 | + serverInfo: server_info, | ||
| 235 | + instructions: '仅提供 Speckit/Spec Kit CLI 的最小封装工具(check/init)。', | ||
| 236 | + }) | ||
| 237 | + return | ||
| 238 | + } | ||
| 239 | + | ||
| 240 | + if (method === 'ping') { | ||
| 241 | + send_result(id, {}) | ||
| 242 | + return | ||
| 243 | + } | ||
| 244 | + | ||
| 245 | + if (method === 'tools/list') { | ||
| 246 | + send_result(id, { tools: list_tools() }) | ||
| 247 | + return | ||
| 248 | + } | ||
| 249 | + | ||
| 250 | + if (method === 'tools/call') { | ||
| 251 | + const name = params?.name | ||
| 252 | + const args = params?.arguments || {} | ||
| 253 | + | ||
| 254 | + if (name === 'speckit_check') { | ||
| 255 | + const res = await run_specify({ | ||
| 256 | + cli: args.cli, | ||
| 257 | + subcommand: 'check', | ||
| 258 | + args: [], | ||
| 259 | + cwd: args.cwd, | ||
| 260 | + timeout_ms: args.timeout_ms, | ||
| 261 | + }) | ||
| 262 | + send_result(id, as_text_content(res)) | ||
| 263 | + return | ||
| 264 | + } | ||
| 265 | + | ||
| 266 | + if (name === 'speckit_init_here') { | ||
| 267 | + const force_flag = args.force ? ['--force'] : [] | ||
| 268 | + const ai_value = typeof args.ai === 'string' && args.ai.trim() ? args.ai.trim() : 'copilot' | ||
| 269 | + const res = await run_specify({ | ||
| 270 | + cli: args.cli, | ||
| 271 | + subcommand: 'init', | ||
| 272 | + args: ['--here', '--ai', ai_value, ...force_flag], | ||
| 273 | + cwd: args.cwd, | ||
| 274 | + timeout_ms: args.timeout_ms, | ||
| 275 | + }) | ||
| 276 | + send_result(id, as_text_content(res)) | ||
| 277 | + return | ||
| 278 | + } | ||
| 279 | + | ||
| 280 | + if (name === 'speckit_run') { | ||
| 281 | + const res = await run_specify({ | ||
| 282 | + cli: args.cli, | ||
| 283 | + subcommand: args.subcommand, | ||
| 284 | + args: args.args, | ||
| 285 | + cwd: args.cwd, | ||
| 286 | + timeout_ms: args.timeout_ms, | ||
| 287 | + }) | ||
| 288 | + send_result(id, as_text_content(res)) | ||
| 289 | + return | ||
| 290 | + } | ||
| 291 | + | ||
| 292 | + send_error(id, -32602, `Unknown tool: ${name}`) | ||
| 293 | + return | ||
| 294 | + } | ||
| 295 | + | ||
| 296 | + if (id !== undefined) { | ||
| 297 | + send_error(id, -32601, `Method not found: ${method}`) | ||
| 298 | + } | ||
| 299 | +} | ||
| 300 | + | ||
| 301 | +let buffer = '' | ||
| 302 | +process.stdin.setEncoding('utf8') | ||
| 303 | +process.stdin.on('data', async (chunk) => { | ||
| 304 | + buffer += chunk | ||
| 305 | + while (buffer.indexOf('\n') !== -1) { | ||
| 306 | + const idx = buffer.indexOf('\n') | ||
| 307 | + const line = buffer.slice(0, idx).trim() | ||
| 308 | + buffer = buffer.slice(idx + 1) | ||
| 309 | + if (!line) { | ||
| 310 | + continue | ||
| 311 | + } | ||
| 312 | + let msg | ||
| 313 | + try { | ||
| 314 | + msg = JSON.parse(line) | ||
| 315 | + } catch (e) { | ||
| 316 | + continue | ||
| 317 | + } | ||
| 318 | + await handle_request(msg) | ||
| 319 | + } | ||
| 320 | +}) | ||
| 321 | + | ||
| 322 | +process.stdin.on('end', () => { | ||
| 323 | + process.exit(0) | ||
| 324 | +}) |
| ... | @@ -8,6 +8,7 @@ | ... | @@ -8,6 +8,7 @@ |
| 8 | "build": ". ~/.nvm/nvm.sh && nvm use 18.19.1 && vite build", | 8 | "build": ". ~/.nvm/nvm.sh && nvm use 18.19.1 && vite build", |
| 9 | "preview": "vite preview", | 9 | "preview": "vite preview", |
| 10 | "test": "vitest run", | 10 | "test": "vitest run", |
| 11 | + "mcp:speckit": "node mcp/speckit_stdio_server.js", | ||
| 11 | "tar": "tar -czvpf dist.tar.gz mlaj", | 12 | "tar": "tar -czvpf dist.tar.gz mlaj", |
| 12 | "build_tar": "npm run build && npm run tar", | 13 | "build_tar": "npm run build && npm run tar", |
| 13 | "scp-dev": "scp dist.tar.gz ipadbiz-inner:/opt/space-dev/f", | 14 | "scp-dev": "scp dist.tar.gz ipadbiz-inner:/opt/space-dev/f", | ... | ... |
specs/.gitkeep
0 → 100644
| 1 | +. |
| 1 | +# 001-recall-activity-history-state - 活动历史页空状态与加载失败提示 | ||
| 2 | + | ||
| 3 | +## 1) 背景与目标 | ||
| 4 | + | ||
| 5 | +- 背景: | ||
| 6 | + - 当前“活动历史”页面会在进入时请求活动列表数据并渲染列表。 | ||
| 7 | + - 当接口返回无数据或请求失败时,页面缺少清晰的页面内反馈,用户难以判断是“没有活动记录”还是“加载失败”。 | ||
| 8 | +- 目标: | ||
| 9 | + - 在不改接口、不新增接口的前提下,为活动历史页补齐“空状态提示”和“加载失败提示 + 重试入口”,提升可理解性与可恢复性。 | ||
| 10 | +- 非目标: | ||
| 11 | + - 不调整接口入参、返回结构或后端逻辑。 | ||
| 12 | + - 不改动页面既有的顶部 Banner、底部按钮与“没找到我的星球活动”弹窗等核心结构与入口。 | ||
| 13 | + - 不引入新的页面路由与新的业务流程。 | ||
| 14 | + | ||
| 15 | +## 2) 用户画像与使用场景 | ||
| 16 | + | ||
| 17 | +- 已登录/已完成必要信息的用户,通过召回链路进入活动历史页,查看历史参与记录。 | ||
| 18 | +- 用户可能出现以下情况: | ||
| 19 | + - 确实没有参与过活动(应看到“暂无活动记录”的空状态)。 | ||
| 20 | + - 网络不稳定、服务异常、参数缺失导致请求失败(应看到“加载失败”的错误提示,并可重试)。 | ||
| 21 | + | ||
| 22 | +## 3) 需求范围 | ||
| 23 | + | ||
| 24 | +- 页面/入口: | ||
| 25 | + - 活动历史页(现有页面入口保持不变)。 | ||
| 26 | +- 核心流程: | ||
| 27 | + - 页面进入后发起列表加载。 | ||
| 28 | + - 加载成功: | ||
| 29 | + - 有数据:展示列表。 | ||
| 30 | + - 无数据:展示空状态。 | ||
| 31 | + - 加载失败:展示失败提示与重试按钮。 | ||
| 32 | +- 状态与异常分支: | ||
| 33 | + - 请求失败(返回 code 不为 1 或发生异常)时进入失败状态。 | ||
| 34 | + - 重试成功后根据数据展示列表或空状态。 | ||
| 35 | + | ||
| 36 | +## 4) 用户故事(User Stories) | ||
| 37 | + | ||
| 38 | +- 作为用户,我想在没有活动记录时看到明确提示,以便确认自己没有缺失记录。 | ||
| 39 | +- 作为用户,我想在列表加载失败时看到错误提示并能重试,以便在网络恢复后继续查看记录。 | ||
| 40 | + | ||
| 41 | +## 5) 验收标准(Acceptance Criteria) | ||
| 42 | + | ||
| 43 | +- 有数据时: | ||
| 44 | + - 页面展示活动列表。 | ||
| 45 | + - 不展示空状态与失败状态提示。 | ||
| 46 | +- 无数据时: | ||
| 47 | + - 当列表加载已完成且列表为空时,页面在列表区域展示空状态提示文案(例如“暂无活动记录”)。 | ||
| 48 | + - 空状态为页面内可见提示,不以 Toast 作为唯一反馈。 | ||
| 49 | +- 加载失败时: | ||
| 50 | + - 当列表请求失败(code!=1 或发生异常)时,页面在列表区域展示失败状态提示文案(例如“加载失败,请稍后重试”)。 | ||
| 51 | + - 失败提示区域提供“重试”按钮;点击后会重新发起列表请求。 | ||
| 52 | + - 重试成功后: | ||
| 53 | + - 有数据则展示列表; | ||
| 54 | + - 无数据则展示空状态; | ||
| 55 | + - 不再展示失败提示。 | ||
| 56 | +- 结构与入口: | ||
| 57 | + - 顶部 Banner、底部按钮、“没找到我的星球活动”入口与弹窗行为不受影响,保持可用。 | ||
| 58 | + | ||
| 59 | +## 6) 交互与视觉要点 | ||
| 60 | + | ||
| 61 | +- 空状态与失败状态展示位置: | ||
| 62 | + - 在列表区域内展示,避免遮挡顶部 Banner 和底部固定按钮。 | ||
| 63 | +- 可读性: | ||
| 64 | + - 文案简洁、明确区分“无数据”与“加载失败”。 | ||
| 65 | +- 一致性: | ||
| 66 | + - 视觉风格与页面现有样式保持一致(背景、圆角卡片、间距等)。 | ||
| 67 | + | ||
| 68 | +## 7) 数据与接口 | ||
| 69 | + | ||
| 70 | +- 不新增接口、不修改接口。 | ||
| 71 | +- 数据来源仍为现有活动列表查询接口。 | ||
| 72 | +- 返回结构固定为 { code, data, msg }: | ||
| 73 | + - code=1 视为成功; | ||
| 74 | + - code!=1 视为失败; | ||
| 75 | + - msg 可作为失败提示文案来源之一(若为空则使用默认失败文案)。 | ||
| 76 | + | ||
| 77 | +## 8) 边界条件与风险 | ||
| 78 | + | ||
| 79 | +- 缓存参数缺失或不完整可能导致请求失败,应进入失败状态并可重试。 | ||
| 80 | +- 网络波动可能导致首次失败、重试成功,状态切换需要正确更新页面展示。 | ||
| 81 | + | ||
| 82 | +## 9) 待确认事项 | ||
| 83 | + | ||
| 84 | +- [NEEDS CLARIFICATION] 失败状态文案是否需要优先展示接口返回 msg?若 msg 为空是否统一使用默认文案? | ||
| 85 | +- [NEEDS CLARIFICATION] 空状态文案是否固定为“暂无活动记录”,还是需要根据场景区分(例如“未查询到您的历史活动”)? |
src/docs/Speckit 测试步骤.md
0 → 100644
| 1 | +## 步骤 0:选一个 feature_key | ||
| 2 | +建议用: | ||
| 3 | + | ||
| 4 | +- 001-recall-activity-history-state | ||
| 5 | +## 步骤 1:/speckit.specify(生成 spec) | ||
| 6 | +在 Trae 的 Copilot Chat 输入: | ||
| 7 | + | ||
| 8 | +``` | ||
| 9 | +/speckit.specify | ||
| 10 | +001-recall-activity-history-state | ||
| 11 | +ActivityHistoryPage 增加一个空状态提示 + 列表加 | ||
| 12 | +载失败提示(不改接口) | ||
| 13 | + | ||
| 14 | +背景: | ||
| 15 | +- 页面文件:src/views/recall/ | ||
| 16 | +ActivityHistoryPage.vue | ||
| 17 | +- 当前页面仅在 onMounted 调用 | ||
| 18 | +searchOldActivityAPI 获取 activities,没有 | ||
| 19 | +loading/empty/error 视图 | ||
| 20 | +- 接口返回结构为 { code, data, msg },不修改接口 | ||
| 21 | +与入参 | ||
| 22 | + | ||
| 23 | +需求: | ||
| 24 | +1) 空状态:当活动列表加载完成且无数据 | ||
| 25 | +(activities 为空)时,展示页面内空状态提示(不要 | ||
| 26 | +toast 代替)。 | ||
| 27 | +2) 加载失败:当 searchOldActivityAPI 请求失败 | ||
| 28 | +(code!=1 或抛异常)时,展示页面内错误提示,并提供 | ||
| 29 | +“重试”按钮;点击重试会重新发起列表请求。 | ||
| 30 | +3) 不影响页面现有结构:顶部 Banner、底部按钮、"没 | ||
| 31 | +找到我的星球活动" 弹窗等保持不变。 | ||
| 32 | +4) 样式:项目已引入 TailwindCSS,布局优先用 | ||
| 33 | +Tailwind 类;必要补充用 Less(保持现有文件风 | ||
| 34 | +格)。 | ||
| 35 | +5) 不新增接口、不改接口返回结构。 | ||
| 36 | +验收: | ||
| 37 | +- 正常有数据:列表正常展示,不显示空/错。 | ||
| 38 | +- 无数据:显示空状态文案(建议:暂无活动记录)。 | ||
| 39 | +- 请求失败:显示失败状态文案(建议:加载失败,请稍后 | ||
| 40 | +重试),点击重试后如果成功则显示列表/空状态。 | ||
| 41 | +``` | ||
| 42 | +预期产物: | ||
| 43 | + | ||
| 44 | +- specs/001-recall-activity-history-state/spec.md | ||
| 45 | +## 步骤 2:处理 [NEEDS CLARIFICATION](如果 Speckit 问你) | ||
| 46 | +它可能会问这些,你直接用下面“推荐回答”贴回去: | ||
| 47 | + | ||
| 48 | +- Q:失败判断是什么? A: searchOldActivityAPI 返回 code!==1 或请求抛异常都视为失败。 | ||
| 49 | +- Q:失败状态是否要展示 msg? A:优先展示接口 msg (若为空则用“加载失败,请稍后重试”)。 | ||
| 50 | +- Q:空状态是整页替换还是列表区域内? A:在“列表区域”显示空状态,不遮挡顶部 Banner 与底部按钮。 | ||
| 51 | +- Q:是否需要 loading 骨架/加载中? A:本次只做空状态与失败提示,不额外做骨架;但允许加一个轻量 loading 状态避免闪烁(可选,不作为验收点)。 | ||
| 52 | +## 步骤 3:/speckit.plan(生成 plan) | ||
| 53 | +在 Copilot Chat 输入: | ||
| 54 | + | ||
| 55 | +``` | ||
| 56 | +/speckit.plan | ||
| 57 | +001-recall-activity-history-state | ||
| 58 | + | ||
| 59 | +技术约束: | ||
| 60 | +- Vue3 <script setup>,不引入新库 | ||
| 61 | +- 仅修改 ActivityHistoryPage.vue(如确有必要允 | ||
| 62 | +许新增一个小的复用组件,但优先不新增文件) | ||
| 63 | +- 不启动/不重启服务,不打开预览 | ||
| 64 | +``` | ||
| 65 | +预期产物: | ||
| 66 | + | ||
| 67 | +- specs/001-recall-activity-history-state/plan.md | ||
| 68 | +## 步骤 4:/speckit.tasks(生成 tasks) | ||
| 69 | +在 Copilot Chat 输入: | ||
| 70 | + | ||
| 71 | +``` | ||
| 72 | +/speckit.tasks | ||
| 73 | +001-recall-activity-history-state | ||
| 74 | +``` | ||
| 75 | +预期产物: | ||
| 76 | + | ||
| 77 | +- specs/001-recall-activity-history-state/tasks.md | ||
| 78 | +## 步骤 5:/speckit.implement(按任务落地改代码) | ||
| 79 | +在 Copilot Chat 输入: | ||
| 80 | + | ||
| 81 | +``` | ||
| 82 | +/speckit.implement | ||
| 83 | +001-recall-activity-history-state | ||
| 84 | +``` | ||
| 85 | +你可以在实现阶段额外强调一句(避免它越界): | ||
| 86 | + | ||
| 87 | +- “只改 ActivityHistoryPage.vue,不要改接口,不要新增服务端逻辑,不要打开预览,不要重启任何服务。” | ||
| 88 | +## 步骤 6:自检(不启动服务) | ||
| 89 | +实现完成后只跑已有测试即可: | ||
| 90 | + | ||
| 91 | +``` | ||
| 92 | +pnpm test | ||
| 93 | +``` | ||
| 94 | +## 你改动点会落在哪里(便于你 review) | ||
| 95 | +Speckit 最终大概率会在这个文件里做这些事(都不改接口): | ||
| 96 | + | ||
| 97 | +- ActivityHistoryPage.vue | ||
| 98 | + - 增加 loading / load_error / error_msg 之类的状态 | ||
| 99 | + - 把 onMounted 里的拉取逻辑抽成 fetch_activities (带 try/catch) | ||
| 100 | + - 模板里在 List 区域根据状态展示: | ||
| 101 | + - 列表 | ||
| 102 | + - 空状态 | ||
| 103 | + - 失败状态 + 重试按钮 | ||
| 104 | +如果你愿意,我也可以直接给你一份“更硬核的 /speckit.specify 文案”(把每条验收标准写成更可验证的条件),这样后面 plan/tasks/implement 的输出会更稳定。 |
| 1 | +# 硬核 /speckit.specify 提示模板(可验证验收标准版) | ||
| 2 | + | ||
| 3 | +> 用途:帮助你在调用 `/speckit.specify` 时,把需求描述成**更可验证、更稳定**的规格说明,让后续 `/speckit.plan`、`/speckit.tasks`、`/speckit.implement` 的输出更可控。 | ||
| 4 | + | ||
| 5 | +--- | ||
| 6 | + | ||
| 7 | +## 一、使用方式总览 | ||
| 8 | + | ||
| 9 | +- 在聊天里调用命令时,将本模板中的「完整提示文案」作为补充说明一起发给智能体,例如: | ||
| 10 | + - `/speckit.specify 001-recall-activity-history-state ActivityHistoryPage 增加一个空状态提示 + 列表加载失败提示(不改接口)` | ||
| 11 | + - 然后追加一段话:「请严格按照《硬核 /speckit.specify 提示模板(可验证验收标准版)》来生成 specs/001-recall-activity-history-state/spec.md」 | ||
| 12 | +- 规格说明的目标: | ||
| 13 | + - 明确 **做什么 / 为什么做**,不写具体实现细节。 | ||
| 14 | + - 每条验收标准都能被前端 / 后端 / 测试同事用「条件 + 操作 + 预期结果」直接验证。 | ||
| 15 | + - 所有分支(成功 / 无数据 / 失败 / 边界情况)都有清晰的预期。 | ||
| 16 | + | ||
| 17 | +--- | ||
| 18 | + | ||
| 19 | +## 二、规格结构模板(spec.md 结构) | ||
| 20 | + | ||
| 21 | +输出文件固定为:`specs/<feature_key>/spec.md`,推荐结构如下: | ||
| 22 | + | ||
| 23 | +1. 背景与目标 | ||
| 24 | + - 背景:说明业务场景、已有行为中的问题或机会。 | ||
| 25 | + - 目标:本次改动要解决什么问题、带来什么价值。 | ||
| 26 | + - 非目标:本次明确**不做**的范围(例如「不改接口、不动路由、不调整整体布局」)。 | ||
| 27 | + | ||
| 28 | +2. 用户画像与使用场景 | ||
| 29 | + - 谁在用(角色、平台、入口)。 | ||
| 30 | + - 在什么场景下触发(来源页面、入口按钮、链路前后步骤)。 | ||
| 31 | + | ||
| 32 | +3. 需求范围 | ||
| 33 | + - 页面 / 入口:涉及到哪些页面、弹窗或组件。 | ||
| 34 | + - 核心流程:从用户视角按步骤描述主流程。 | ||
| 35 | + - 状态与异常分支:列出所有状态(正常、有数据、无数据、加载中、失败等)和切换条件。 | ||
| 36 | + | ||
| 37 | +4. 用户故事(User Stories) | ||
| 38 | + - 使用「作为……我想……以便……」的句式,每个故事只描述一个明确目的。 | ||
| 39 | + | ||
| 40 | +5. 验收标准(Acceptance Criteria)——**重点加强部分** | ||
| 41 | + - 使用「可验证条件」描述,每条至少包含: | ||
| 42 | + - 前置条件(环境、数据状态、接口返回等)。 | ||
| 43 | + - 用户操作(点击、进入页面、刷新、重试等)。 | ||
| 44 | + - 预期结果(UI 展示、状态变化、接口调用、副作用)。 | ||
| 45 | + - 必要时补充「不允许出现的结果 / 行为」以减少歧义。 | ||
| 46 | + | ||
| 47 | +6. 交互与视觉要点 | ||
| 48 | + - 交互:点击反馈、加载状态、重试行为、动画等。 | ||
| 49 | + - 视觉:布局位置、层级关系、是否与现有组件对齐。 | ||
| 50 | + - 终端:默认移动端优先,如有 PC 差异需单独说明。 | ||
| 51 | + | ||
| 52 | +7. 数据与接口 | ||
| 53 | + - 数据字段:只描述字段含义,不写具体数据结构实现细节。 | ||
| 54 | + - 接口约定:本项目约定所有接口返回结构均为 `{ code, data, msg }`: | ||
| 55 | + - `code = 1` 表示成功; | ||
| 56 | + - `code ≠ 1` 表示失败; | ||
| 57 | + - `data` 为业务数据; | ||
| 58 | + - `msg` 为提示文案(前端可用作错误提示文本来源)。 | ||
| 59 | + | ||
| 60 | +8. 边界条件与风险 | ||
| 61 | + - 写出「极端但可能真实发生」的情况以及期望行为。 | ||
| 62 | + - 对于不确定的行为,用自然语言描述当前的默认假设。 | ||
| 63 | + | ||
| 64 | +9. 待确认事项 | ||
| 65 | + - 所有不确定点都必须以 `[NEEDS CLARIFICATION]` 开头。 | ||
| 66 | + - 每条都要写成一个具体的问题,便于后续和产品 / 业务沟通。 | ||
| 67 | + | ||
| 68 | +--- | ||
| 69 | + | ||
| 70 | +## 三、硬核验收标准书写规范 | ||
| 71 | + | ||
| 72 | +编写验收标准时,建议将每条标准写成「**编号 + 条件 + 操作 + 预期结果**」的形式,示例如下: | ||
| 73 | + | ||
| 74 | +- AC1:正常有数据时展示列表 | ||
| 75 | + - 条件:接口 `searchOldActivityAPI` 请求成功,返回 `code = 1`,`data.list` 为长度 ≥ 1 的数组。 | ||
| 76 | + - 操作:用户进入 `ActivityHistoryPage`,页面完成首次数据加载。 | ||
| 77 | + - 预期结果: | ||
| 78 | + - 列表区域展示所有活动记录卡片,条数等于 `data.list.length`。 | ||
| 79 | + - 不展示空状态文案、不展示失败状态或重试按钮。 | ||
| 80 | + - 顶部 Banner、底部固定按钮等既有内容保持当前实现,不被遮挡或改变行为。 | ||
| 81 | + | ||
| 82 | +- AC2:无数据时展示空状态 | ||
| 83 | + - 条件:接口 `searchOldActivityAPI` 请求成功,返回 `code = 1`,但 `data.list` 为空数组或不存在可用记录。 | ||
| 84 | + - 操作:用户进入 `ActivityHistoryPage`,页面完成首次数据加载。 | ||
| 85 | + - 预期结果: | ||
| 86 | + - 列表原本展示卡片的位置,显示空状态区域(如「暂无活动记录」文案)。 | ||
| 87 | + - 空状态展示在页面内容区域内,不通过 Toast 作为唯一反馈。 | ||
| 88 | + - 不展示失败状态或重试按钮。 | ||
| 89 | + - 顶部 Banner、底部按钮与其他入口可正常点击、滚动不受影响。 | ||
| 90 | + | ||
| 91 | +- AC3:请求失败时展示失败状态 + 重试按钮 | ||
| 92 | + - 条件: | ||
| 93 | + - 接口 `searchOldActivityAPI` 返回 `code ≠ 1`;或 | ||
| 94 | + - 请求发生网络异常 / JS 异常导致无法获取数据。 | ||
| 95 | + - 操作:用户进入 `ActivityHistoryPage`,触发列表加载。 | ||
| 96 | + - 预期结果: | ||
| 97 | + - 列表区域显示失败状态区域,包含错误提示文案和「重试」按钮。 | ||
| 98 | + - 提示文案优先使用接口返回的 `msg` 字段;若 `msg` 为空或不可用,则使用默认文案(例如「加载失败,请稍后重试」)。 | ||
| 99 | + - 不展示有效的活动卡片,不展示空状态提示。 | ||
| 100 | + - 顶部 Banner、底部按钮与其他入口仍可正常使用。 | ||
| 101 | + | ||
| 102 | +- AC4:点击重试后的行为 | ||
| 103 | + - 条件:页面当前处于失败状态,且显示重试按钮。 | ||
| 104 | + - 操作:用户点击失败状态区域中的「重试」按钮。 | ||
| 105 | + - 预期结果: | ||
| 106 | + - 重新发起一次 `searchOldActivityAPI` 请求,使用与首次请求相同的参数。 | ||
| 107 | + - 在请求结果返回前,页面可选择短暂展示「加载中」状态或保持失败状态(需在规格中明确);无论哪种方式,都不得多次并发重复请求。 | ||
| 108 | + - 重试成功且有数据:页面展示列表卡片,隐藏失败状态与重试按钮。 | ||
| 109 | + - 重试成功但无数据:页面展示空状态,隐藏失败状态与重试按钮。 | ||
| 110 | + - 重试仍然失败:保持或更新失败状态提示文案(可使用新的 `msg`),重试按钮仍可再次点击。 | ||
| 111 | + | ||
| 112 | +- AC5:缓存或参数异常时的行为 | ||
| 113 | + - 条件:本地缓存的查询参数缺失、格式异常或读取缓存时抛出异常。 | ||
| 114 | + - 操作:用户正常进入 `ActivityHistoryPage`。 | ||
| 115 | + - 预期结果: | ||
| 116 | + - 页面不会出现崩溃或白屏。 | ||
| 117 | + - 若无法构造有效请求参数,则进入失败状态,并展示失败提示与重试按钮。 | ||
| 118 | + - 若重试按钮依赖重新读取缓存,则需要在规格中说明默认兜底行为(例如「使用默认时间范围重试」或「继续展示失败状态并提示用户稍后重试」)。 | ||
| 119 | + | ||
| 120 | +编写其他页面的验收标准时,可参考上述形式,将核心场景拆分为 AC1 / AC2 / AC3...,每条都保证: | ||
| 121 | + | ||
| 122 | +- 可以通过**明确的前置条件**在测试环境中复现; | ||
| 123 | +- 可以通过**清晰的操作步骤**验证结果; | ||
| 124 | +- 可以通过**观察 UI 或接口行为**判断是否通过。 | ||
| 125 | + | ||
| 126 | +--- | ||
| 127 | + | ||
| 128 | +## 四、可直接复用的 /speckit.specify 提示文案 | ||
| 129 | + | ||
| 130 | +当你在聊天中描述新需求时,可以在自然语言需求后追加如下提示,引导智能体按「硬核」方式输出规格说明: | ||
| 131 | + | ||
| 132 | +> 请将上述需求整理为 Spec Kit 规格说明,输出到 `specs/<feature_key>/spec.md`,并严格遵守以下要求: | ||
| 133 | +> 1. 使用本项目既有的 spec 结构:背景与目标、用户画像与使用场景、需求范围、用户故事、验收标准、交互与视觉要点、数据与接口、边界条件与风险、待确认事项。 | ||
| 134 | +> 2. 所有说明使用中文,专注描述「做什么 / 为什么」,不要写实现细节(例如具体组件名、函数名、接口实现代码)。 | ||
| 135 | +> 3. 在「验收标准」一节中,以 AC1 / AC2 / AC3... 的形式列出所有可验证的标准。每条必须包含: | ||
| 136 | +> - 条件:清晰的前置条件(数据状态、接口返回、环境等); | ||
| 137 | +> - 操作:用户在前端界面上的具体操作; | ||
| 138 | +> - 预期结果:界面展示、状态变化、接口调用或其他可观察行为; | ||
| 139 | +> - 如有必要,补充「不允许出现的结果」。 | ||
| 140 | +> 4. 明确区分以下几类场景,并分别给出验收标准: | ||
| 141 | +> - 正常有数据时的行为; | ||
| 142 | +> - 无数据时的行为; | ||
| 143 | +> - 请求失败 / 异常时的行为; | ||
| 144 | +> - 点击重试或重新加载时的行为; | ||
| 145 | +> - 缓存缺失、参数异常等边界条件。 | ||
| 146 | +> 5. 在「数据与接口」部分中,明确约定接口返回结构统一为 `{ code, data, msg }`: | ||
| 147 | +> - `code = 1` 视为成功,`code ≠ 1` 视为失败; | ||
| 148 | +> - `msg` 用作错误提示文案的主要来源; | ||
| 149 | +> - 若本次需求不允许改动接口,请在此处显式说明「不新增接口、不修改现有接口」。 | ||
| 150 | +> 6. 所有不确定点必须写入「待确认事项」并使用 `[NEEDS CLARIFICATION]` 前缀,形式为可直接询问产品 / 业务同学的具体问题。 | ||
| 151 | +> 7. 请避免在验收标准中使用模糊描述(例如「尽量」「可能」「大概」),改用可观测、可验证的具体条件(例如「当 X 时必须显示 Y 文案,不展示 Z」)。 | ||
| 152 | + | ||
| 153 | +--- | ||
| 154 | + | ||
| 155 | +## 五、示例:ActivityHistoryPage 空状态 + 失败提示(摘要示例) | ||
| 156 | + | ||
| 157 | +以下是基于 `ActivityHistoryPage` 需求的验收标准摘要示例,可作为编写类似功能规格时的参考: | ||
| 158 | + | ||
| 159 | +- AC1:有历史活动时 | ||
| 160 | + - 条件:`searchOldActivityAPI` 返回 `code = 1` 且 `data.list` 长度 ≥ 1。 | ||
| 161 | + - 操作:进入 `ActivityHistoryPage`,等待列表加载完成。 | ||
| 162 | + - 预期结果: | ||
| 163 | + - 页面展示等同于 `data.list` 数量的活动卡片; | ||
| 164 | + - 不展示空状态和失败状态; | ||
| 165 | + - 头部 Banner、底部按钮和「没找到我的星球活动」入口可正常使用。 | ||
| 166 | + | ||
| 167 | +- AC2:没有历史活动时 | ||
| 168 | + - 条件:`searchOldActivityAPI` 返回 `code = 1` 且 `data.list` 为空数组或无可展示记录。 | ||
| 169 | + - 操作:进入 `ActivityHistoryPage`,等待列表加载完成。 | ||
| 170 | + - 预期结果: | ||
| 171 | + - 列表区域展示空状态文案(如「暂无活动记录」); | ||
| 172 | + - 不展示失败状态和重试按钮; | ||
| 173 | + - 其他区域交互行为与有数据时保持一致,不出现遮挡。 | ||
| 174 | + | ||
| 175 | +- AC3:接口失败或异常时 | ||
| 176 | + - 条件:`searchOldActivityAPI` 返回 `code ≠ 1`,或请求抛出异常。 | ||
| 177 | + - 操作:进入 `ActivityHistoryPage`,触发列表加载。 | ||
| 178 | + - 预期结果: | ||
| 179 | + - 列表区域展示失败状态文案,优先使用接口返回的 `msg`,否则使用默认文案(如「加载失败,请稍后重试」); | ||
| 180 | + - 同时展示「重试」按钮; | ||
| 181 | + - 不展示列表卡片和空状态; | ||
| 182 | + - 用户仍可操作顶部和底部区域。 | ||
| 183 | + | ||
| 184 | +- AC4:点击重试 | ||
| 185 | + - 条件:页面处于失败状态。 | ||
| 186 | + - 操作:点击失败状态中的「重试」按钮。 | ||
| 187 | + - 预期结果: | ||
| 188 | + - 重新调用 `searchOldActivityAPI`; | ||
| 189 | + - 根据新结果切换到 AC1 或 AC2 描述的状态; | ||
| 190 | + - 若依旧失败,保持或更新失败状态提示文案,仍可再次重试。 | ||
| 191 | + | ||
| 192 | +在实际书写 spec.md 时,可在此基础上补全其它章节(背景、用户故事、边界条件等),并针对具体业务做适当调整。 | ||
| 193 | + |
-
Please register or login to post a comment