1. 原生 JS 分页横向图片浏览为什么要接统一 Key 通道分页浏览横向图片类轮播这个需求前端圈里几乎人人都写过一个overflow-x: hidden的容器里面塞一排img点「上一页 / 下一页」时用innerHTML换一批图。它不依赖任何框架纯原生 JS 就能跑特别适合做图片墙、商品缩略图、相册预览这类轻量场景。但真正把它放到业务里问题就来了。图片列表往往不是写死在 HTML 里的而是从后端接口拉回来的而接口调用又绕不开鉴权——Key 放前端怕泄露放后端要维护一套代理多个环境本地、测试、预发还要各配一份。我试过最省事的做法是把模型/接口调用统一收敛到一个 Key 通道上前端只认一个settings.json骨架切换环境只改一个字段。这篇就聚焦两件事一是原生 JS 分页横向图片的核心逻辑怎么写得稳、可扩展二是怎么通过 TaoToken 的统一 Key/API 通道把接口调用接进来并给出一份可以直接复制的settings.json配置骨架最后用几个验证动作确认连通性。适合正在写图片浏览组件、又不想在鉴权上反复折腾的前端同学。TaoToken 在这里扮演的角色是一个统一的 API 入口你拿到一个 Key就能通过https://taotoken.net/api调用模型对话、代码补全等能力。官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册和文档都在上面。对图片浏览场景来说它最实用的地方是当你想给图片加「AI 生成描述」「智能打标签」这类能力时不用再单独接一套鉴权直接复用同一个 Key 通道即可。2. TaoToken 前置准备Key、通道与 settings.json 定位在动手写配置之前先把几个概念对齐不然后面容易懵。TaoToken 的 API 基址是https://taotoken.net/api注意这个地址不带任何查询参数是纯粹的接口前缀。你所有的请求都拼在它后面比如模型对话通常是/v1/chat/completions这类路径。Key 则在控制台里生成形如sk-开头的一串字符。控制台入口在 https://taotoken.net/console API Keys 管理页在 https://taotoken.net/api-keys 。settings.json这个文件本质上是前端项目的「环境配置中心」。原生 JS 项目没有构建工具时你可以把它放在config/目录下用fetch读进来有构建工具时它可以是public/settings.json运行时加载。它的作用是把 API 基址、Key、超时、分页参数这些易变的东西集中管理代码里只引用字段名不写死值。这里有个关键点Key 不要硬编码进 JS 源码。哪怕是小项目也建议把 Key 放在settings.json里并且这个文件在部署时由服务端注入或通过环境变量替换。前端代码只负责读取不负责存储明文。如果你只是本地自测那直接写进settings.json没问题但上线前一定要换掉。配置骨架大致长这样字段含义我后面会逐个解释{ api: { baseUrl: https://taotoken.net/api, apiKey: sk-你的Key, timeout: 15000, model: claude-3-5-sonnet }, gallery: { pageSize: 4, containerId: div1, imageDir: images/, imageExt: .jpg }, debug: { logLevel: info, mock: false } }api段管接口gallery段管图片浏览debug段管调试开关。这样分层的好处是换模型只改model换图片目录只改imageDir互不影响。3. 可复制配置settings.json 骨架与加载逻辑先给一份完整可用的settings.json你可以直接复制到项目里改字段值。{ api: { baseUrl: https://taotoken.net/api, apiKey: sk-替换成你的Key, timeout: 15000, model: claude-3-5-sonnet, maxRetries: 2 }, gallery: { pageSize: 4, containerId: div1, imageDir: images/, imageExt: .jpg, totalImages: 7, loop: false }, debug: { logLevel: info, mock: false } }字段说明用表格对照更清楚字段含义建议值api.baseUrl接口前缀固定https://taotoken.net/apiapi.apiKey鉴权 Key控制台生成勿提交仓库api.timeout请求超时毫秒10000–20000api.model默认模型名按文档选api.maxRetries失败重试次数1–3gallery.pageSize每页图片数4 或 6gallery.containerId横向容器 id与 HTML 一致gallery.imageDir图片目录相对路径gallery.totalImages图片总数与后端一致gallery.loop是否循环翻页布尔debug.mock是否用假数据本地调试 true加载逻辑用一个独立的config.js处理避免和业务代码混在一起// config.js let SETTINGS null; async function loadSettings() { if (SETTINGS) return SETTINGS; const res await fetch(config/settings.json); if (!res.ok) throw new Error(settings.json 加载失败: res.status); SETTINGS await res.json(); return SETTINGS; } function getApiConfig() { if (!SETTINGS) throw new Error(请先调用 loadSettings()); return SETTINGS.api; }注意fetch读本地 JSON 时如果你直接双击打开index.htmlfile://协议浏览器会因为跨域策略拒绝读取。解决办法是用一个静态服务器比如npx serve .或python -m http.server这也是后面验证动作的前提。4. 分页横向图片核心逻辑从写死到配置驱动原始写法里pre()和next()把图片路径拼死成images/1.jpg到images/4.jpg页数、每页数量都是硬编码。这种写法能跑但一改需求就崩。我们把它改成配置驱动。核心思路是用「当前页」和「每页数量」算出这一页的图片索引区间再统一渲染。这样不管每页 4 张还是 6 张逻辑都一样。// gallery.js let currentPage 1; let totalPage 1; let cfg null; function initGallery(settings) { cfg settings.gallery; totalPage Math.ceil(cfg.totalImages / cfg.pageSize); renderPage(1); } function renderPage(page) { if (page 1 || page totalPage) return; currentPage page; const start (page - 1) * cfg.pageSize 1; const end Math.min(page * cfg.pageSize, cfg.totalImages); let html ; for (let i start; i end; i) { html img src${cfg.imageDir}${i}${cfg.imageExt} alt图片${i}; } document.getElementById(cfg.containerId).innerHTML html; updatePager(); } function pre() { if (currentPage 1) { if (cfg.loop) renderPage(totalPage); else console.log(已经是第一页了); return; } renderPage(currentPage - 1); } function next() { if (currentPage totalPage) { if (cfg.loop) renderPage(1); else console.log(已经是最后一页了); return; } renderPage(currentPage 1); } function updatePager() { const el document.getElementById(pager); if (el) el.textContent ${currentPage} / ${totalPage}; }HTML 部分保持极简容器和按钮分开div classdiv1 iddiv1/div a onclickpre()上一页/a a onclicknext()下一页/a span idpager/spanCSS 里overflow-x: hidden配合white-space: nowrap让图片横向排列超出部分隐藏。如果你想要平滑滚动效果可以把hidden换成auto并加scroll-behavior: smooth但那样会出现滚动条看设计取舍。.div1 { width: 410px; height: 100px; overflow-x: hidden; white-space: nowrap; } .div1 img { display: inline-block; height: 100px; margin-right: 4px; }到这里分页浏览本身已经能跑了。接下来才是重点怎么把 TaoToken 的接口调用接进来让图片浏览具备「AI 能力」。5. 接入 TaoToken统一 Key 通道与请求封装图片浏览场景接 TaoToken最常见的用法是给图片生成描述或标签。比如用户翻到某一页前端把这一页的图片信息发给模型返回一段文字描述展示在图片下方。请求封装要处理三件事拼 URL、带鉴权头、处理超时和重试。下面是一个通用的request函数// api.js async function request(path, payload) { const api getApiConfig(); const url api.baseUrl path; const controller new AbortController(); const timer setTimeout(() controller.abort(), api.timeout); let lastErr null; for (let i 0; i api.maxRetries; i) { try { const res await fetch(url, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer api.apiKey }, body: JSON.stringify(payload), signal: controller.signal }); clearTimeout(timer); if (!res.ok) throw new Error(HTTP res.status); return await res.json(); } catch (e) { lastErr e; if (i api.maxRetries) await new Promise(r setTimeout(r, 500 * (i 1))); } } throw lastErr; }调用模型对话接口时路径通常是/v1/chat/completionspayload 结构如下async function describeImages(imageUrls) { const api getApiConfig(); const payload { model: api.model, messages: [ { role: user, content: 请用一句话描述这些图片的主题 imageUrls.join(、) } ] }; const data await request(/v1/chat/completions, payload); return data.choices?.[0]?.message?.content || ; }把这段接到翻页逻辑里每次renderPage之后触发一次描述请求async function renderPageWithAI(page) { renderPage(page); const start (page - 1) * cfg.pageSize 1; const end Math.min(page * cfg.pageSize, cfg.totalImages); const urls []; for (let i start; i end; i) { urls.push(${cfg.imageDir}${i}${cfg.imageExt}); } try { const desc await describeImages(urls); document.getElementById(desc).textContent desc; } catch (e) { console.warn(描述生成失败:, e.message); } }这里有个坑要注意AbortController的timer在重试循环里只设了一次如果第一次请求就超时后续重试会立刻被 abort。更严谨的写法是把controller和timer放进循环内部每次重试重新创建。我在实际项目里踩过这个坑表现为「重试永远失败」排查了半天才发现是 signal 被复用了。6. 验证动作确认接口连通与分页正确配置写完别急着上业务先做三个验证动作。动作一验证 settings.json 能加载。启动静态服务器后在浏览器控制台执行fetch(config/settings.json).then(r r.json()).then(console.log)能看到完整对象说明路径和格式没问题。如果报 404检查文件位置如果报 JSON 解析错误用 JSONLint 校验一下。动作二验证 Key 通道连通。在控制台直接发一个最小请求fetch(https://taotoken.net/api/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer sk-你的Key }, body: JSON.stringify({ model: claude-3-5-sonnet, messages: [{ role: user, content: ping }] }) }).then(r r.json()).then(console.log).catch(console.error)返回里带choices字段就说明通道通了。如果返回 401检查 Key 是否复制完整、有没有多余空格返回 404检查路径拼写返回超时检查网络和baseUrl是否写成了带斜杠的版本。动作三验证分页边界。把totalImages设成 7、pageSize设成 4然后手动调用renderPage(1)、renderPage(2)观察第二页是否只渲染 3 张图7 减 4 等于 3。再点「下一页」到最后一页确认不会越界。这一步能提前发现Math.min和Math.ceil的边界问题。三个动作都通过后再打开页面点按钮图片应该正常切换描述文字也会异步出现。如果描述一直不出现看控制台有没有描述生成失败的警告多半是 Key 或模型名的问题。7. 本篇常见错排查错误一settings.json加载失败控制台报 CORS。这是file://协议导致的不是配置问题。用npx serve .起一个本地服务或者用 VS Code 的 Live Server 插件访问http://localhost:xxxx即可。错误二翻页后图片不显示但控制台无报错。检查imageDir和imageExt拼接后的路径是否和实际文件一致。常见的是imageDir写了images但实际目录是img或者扩展名大小写不匹配.JPG和.jpg在部分服务器上不等价。错误三请求返回 401。Key 无效或过期。去 https://taotoken.net/api-keys 重新生成一个注意复制时不要带上首尾空格。如果 Key 是从环境变量注入的检查注入逻辑有没有把换行符带进去。错误四请求一直 pending 直到超时。大概率是baseUrl写错了比如写成了https://taotoken.net/api/末尾多斜杠导致路径拼成//v1/...。统一去掉末尾斜杠用baseUrl path拼接。错误五重试逻辑导致重复请求。如果maxRetries设得太大加上超时时间用户点一次按钮可能等很久。建议maxRetries不超过 2超时不超过 15 秒并且给按钮加一个 loading 状态防止用户连点。错误六图片描述返回乱码或截断。检查Content-Type是否设成了application/json以及响应解析是否用了res.json()。如果模型返回的是流式数据需要改用res.body.getReader()逐块读取普通json()会失败。8. 下一步把 Key 通道用到更多场景分页横向图片浏览只是一个入口。当你把settings.json骨架和request封装搭好之后同一套 Key 通道可以复用到很多地方给图片批量生成 alt 文本、做图片内容审核、根据图片生成 SEO 描述甚至把图片列表喂给模型做智能分组。如果你主要在写代码想把这个通道用在长期编码和 Agent 场景上可以看看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。它更适合需要持续调用、批量处理的开发流程。想先手动验证模型返回效果直接去模型对话页试https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。把上面describeImages里的 prompt 粘进去看看返回格式再决定前端怎么解析。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面有完整的接口路径和参数说明。Key 管理和生成在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。建议先把这三个页面过一遍再回头调settings.json里的model字段能少走不少弯路。最后提醒一句settings.json里的 Key 字段上线前一定要通过服务端注入替换别把明文提交到 Git 仓库。本地调试用debug.mock: true走假数据也能避免频繁消耗额度。