项目:PEXELS 壁纸应用 | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
本篇给出 6 个扩展场景的标准作业程序(SOP),全部以当前源码实际结构为依据,路径均相对 PEXELS/。无论哪个场景,先记住三条公共约定,违反它们是扩展出问题的主要来源:
约定一:数据只走"协议层 → 门面层 → 页面层"单行道。新增或修改任何接口调用,协议方法进 src/utils/pexels.ts 的 PexelsClient,降级决策进 src/api/pexels.ts,页面只 import 门面函数。跳过门面直连协议层会绕开错误分级、total 截断与无 Key 降级(《03-接口设计》3.2 节的降级矩阵会对你失效)。
约定二:新请求先过"配额三问"(见《00-项目总览》第 3.1 节):能不能晚点发(按需触发)、能不能不发(缓存/静态化)、能不能只发一次(模块级缓存)。参考实现:封面按需排队(src/utils/pexels-collection-cache.ts)、分类静态化(src/constants/index.ts)、元信息 metaCache。
约定三:keep-alive 与 name 的一致性。凡是挂进 src/views/layout/index.vue 的 CACHE_NAMES 的页面,必须同时做到:defineOptions({ name }) 与路由 name 一致、实现滚动恢复(useScrollRestore)、实现失活挂起(isActive 开关 + onActivated 比对补拉,参考 src/views/main/components/list/index.vue 第 160~192 行)。三个配套缺一个,缓存页就会出现"白屏列表""滚动归零"或"白烧配额"三种缺陷之一。
每个 SOP 的结尾给出验收清单,验收环境准备(无 Key 演示模式 + 有 Key 真实模式各一套)见《06-部署运行》第 2 节。
场景:给首页增加一个内容分类(如「美食」)。这是最小改动路径,5 分钟级。
| 步骤 | 文件 | 操作 |
|---|---|---|
| 1 | src/constants/index.ts | 在 CATEGORY_NOMAR_DATA 数组追加 { id: 'food', name: '美食' } |
| 2 | 无其它改动 | 导航(src/views/main/components/navigation/index.vue)、store、接口参数全部自动跟随 |
约束与原理:id 必须唯一且必须是有效的 Pexels 搜索词——id 会直接作为 query 参数发给 /v1/search(src/api/pexels.ts 的 getPexelsList 中 query = searchText || categoryId)。注释里记录的历史 bug:早期后 4 项 id 重复导致重复 key 警告与选中滑块错位(《07-模块详解》第 8 节)。验收:PC 端点新分类出现墨条且列表刷新;移动端滑块位置正确;DevTools Network 里可见 query=food;切回「全部」走 /curated;快速连续切换分类无旧响应覆盖(序号机制生效)。
场景:新增一个独立路由页面。按"是否进 keep-alive、是否复用列表骨架"分两种走法。
B-1 轻量静态页(如「关于」):
src/views/about/index.vue,defineOptions({ name: 'about' });src/router/index.ts 的 layout children 里挂 { path: '/about', name: 'about', component: () => import('@/views/about/index.vue') };src/views/layout/components/main-tabs.ts 的 MAIN_TABS 加一项(icon 从 @lucide/vue 引入)——PC 的 segmented 与移动端 tabbar 会同时出现新入口(两端共用同一份配置);若只想挂在二级位置(如 header 下拉),改 src/views/layout/components/header/index.vue。/404(src/views/error/404/index.vue)。B-2 复用列表骨架的内容页(如「收藏的搜索词」页):以 src/views/videos/ 目录为模板——页面壳(吸顶工具栏 + <list-vue :mode :query>)与列表组件(infinite-list + waterfall + media-viewer + 序号竞态防护)整套复制改造。四个必查点:① 列表组件的 requestSequence/applied/isActive 三件套原样保留(它们的防御逻辑见《07-模块详解》第 11 节,删掉任何一个都会复活第 4 节档案里的旧坑⑦⑧⑪⑫);② 瀑布流 responsiveColumns 梯度按内容密度重选(视频页 4 列封顶是因为视频卡片信息密度高);③ 若页面要进 CACHE_NAMES,执行约定三;④ 弹层数据必须经 toViewerItems 归一化(src/utils/viewer.ts),且 viewerItems 与列表同序——这是"弹层左右切换不错位"的不变量。验收:切换筛选快速连点无错乱;失败态出现"加载失败,点击重试"且重试不跳页;/新页面/:id 类详情路由刷新可达(部署侧 SPA 回退,见《06-部署运行》4.2)。
场景:给图片搜索加"横图/竖图/方图"与颜色筛选 UI。协议层已经先行——PexelsClient.search 已有 orientation、color 参数(src/utils/pexels.ts,类型 Orientation/ColorFilter 定义齐全,共 12 个色名),只是 UI 未暴露。
src/api/pexels.ts 给 PexelsListQuery 增加 orientation?: string、color?: string 字段,并在 client.search(query, { page, perPage, orientation, color }) 处透传;client.curated 不支持这些参数(官方 /curated 无筛选),"全部"分类下忽略筛选并在 UI 上禁用。src/store/index.ts 的 app store 并注意与 changeCurrentCategory 的清空语义联动;若仅首页局部则放列表页 ref,并并入 appliedQuery 快照比对(syncWithStore 同款逻辑),否则筛选变化不触发重置。src/views/main/index.vue 的吸顶工具栏加筛选控件,可直接复用 m-segmented(src/libs/segmented/index.vue)承载横/竖/方三态。{ list, total })。验收:选「竖图」后 Network 中 /search?...&orientation=portrait 参数正确;切换筛选与切换分类/搜索词互不覆盖(合并 watch 生效);无 Key 模式下筛选控件可点但列表仍为占位图且不报错。同构扩展:视频侧的 size/locale(videosSearch)与 min_width/min_height/min_duration/max_duration(videosPopular,见《15-接口全量参考》第 1.2 节)走完全相同的四步。
场景:接入第二个图片供应商(如 Unsplash)作为并存数据源,或整体替换 Pexels。本项目的分层在这里兑现红利:页面层认的形状可以完全不变。
src/utils/ 新建 unsplash.ts(或直接改写 pexels.ts),实现一个同构客户端——构造器收 (apiKey, baseUrl)、私有 request 统一鉴权与错误分级(抛带 status 的 Error 子类,对齐 PexelsApiError 的契约,门面层靠 err.status 分流)、9 个语义对应的方法名可不同但返回形状必须归一化到本项目的三个模型:NormalizedWallpaper(字段清单见《04-数据模型》3.2)、NormalizedVideo、NormalizedCollection。归一化是本步骤的全部工作量所在:外家的字段名、尺寸档位、分页字段都不同,逐一映射并兜底。src/api/pexels.ts 的通道计算处增加来源选择逻辑(如新的 VITE_IMAGE_SOURCE 变量决定实例化哪个 client);每个门面函数内"用哪个 client"收敛为一处判断,降级矩阵与 total 截断逻辑原样复用(各家分页上限不同,PEXELS_MAX_TOTAL 改名并按来源取值)。@/utils/pexels 具名导入的约定不变——把共享模型类型留在原文件或移到独立的 src/utils/media-types.ts,页面层 import 路径统一替换(全局搜索 from '@/utils/pexels' 可定位全部 10 余处)。验收:切换来源变量后全站五个页面可用;无 Key(新家)降级正常;401/429 行为一致(错误文案在协议层,按新家文案改);pnpm type-check 通过(归一化字段遗漏会在这一步被 strict 模式拦截——这正是类型门禁在扩展场景下的价值)。若为整体替换:删除旧协议文件时同步清理《13-技术债务与演进建议》清单中 pexels 命名的缓存文件与 dist chunk 名预期。
场景:比 SOP-D 更进一步——引入一个 Pexels 没有的内容类型(如「插画/矢量图」或「 GIF 动图」),需要新的实体、新的降级形状与新的展示分支。
src/utils/pexels.ts):新增原始结构接口 + 归一化模型(如 NormalizedIllustration)+ 归一化函数。关键决策点:若新类型在展示上等价于照片(有图有宽高有作者),直接归一化成 NormalizedWallpaper 复用全部下游(参照 normalizeCollectionMediaResponse 对混排项打 type 标签的做法);只有展示形态真的不同(如 GIF 需 <video> 承载)才新建模型。getIllustrationList 等函数,完整复制现有骨架:参数钳制 → 无 client 短路 → try/isAuthOrLimitError 分流 → total 截断;新增对应的降级生成器(createFallbackIllustration,id 规则对齐现有 (page-1)×perPage+i+1 与 Picsum 取模防 404 的经验,见《15-接口全量参考》第 3 节)。NormalizedWallpaper,最小改动是让 getPexelsList 支持来源切换,首页瀑布流与弹层零改动;确需新页面则回 SOP-B-2。toViewerItem 的鸭子测试只认 videoFiles(src/utils/viewer.ts)——新类型若非视频,会自动按照片分支处理;若它是动画(如 GIF 走 img 循环播放),无需动弹层。验收:新增类型在列表、弹层、下载(文件名扩展名规则,参考 src/components/media-viewer/index.vue 的 getFileExtension)三处形态正确;无 Key 演示模式有对应占位;429 时与现有内容同进退。风险提示:E 步骤最容易犯的错是在页面层写 if (新类型) 分支——一旦出现第二处,就应回到第 1 步重新审视归一化是否到位(单一数据模型收敛多源差异是本项目最重要的既定决策,《01-系统架构》决策四)。
F-1 新增主题变量(颜色、字号、阴影等):
src/styles/index.css,在 @theme 块追加 CSS 变量,如 --color-brand-green: #22c55e;;text-brand-green bg-brand-green/10(Tailwind 4 自动从 @theme 生成对应工具类与透明度变体);.dark class 驱动(@custom-variant dark,见《07-模块详解》第 12 节),新增颜色若需暗色区分,在使用处写 dark: 变体,或在 @theme 补一套对应变量由组件分支取用——不要另起一套 class 切换机制。w-[15px]),rem 间距会被根字号放大约 2.5 倍;lucide 描边图标只用 text-* 上色。验收:明暗两态 + 移动端/PC 各过一遍。F-2 新增一个 m- 通用组件*:
src/libs/<name>/index.vue,写好 props/emits 与清理逻辑——清理是本库的第一纪律:定时器、观察器、滚动锁必须都在 onUnmounted(涉及 keep-alive 的再加 onDeactivated),参照 src/libs/popup/index.vue 的双钩子模式与注释里的反面案例;src/libs/index.ts 的 glob 会自动把它注册为异步全局组件 m-<name>(注意:message 类命令式组件要排除在 glob 外并单独走 message() 工厂,理由见该文件注释);import { Xxx } from '@lucide/vue',字符串名动态渲染才登记 src/libs/icon-map.ts;@/libs 的 message(UI 层内部互用合法)。<name>-hash.js 独立 chunk(异步 glob 生效);构建无 300KB 告警;pnpm type-check 通过。任何 SOP 完成后执行同一套收尾:pnpm type-check(唯一自动门禁,见《09-测试与质量保障》第 2 节);pnpm build 观察是否出现 chunk 体积告警(新依赖落错分组的信号,《06-部署运行》第 8 节);按《06-部署运行》第 5 节的手工验收路径过一遍受影响流程;若扩展引入了新的历史坑,把"为什么"写进触发点旁边的注释——这是本项目质量档案(《09-测试与质量保障》第 4 节)持续生长的方式;若改动涉及接口形状或降级矩阵,同步修订《03-接口设计》与《15-接口全量参考》对应表格,保持文档集与源码一致。