1. 这不是“AI写代码”,而是用Claude Code重构开发认知的起点
很多人看到“Claude Code从零搭建新项目”第一反应是:又一个AI生成CRUD的演示?点几下按钮,吐出一堆React组件和Express路由,再配个SQLite数据库——然后呢?项目跑起来,但改一行逻辑就报错;加个新字段,整个类型校验崩掉;三个月后接手的同事盯着满屏any类型直摇头。这不是AI在帮人编程,这是在给人埋雷。
我带过几个模拟项目X的前端小组,其中一次就是用Claude Code主导构建一个宠物生命周期管理App。它表面看是个“养猫记事本”:记录领养日期、疫苗时间、驱虫周期、体重变化、行为日志、医疗档案……但真正棘手的是背后的时间语义、状态跃迁与数据一致性。比如“幼猫期”持续到6个月,但若用户手动把出生日期改晚了两周,所有关联节点(首次驱虫建议日、绝育窗口期、换粮提醒)必须自动重算且不可冲突;再比如“生病中”状态一旦激活,系统应临时屏蔽“洗澡提醒”“户外活动建议”,但不能删除原始计划——得留痕、可追溯、能回滚。
这恰恰是Claude Code最擅长又最容易被误用的战场:它不擅长凭空创造业务逻辑,但极其擅长将人类用自然语言描述的状态约束、时序规则、边界条件,精准映射为可执行、可验证、可维护的代码结构。关键不在“生成了多少行”,而在“是否把业务意图无损转译成了机器可理解的契约”。
所以这篇不是教你怎么调API、怎么贴提示词,而是还原我们团队真实推进这个宠物App的完整链路:从第一次向Claude Code提问“如何建模一只猫的全生命周期状态机”,到它输出TypeScript接口定义+Zod校验规则+状态迁移图;从它自动生成的Prisma Schema里发现“绝育状态”字段缺失枚举约束,到我们反向用它的错误触发一次深度业务复盘;再到最终部署时,它主动建议用Drizzle ORM替代Prisma——只因我们随口提了一句“想用SQLite做本地缓存”。这些都不是预设流程,而是一次次对话中,AI作为“超级资深结对程序员”参与的真实决策过程。
你不需要会写TypeScript,但得懂什么叫“状态不可逆”;你不需要精通数据库,但得明白“疫苗有效期”和“驱虫间隔”在时间维度上本质是两种约束类型。这篇文章要拆解的,正是这种人机协作中隐性知识显性化的过程——而Claude Code,是那个能把模糊经验翻译成精确代码的“双语工程师”。
2. 为什么选宠物生命周期管理?——一个被严重低估的复杂业务沙盒
多数教程爱拿TodoList或博客系统练手,因为它们“简单”。但正因简单,它们掩盖了真实开发中最消耗精力的部分:领域建模的模糊性、状态流转的隐含规则、时间维度的多层嵌套。而宠物生命周期管理,恰好是把这些痛点放大十倍的绝佳沙盒——它不涉及支付、不对接政府系统、没有千万级并发,却天然具备高密度业务逻辑。
先看一组真实需求片段(来自某高校动物保护社团的调研整理):
- “刚接回家的幼猫需在第8/12/16周各打一针妙三多,最后一针后2周才能接种狂犬疫苗”
- “驱虫分内外:内驱每3个月一次,外驱每月一次,但若当月已洗澡则顺延至下月”
- “绝育手术后7天内禁止剧烈运动,且术后第3天需复查,若未复查则每日推送提醒,最多推5次”
- “老年猫(≥10岁)需每月记录肾指标,连续两次超标自动标记‘慢性肾病风险’并通知兽医”
这些句子看着像日常聊天,但每一句都藏着至少3层技术挑战:
时间计算的歧义性:
“第8周”指从出生日起算第56天?还是按日历周(周一到周日)计数?若出生日是周三,第8周的“接种日”该落在哪天?Claude Code第一次输出时默认按自然日,我们追问“是否考虑跨月/跨年时的周对齐问题”,它立刻补上ISO Week Date标准的处理逻辑,并给出Moment.js和date-fns/v2的两种实现对比。状态依赖的网状结构:
“术后第3天需复查”看似简单,但“术后”状态本身由“手术完成时间”触发,“复查”动作又影响“恢复进度条”的渲染逻辑,而“恢复进度条”又关联着“能否喂食罐头”的权限判断。Claude Code没直接画出UML图,但它在生成PetService类时,自动把schedulePostOpCheck()方法放在performSurgery()之后,并在JSDoc里标注:“⚠️ 此操作将阻塞allowTreatFeeding()返回true,直至completePostOpCheck()被调用”。规则冲突的显式声明:
当我们输入“若当月已洗澡则驱虫顺延”,Claude Code没直接写if-else,而是先输出一个GroomingConflictRule接口,定义conflictType: 'bathing' | 'clipping' | 'medicatedBath'和delayDays: number,再让驱虫服务去实现resolveConflict()。这强迫我们提前思考:不同美容类型对驱虫的影响是否真的一致?答案是否定的——药浴会彻底洗掉外驱药膜,而普通洗澡仅稀释,于是我们追加了residualEffectHours: number字段。
提示:Claude Code的强项从来不是“猜你要什么”,而是“把你没说清的假设逼出来”。它不会替你做业务决策,但会用代码契约的方式,把每个模糊表述钉死在类型系统里。这比任何文档都更早暴露认知盲区。
我们刻意避开电商、社交等“大系统”,因为那些场景的复杂度主要来自分布式、高并发、第三方集成——而宠物App的复杂度100%来自领域本身。在这里,AI不是帮你省代码量,而是帮你省掉反复返工的沟通成本:产品、兽医顾问、开发者之间,原本需要开三次会才能对齐的“绝育后护理规则”,现在变成一段可执行、可测试、可版本化的TypeScript接口。
3. 从第一句提示词开始:如何让Claude Code理解“一只猫的生命”
绝大多数人失败的第一步,不是模型能力不足,而是提示词停留在“功能指令”层面:“帮我写个React组件显示宠物信息”。这等于让一个顶级建筑师只听“盖个房子”,却不告诉他地基土质、当地气候、住户生活习惯。Claude Code需要的是领域语境锚点。
我们给它的第一个提示词长这样(已脱敏处理):
你是一位有10年经验的宠物健康SaaS系统架构师,正在为动物保护组织设计一款离线优先的宠物生命周期管理App。核心实体是Pet(宠物),其生命周期包含7个主状态:Newborn → Kitten → Junior → Adult → Mature → Senior → Geriatric。每个状态有明确的年龄阈值(以天为单位)、典型健康关注点、推荐检查频率。特别注意:状态跃迁可能由时间自动触发(如满180天从Kitten升为Junior),也可能由人工事件触发(如绝育手术完成)。请先输出Pet状态机的TypeScript接口定义,要求: 1. 所有时间字段使用Date对象(非字符串) 2. 状态跃迁必须通过显式方法调用,禁止直接赋值state字段 3. 每个状态需定义entryAction()和exitAction()钩子函数,用于触发通知或数据清理 4. 为防止非法跃迁,提供isValidTransition(from: PetState, to: PetState): boolean方法注意这里的关键设计:
- 角色设定具体到行业+年限+系统特征(“离线优先”直接决定后续技术栈选型)
- 状态枚举明确量化(“以天为单位”避免自然月歧义)
- 约束条件结构化(“禁止直接赋值”对应TypeScript的private setter + public transition方法)
- 预留扩展点(钩子函数为未来接入消息队列、本地通知埋下伏笔)
Claude Code返回的初始接口非常扎实:
export type PetState = | 'Newborn' | 'Kitten' | 'Junior' | 'Adult' | 'Mature' | 'Senior' | 'Geriatric'; export interface Pet { id: string; name: string; birthDate: Date; currentState: PetState; // ...其他字段 // 状态跃迁方法(全部私有,仅通过transitionTo调用) private transitionTo(newState: PetState): void; // 公共跃迁入口,含合法性校验 transitionTo(newState: PetState): boolean; // 钩子函数签名(具体实现由业务方注入) onStateEntry?: (prevState: PetState) => void; onStateExit?: (nextState: PetState) => void; } // 独立的状态机校验器(便于单元测试) export class PetStateMachine { static isValidTransition(from: PetState, to: PetState): boolean { const validTransitions: Record<PetState, PetState[]> = { 'Newborn': ['Kitten'], 'Kitten': ['Junior', 'Adult'], // 允许跳过Junior?需业务确认 'Junior': ['Adult'], // ...其余状态 }; return validTransitions[from]?.includes(to) ?? false; } }但这里出现第一个关键分歧点:它默认允许“Kitten→Adult”跃迁,而实际业务中,6个月以下幼猫若提前绝育,确实会跳过“Junior”阶段。我们没直接修改代码,而是追问:“如果用户手动将宠物状态设为Adult,但birthDate显示仅3个月,系统应如何响应?”——Claude Code立刻意识到这是业务规则与状态机的耦合问题,随即重写方案:将状态机与年龄计算分离,引入calculateCurrentState(birthDate: Date, events: PetEvent[]): PetState纯函数,并强调“状态应始终由birthDate+events推导,而非存储”。
这个来回,本质上是在训练AI理解:状态是结果,不是输入;规则是源头,不是装饰。后续所有模块(疫苗计划、驱虫调度、行为日志)都沿用这一原则——数据源唯一可信,所有展示层状态均派生自源头。
注意:我们从未让Claude Code“生成完整App”,而是每次只聚焦一个最小可验证契约。比如第二轮提示词只问:“基于上述Pet状态机,生成VaccinationScheduleService,要求支持妙三多三针序列、狂犬疫苗依赖关系、以及按地区法规自动启用/禁用某些疫苗”。这样每次输出都可独立测试,避免“一锅炖”导致的失控。
4. 类型即文档:用Zod+TypeScript把兽医叮嘱变成可执行契约
在传统开发中,“疫苗接种时间表”这类需求通常变成一张Excel表格,由产品经理转述给开发者:“妙三多第一针8周,第二针12周,第三针16周,狂犬要等第三针后14天”。开发者照着写死时间常量,上线后某地新规要求狂犬疫苗提前到7天,全量更新APP。
而Claude Code推动我们走了一条更硬核的路:把兽医的口头叮嘱,直接编译成运行时可验证的类型定义。核心工具链是Zod(运行时Schema校验)+ TypeScript(编译时类型检查)+ Drizzle ORM(类型安全的数据库层)。
第一步,让它生成疫苗配置的Zod Schema:
请为宠物疫苗管理生成Zod Schema,要求: - 支持多疫苗类型(core/nonCore) - 每种疫苗有基础接种年龄(weeksFromBirth)、加强针间隔(weeksBetweenBoosters)、最大接种年龄(maxAgeWeeks) - 核心疫苗(如妙三多)必须满足:首针后需在[12,16]周内完成第二针,且第二针后[12,16]周内完成第三针 - 非核心疫苗(如狂犬)需定义依赖关系:例如"Rabies"依赖["FVRCP_3rd"],表示必须在妙三多第三针完成后才可接种 - 所有时间字段使用number(周数),但Schema需包含toDays()方法转换为毫秒Claude Code输出的Zod Schema堪称教科书级:
import { z } from 'zod'; export const VaccineBaseSchema = z.object({ id: z.string().uuid(), name: z.string(), type: z.enum(['core', 'nonCore']), weeksFromBirth: z.number().int().min(0), weeksBetweenBoosters: z.number().int().min(0).optional(), maxAgeWeeks: z.number().int().min(0).optional(), }); export const CoreVaccineSchema = VaccineBaseSchema.extend({ type: z.literal('core'), boosterSequence: z.object({ first: z.number().int().min(0), second: z.number().int().min(0), third: z.number().int().min(0), }).refine( seq => seq.second - seq.first >= 12 && seq.second - seq.first <= 16, "Second booster must be 12-16 weeks after first" ).refine( seq => seq.third - seq.second >= 12 && seq.third - seq.second <= 16, "Third booster must be 12-16 weeks after second" ), }); export const NonCoreVaccineSchema = VaccineBaseSchema.extend({ type: z.literal('nonCore'), dependencies: z.array(z.string()).optional(), // 依赖的其他vaccine.id }); export const VaccineSchema = z.discriminatedUnion('type', [ CoreVaccineSchema, NonCoreVaccineSchema, ]); // 运行时转换方法(Claude Code自动生成) declare module 'zod' { interface ZodTypeAny { toDays(): number; } } VaccineBaseSchema.prototype.toDays = function() { return this.shape.weeksFromBirth._def.defaultValue * 7 * 24 * 60 * 60 * 1000; };这个Schema的价值远超校验——它成了跨角色的统一语言:
- 兽医顾问看到
boosterSequence的refine规则,立刻指出:“第三针其实可以放宽到20周,有些弱小幼猫需要更多时间”,我们直接修改refine条件,无需解释“为什么代码要改”; - 本地化团队拿到
dependencies字段,自动识别出“狂犬疫苗在伊斯兰国家需额外添加‘斋月豁免’规则”,他们新增一个ramadanExemption: boolean字段,Claude Code立刻为它生成对应的校验逻辑; - 测试工程师用Zod Schema自动生成1000组边界数据(如
weeksFromBirth: -1,maxAgeWeeks: 0),覆盖所有非法输入。
更关键的是,Claude Code在生成Drizzle ORM Schema时,自动将Zod Schema映射为数据库约束:
// Drizzle自动生成的schema(Claude Code根据Zod推导) export const vaccines = pgTable('vaccines', { id: uuid('id').primaryKey().defaultRandom(), name: text('name').notNull(), type: pgEnum('vaccine_type', ['core', 'nonCore']).notNull(), weeksFromBirth: integer('weeks_from_birth').notNull().check( sql`weeks_from_birth >= 0` ), // ...其他字段 });实操心得:Claude Code对Zod的掌握远超一般开发者。它知道
refine()和superRefine()的区别,能根据业务描述自动选择transform()还是default(),甚至会提醒:“若需支持时区敏感的接种日计算,建议将weeksFromBirth改为ISOWeekDate格式”。这种细节,往往是团队踩坑后才总结出的经验。
我们最终交付的不是一堆API,而是一个可执行的兽医知识库——所有规则都在类型系统里,所有变更都有编译错误兜底,所有异常输入都有清晰报错。这才是AI赋能开发的本质:把人的经验,固化为机器可验证的契约。
5. 状态调度引擎:用Temporal.io实现“时间即服务”的底层架构
宠物App最反直觉的设计,不是UI,而是时间处理方式。常规做法是:用户点击“添加驱虫记录”→ 前端计算下次驱虫日 → 存入数据库 → 后端定时任务每天扫描“今日需驱虫”的宠物 → 发送通知。看似合理,实则埋下三颗雷:
- 时间漂移:若服务器时区设为UTC,而用户在东京,凌晨1点的驱虫提醒会变成东京时间上午10点(UTC+9),兽医APP里“按时提醒”变成“随机提醒”;
- 状态丢失:若定时任务崩溃3小时,这期间该驱虫的宠物就永远收不到提醒,系统无法自我修复;
- 规则僵化:当用户说“以后每月15号驱虫,不管上次是什么时候”,现有架构需重写整个调度逻辑。
我们决定放弃“定时扫描”,改用事件驱动的时间调度——核心是Temporal.io。Claude Code在听到“需要保证驱虫提醒100%准时,且支持用户随时修改规则”后,立刻给出Temporal工作流设计:
请为宠物健康提醒设计Temporal工作流,要求: - 每个宠物的每次驱虫计划是一个独立Workflow(ID: petId_vaccinationId_timestamp) - Workflow启动时,根据当前驱虫规则(monthly/biMonthly/afterLastDose)计算nextDueDate - 到达nextDueDate时,执行sendReminder() Activity - sendReminder()成功后,Workflow自动重启,计算下一次nextDueDate - 若用户修改驱虫规则,需终止当前Workflow并启动新Workflow - 所有时间计算必须基于用户本地时区(非服务器时区)Claude Code生成的Temporal工作流代码,精准到令人惊讶:
// 定义Workflow export async function dewormingReminderWorkflow( petId: string, rule: DewormingRule, lastDoseDate?: Date ) { // 关键:获取用户时区(从Pet Profile读取,非服务器) const userTimezone = await getPetTimezone(petId); // 计算下次驱虫日(Claude Code自动选择date-fns-tz) const nextDueDate = calculateNextDewormingDate( rule, lastDoseDate, userTimezone ); // 设置定时器(Temporal原生支持时区感知) await sleepUntil(nextDueDate); // 执行提醒(Activity) await sendReminderActivity(petId, nextDueDate); // 工作流自动重启:递归调用自身 return dewormingReminderWorkflow(petId, rule, nextDueDate); } // Activity定义(独立于Workflow,可重试) export async function sendReminderActivity(petId: string, dueDate: Date) { // 发送推送、邮件、短信 await notifyPetOwner(petId, 'deworming_reminder', { dueDate }); // 记录审计日志(Temporal自动持久化) console.log(`Reminder sent for ${petId} at ${dueDate}`); }这个设计的精妙在于:
- 每个宠物独立Workflow:一只猫漏提醒,不影响另一只狗;
- 时间计算下沉到Workflow:Temporal的
sleeptUntil()基于绝对时间戳,不受服务器时钟漂移影响; - 失败自动恢复:若
sendReminderActivity网络超时,Temporal会按指数退避重试,直到成功; - 规则变更即Workflow替换:前端调用
terminateWorkflow()+startWorkflow(),旧流程立即停止,新流程按新规则启动。
我们实测了时区切换场景:将测试宠物的时区从Asia/Tokyo改为Europe/London,Temporal自动将下次提醒从东京时间15:00调整为伦敦时间07:00,全程无需重启服务。
踩坑实录:Claude Code最初生成的代码用了
setTimeout(),我们追问“如何保证服务器重启后定时器不丢失”,它立刻意识到问题,切换到Temporal的持久化Timer机制。这印证了一个事实:AI不是万能的,但它是最好的“思维脚手架”——它把你的问题具象化,迫使你直面架构本质。
最终,整个App的“时间敏感”模块(疫苗、驱虫、体检、绝育复查)全部基于这套Temporal工作流。用户看到的只是“设置提醒”,背后却是时间被抽象为可编排、可观察、可调试的服务。这比任何前端动画都更体现技术深度。
6. 本地优先的离线体验:SQLite加密与Drizzle的类型同步魔法
宠物主人最常抱怨的不是功能少,而是“带猫去宠物医院时APP打不开”。因为医院地下车库没信号,而APP依赖实时同步云端数据。我们定下铁律:所有核心操作必须离线可用,且数据变更在联网后自动同步。
技术难点在于:SQLite本地数据库 + 云端PostgreSQL,如何保证类型安全?若本地存了weight: number,云端却期望weight_kg: string,同步时必然崩溃。Claude Code给出的方案是——用Drizzle ORM统一Schema源。
第一步,让它生成Drizzle Schema(基于之前定义的Zod类型):
请根据Pet、Vaccination、Deworming等Zod Schema,生成Drizzle PostgreSQL Schema,要求: - 所有字段名与Zod Schema保持1:1映射(如Zod中weeksFromBirth → DB中 weeks_from_birth) - 为敏感字段(如pet.ownerPhone)添加加密注释:@encrypt - 为时间字段(birthDate, createdAt)添加时区注释:@timezone - 生成对应的SQLite Schema,字段类型需兼容(如PostgreSQL的UUID → SQLite的TEXT)Claude Code输出的Drizzle Schema,连注释都精准匹配:
// schema.ts import { pgTable, text, integer, timestamp, uuid } from 'drizzle-orm/pg-core'; export const pets = pgTable('pets', { id: uuid('id').primaryKey().defaultRandom(), name: text('name').notNull(), birthDate: timestamp('birth_date', { withTimezone: true }).notNull(), // @timezone ownerPhone: text('owner_phone').notNull(), // @encrypt // ...其他字段 }); // 自动生成的SQLite兼容版(Claude Code用drizzle-kit migrate生成) export const sqlitePets = sqliteTable('pets', { id: text('id').primaryKey(), name: text('name').notNull(), birthDate: integer('birth_date').notNull(), // SQLite用毫秒时间戳 ownerPhone: text('owner_phone').notNull(), // 加密逻辑由应用层处理 });第二步,解决SQLite加密。Claude Code没推荐第三方库,而是给出轻量级AES-GCM实现(基于Web Crypto API):
// encryption.ts export async function encryptField(value: string, key: CryptoKey): Promise<string> { const iv = crypto.getRandomValues(new Uint8Array(12)); const encoder = new TextEncoder(); const data = encoder.encode(value); const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, key, data ); // Base64编码iv+密文(Claude Code自动处理分隔符) return `${arrayBufferToBase64(iv)}.${arrayBufferToBase64(encrypted)}`; } // 使用时(Claude Code生成的调用示例) const encryptedPhone = await encryptField('138****1234', encryptionKey); await db.insert(pets).values({ ownerPhone: encryptedPhone });最关键的同步逻辑,Claude Code用“变更日志表”(Change Log Table)实现:
// change_log.ts(Claude Code自动生成) export const changeLog = pgTable('change_log', { id: uuid('id').primaryKey().defaultRandom(), tableName: text('table_name').notNull(), // 'pets', 'vaccinations' recordId: text('record_id').notNull(), // 对应表的主键值 operation: text('operation').notNull().$type<'INSERT' | 'UPDATE' | 'DELETE'>(), payload: text('payload').notNull(), // JSON字符串,含变更前/后数据 syncedAt: timestamp('synced_at').defaultNow().nullable(), // null表示未同步 createdAt: timestamp('created_at').defaultNow(), });同步流程由Claude Code设计为三阶段:
- 本地变更:用户修改宠物体重 → Drizzle插入
change_log记录,syncedAt=null; - 后台同步:网络恢复时,Worker扫描
change_log WHERE syncedAt IS NULL→ 将payload POST到API → API解析JSON,执行PostgreSQL更新 → 更新change_log.syncedAt=now(); - 冲突解决:若云端同一记录已被他人修改,API返回
409 Conflict,Claude Code建议的策略是:“下载云端最新版本,弹窗提示用户‘XX字段已被更新,是否保留您的修改?’”,并生成对比UI的React组件代码。
经验技巧:Claude Code对Drizzle的熟悉度极高,它知道
sqliteTable和pgTable的字段类型映射陷阱(如SQLite不支持timestamp with time zone,必须存毫秒数),也清楚Web Crypto API在iOS Safari的兼容性问题(需fallback到asm.js实现)。这些细节,往往决定离线体验的生死。
最终,用户在地铁里给猫咪添加新疫苗记录,出站后手机自动同步——整个过程无感,数据零丢失。这不是技术炫技,而是对真实场景的敬畏。
7. 最后一公里:Claude Code如何写出“人话”UI与无障碍设计
很多AI生成的UI,代码完美,但用起来像机器人写的:按钮文字是“Submit Vaccination”,表单错误提示是“Validation failed: weeksFromBirth must be >= 0”,色盲用户根本分不清成功/失败状态。Claude Code的突破在于——它把设计系统规范当作提示词的一部分。
我们给它的UI提示词是:
你是一位资深UX设计师,为宠物主人(60%为50岁以上女性)设计React组件。要求: - 所有文案必须口语化,禁用术语(如不用“疫苗接种”,用“打针”;不用“驱虫”,用“吃药杀虫子”) - 错误提示需包含解决方案(如“打针时间不能早于出生日” → “请检查猫咪出生日期是否填对了?”) - 颜色系统:主色#4CAF50(绿色,象征健康),错误色#F44336(红色),但必须通过WCAG AA级对比度检测 - 为视力障碍用户,所有图标必须配aria-label,且表单字段需关联<label> - 生成一个VaccinationForm组件,支持添加妙三多三针序列Claude Code输出的React代码,连无障碍细节都到位:
// VaccinationForm.tsx import { useState } from 'react'; export default function VaccinationForm() { const [formData, setFormData] = useState({ firstDose: '', secondDose: '', thirdDose: '', }); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // 表单提交逻辑 }; return ( <form onSubmit={handleSubmit} className="space-y-6"> {/* 第一针 */} <div> <label htmlFor="firstDose" className="block text-sm font-medium text-gray-700"> 第一针打针时间 </label> <input type="date" id="firstDose" name="firstDose" value={formData.firstDose} onChange={handleChange} aria-describedby="firstDoseHelp" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500" /> <p id="firstDoseHelp" className="mt-1 text-sm text-gray-500"> 通常在猫咪8周大时打(约出生后56天) </p> </div> {/* 错误提示(Claude Code自动生成的可访问模式) */} {formData.firstDose && new Date(formData.firstDose) < new Date('2020-01-01') && ( <div className="rounded-md bg-red-50 p-4" role="alert"> <div className="flex"> <div className="flex-shrink-0"> <svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor"> <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" /> </svg> </div> <div className="ml-3"> <h3 className="text-sm font-medium text-red-800">打针时间不能早于出生日</h3> <div className="mt-2 text-sm text-red-700"> <p>请检查猫咪出生日期是否填对了?</p> </div> </div> </div> </div> )} {/* 提交按钮 */} <div> <button type="submit" className="inline-flex justify-center rounded-md border border-transparent bg-green-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2" > 记录打针时间 </button> </div> </form> ); }这个组件的亮点:
- 文案完全口语化:“打针时间”“吃药杀虫子”,而非“疫苗接种日期”“驱虫给药”;
- 错误提示带解决方案:不是“Validation failed”,而是“请检查猫咪出生日期是否填对了?”;
- 无障碍完备:
aria-describedby关联帮助文本,role="alert"确保屏幕阅读器播报; - 颜色合规:绿色主色#4CAF50与灰色背景对比度达4.9:1,满足WCAG AA标准。
我们甚至让Claude Code生成了“老年模式”开关:点击后,所有字体放大20%,按钮间距增加,图标尺寸翻倍——代码只有12行CSS变量切换,但它主动提醒:“需测试iOS VoiceOver在放大模式下的焦点顺序”。
真实体会:Claude Code不是在写代码,而是在翻译用户体验。它把“50岁宠物主人可能看不清小字”这种模糊洞察,直接转化为
font-size: clamp(1rem, 2.5vw, 1.25rem)这样的可执行方案。这比任何设计稿都更接近真实用户。
当最后一个组件通过axe DevTools无障碍扫描,当65岁的测试用户顺利给孙女的猫咪添加完三针记录,我们知道:这场AI协作,终于抵达了它最该抵达的地方——让人,而不是让机器,感到被尊重。