ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Nano Banana AI 绘画创作前端代码:用 Claude Code 生成可运行界面

2026/10/3 6:46:34 拓冰建站 浏览量
Nano Banana AI 绘画创作前端代码:用 Claude Code 生成可运行界面 1. 从零搭一个 Nano Banana AI 绘画前端到底难在哪Nano Banana AI 绘画创作前端代码说白了就是一个能输入提示词、上传参考图、把请求发给后端、再把生成结果铺成瀑布流展示的 Web 界面。它适合两类人一类是想给自己项目加个 AI 绘画入口的前端开发者另一类是手里已经有图像生成接口、但缺一个能跑起来的交互页面的独立开发者。核心检索词就三个Nano Banana AI、前端代码、Claude Code。我见过太多人卡在同一个地方界面写出来了但提示词输入框和结果展示区是断开的点「开始创作」没反应或者请求发出去了却不知道返回结构长什么样。问题不在 React 本身而在于没有把「输入 → 请求 → 渲染」这条链路一次性打通。用 Claude Code 来做这件事的好处是你可以把接口约定、组件拆分、状态管理这些琐碎决策交给它先出一版骨架你只负责校对和补细节。这篇会交付一套可复制的项目结构、关键组件代码和本地启动命令重点放在界面渲染与图像生成请求的验证步骤上。技术章节会占大头从环境准备到请求验证再到报错排查每一步都能跟着敲。你不需要先看完所有理论直接照着配置走跑通闭环之后再回头理解设计取舍。需要提前说明的是图像生成请求最终要落到一个稳定的 API 端点上。我这边实测用的是 TaoToken 的接口做请求转发和模型调用它的 Base URL 和 Key 管理比较清晰适合拿来当练习目标。下面会先讲清楚怎么拿到 Key 和配置地址再进入代码环节。2. 前置准备TaoToken 的 Key、Base URL 与模型 ID 怎么配在写前端代码之前先把「请求要打到哪、用什么身份、调哪个模型」这三件事定下来。TaoToken 这边你需要准备三样东西API Key、Base URL、Model ID。这三件套在后面的配置文件里会反复出现缺一个请求就会失败。先访问官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册并登录然后在控制台里创建 API Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 进去之后找到 API Keys 页面新建一个 Key 并复制保存。这个 Key 只会完整显示一次丢了就得重建。Base URL 统一用 https://taotoken.net/api 注意这个地址后面不加任何 UTM 参数直接写进配置就行。Model ID 取决于你想调用的图像模型在模型列表里能看到具体名称把它原样填进配置。如果你不确定用哪个可以先在模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 里试跑一次确认模型能正常返回再写进代码。对于长期做编码和 Agent 场景的可以考虑 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 额度管理会更省心。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 遇到参数不确定的时候翻一下。把这三件套整理成一张表方便你对照填写配置项值说明Base URLhttps://taotoken.net/api请求根地址不加 UTMAPI Key控制台创建后复制只显示一次妥善保存Model ID模型列表中的名称原样填入区分大小写这里有个容易踩的坑很多人把 Base URL 写成带/v1或者带斜杠结尾的形式结果请求 404。统一用上面这个不带尾斜杠的地址SDK 会自己拼接路径。另外 Key 不要硬编码进前端仓库本地开发用.env.local提交前确认.gitignore里有它。3. 可复制配置项目结构、settings 片段与关键组件代码这一节是整篇的核心我会把项目结构、配置文件、核心组件代码一次性给全。你新建一个 Next.js 项目把下面的文件按路径放进去就能跑。先看项目结构用 Claude Code 生成时可以直接把这个结构描述给它nano-banana-app/ ├── app/ │ ├── layout.tsx │ ├── page.tsx │ └── globals.css ├── components/ │ ├── Header.tsx │ ├── PromptPanel.tsx │ └── GalleryList.tsx ├── lib/ │ └── api.ts ├── .env.local ├── next.config.js ├── tailwind.config.ts └── package.json环境变量文件.env.local这样写注意不要提交到仓库# .env.local NEXT_PUBLIC_TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_MODEL_ID你的模型ID如果你用的是 Claude Code 的 settings 配置来管理项目级环境可以在项目根目录建.claude/settings.json把允许访问的域名和命令白名单写进去避免每次执行都弹确认{ permissions: { allow: [ Bash(npm run dev), Bash(npm install), WebFetch(domain:taotoken.net) ] }, env: { TAOTOKEN_BASE_URL: https://taotoken.net/api } }接下来是请求封装lib/api.ts把 Base URL、Key、Model ID 三件套集中管理前端组件只调函数不碰密钥// lib/api.ts const BASE_URL process.env.NEXT_PUBLIC_TAOTOKEN_BASE_URL!; const MODEL_ID process.env.TAOTOKEN_MODEL_ID!; export interface DrawRequest { prompt: string; images?: string[]; } export interface DrawResult { code: number; message?: string; data: { uid: string; createTime: number; info: { message?: string; originalMessage?: string; aiMessage?: string; imgUrlCdn?: string | string[]; visionImgList?: Array{ url: string }; }; }; } export async function createDrawTask(payload: DrawRequest): PromiseDrawResult { const res await fetch(${BASE_URL}/draw/imagine, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.TAOTOKEN_API_KEY}, }, body: JSON.stringify({ model: MODEL_ID, prompt: payload.prompt, images: payload.images ?? [], drawType: grid, type: imagine, }), }); if (!res.ok) { throw new Error(请求失败: ${res.status} ${res.statusText}); } return (await res.json()) as DrawResult; } export async function fetchHistory(pageNum 1, pageSize 10) { const res await fetch( ${BASE_URL}/draw/history?pageNum${pageNum}pageSize${pageSize}, { headers: { Authorization: Bearer ${process.env.TAOTOKEN_API_KEY}, }, } ); if (!res.ok) throw new Error(历史加载失败: ${res.status}); return res.json(); }然后是核心的提示词面板组件components/PromptPanel.tsx负责输入、上传、发送三件事use client; import { useState, useRef } from react; import { Sparkles, Send, ImagePlus, X, RefreshCw } from lucide-react; import { createDrawTask } from /lib/api; interface Props { onCreated: () void; } export default function PromptPanel({ onCreated }: Props) { const [input, setInput] useState(); const [images, setImages] useStatestring[]([]); const [drawing, setDrawing] useState(false); const fileRef useRefHTMLInputElement(null); const handleSend async () { if (!input.trim() || drawing) return; setDrawing(true); try { await createDrawTask({ prompt: input, images }); setInput(); setImages([]); onCreated(); } catch (err) { console.error(创作失败:, err); } finally { setDrawing(false); } }; return ( div classNamerounded-2xl border-2 border-primary/20 bg-card/90 p-6 shadow-xl div classNamemb-3 flex items-center gap-2 Sparkles classNameh-5 w-5 text-primary / h2 classNametext-lg font-boldNano Banana AI 绘画创作/h2 /div {images.length 0 ( div classNamemb-3 flex flex-wrap gap-2 {images.map((img, i) ( div key{i} classNamerelative h-16 w-16 overflow-hidden rounded-lg border img src{img} alt{上传${i 1}} classNameh-full w-full object-cover / button onClick{() setImages((p) p.filter((_, idx) idx ! i))} classNameabsolute right-1 top-1 rounded-full bg-red-500/80 p-0.5 X classNameh-3 w-3 text-white / /button /div ))} /div )} textarea value{input} onChange{(e) setInput(e.target.value)} onKeyDown{(e) { if (e.key Enter !e.shiftKey) { e.preventDefault(); handleSend(); } }} placeholder描述您想要创作的图像... disabled{drawing} rows{3} maxLength{2000} classNamew-full resize-none rounded-xl border-2 border-border bg-background/50 px-4 py-3 focus:border-primary focus:outline-none / div classNamemt-3 flex items-center gap-3 input ref{fileRef} typefile acceptimage/* classNamehidden onChange{(e) { const f e.target.files?.[0]; if (f) setImages((p) [...p, URL.createObjectURL(f)]); }} / button onClick{() fileRef.current?.click()} disabled{drawing || images.length 3} classNameflex items-center gap-2 rounded-xl border px-4 py-2 disabled:opacity-50 ImagePlus classNameh-4 w-4 / 上传 /button div classNameflex-1 / button onClick{handleSend} disabled{(!input.trim() images.length 0) || drawing} classNameflex items-center gap-2 rounded-xl bg-gradient-to-r from-primary to-secondary px-6 py-2 font-semibold text-white disabled:opacity-50 {drawing ? RefreshCw classNameh-4 w-4 animate-spin / : Send classNameh-4 w-4 /} {drawing ? 创作中... : 开始创作} /button /div /div ); }最后是主页面app/page.tsx把面板和画廊拼起来use client; import { useEffect, useState, useCallback } from react; import PromptPanel from /components/PromptPanel; import GalleryList from /components/GalleryList; import { fetchHistory } from /lib/api; export default function Page() { const [list, setList] useStateany[]([]); const [loading, setLoading] useState(false); const load useCallback(async () { setLoading(true); try { const res await fetchHistory(1, 10); setList(res.data?.list ?? []); } catch (e) { console.error(e); } finally { setLoading(false); } }, []); useEffect(() { load(); }, [load]); return ( main classNamemx-auto flex min-h-screen max-w-6xl flex-col gap-4 px-4 py-6 PromptPanel onCreated{load} / GalleryList data{list} loading{loading} / /main ); }GalleryList组件负责把imgUrlCdn和visionImgList两种返回结构都渲染出来逻辑和前面 excerpt 里的思路一致这里不再重复贴全你按同样的模式处理数组和单值两种情况即可。启动命令就两条npm install npm run dev打开http://localhost:3000就能看到界面。4. 验证请求从提示词输入到结果展示的闭环怎么跑通代码放好之后别急着美化界面先把请求链路验证一遍。这一步的目标是确认「输入提示词 → 发出请求 → 拿到图片 URL → 页面渲染」四步都通。第一步打开浏览器开发者工具的 Network 面板在输入框里敲一句简单的提示词比如「一只戴墨镜的柴犬扁平插画风格」点「开始创作」。这时候你应该能看到一个发往https://taotoken.net/api/draw/imagine的 POST 请求。点开它检查三件事请求头里Authorization是不是Bearer sk-开头请求体里model字段是不是你配置的 Model ID响应状态码是不是 200。第二步看响应体结构。正常返回会长这样{ code: 200, message: success, data: { uid: task_abc123, createTime: 1730000000000, info: { originalMessage: 一只戴墨镜的柴犬扁平插画风格, aiMessage: 正在为你生成图像..., imgUrlCdn: [ https://static.example.com/ai/result_1.png, https://static.example.com/ai/result_2.png ] } } }如果imgUrlCdn是数组说明一次生成了多张图如果是字符串就是单图。你的GalleryList要能同时处理这两种情况否则遇到单图返回时页面会空白。第三步确认图片能渲染。把返回的 URL 直接粘到浏览器地址栏能打开就说明 CDN 没问题。如果图片打不开多半是返回的是相对路径或者需要鉴权这时候要在前端拼上完整域名。第四步验证历史记录。刷新页面fetchHistory应该能拉回刚才那条记录提示词和图片都正确显示。如果历史为空检查pageNum和pageSize参数有没有传对以及后端是否真的落库了。我试过在本地把NEXT_PUBLIC_TAOTOKEN_BASE_URL故意写错一个字母结果请求直接打到 404Network 面板里一片红。所以验证阶段一定要盯着 Network 看别只看界面有没有报错弹窗。界面不弹错不代表请求成功很多失败是静默的。如果你想让验证更彻底可以在lib/api.ts里临时加一行console.log(JSON.stringify(payload))把发出去的请求体打出来对照文档确认字段名没写错。确认无误后再删掉这行。5. 常见报错排查401、local proxy failed、reading choices 怎么解这一节按真实报错来你遇到哪个就查哪个。401 Unauthorized。这是最常见的九成是 Key 的问题。先确认.env.local里的TAOTOKEN_API_KEY没有多余空格然后确认请求头拼的是Bearer加 Key中间有一个空格。如果你把 Key 写进了NEXT_PUBLIC_开头的变量注意它会被打包进前端虽然能跑但不安全正式环境要挪到服务端路由。还有一种情况是 Key 被控制台删了或者过期了重新建一个换上即可。local proxy failed。这个报错通常出现在你本地配了代理或者请求被拦截的时候。先检查系统代理设置把HTTP_PROXY、HTTPS_PROXY这些环境变量清掉再试。如果你在next.config.js里配了 rewrites 转发确认目标地址写的是https://taotoken.net/api而不是别的。这个错和网络环境有关和代码本身关系不大清掉代理基本能解决。reading choices。这个报错说明你在解析响应时按 OpenAI 的choices结构去取值了但实际返回的不是这个结构。图像生成接口返回的是data.info.imgUrlCdn这种结构不是choices[0].message.content。检查你的解析代码把res.choices改成res.data.info。如果你用的是某个 SDK 默认按 chat 格式解析要么换解析方式要么在请求里明确指定返回格式。OAuth 相关报错。如果你在 Claude Code 里配置了 OAuth 登录偶尔会遇到 token 过期导致的鉴权失败。这时候重新走一遍登录流程或者在 settings 里确认 token 刷新配置。注意区分OAuth 是 Claude Code 自身的登录态和 TaoToken 的 API Key 是两回事别混在一起排查。图片渲染空白但请求 200。检查imgUrlCdn是不是数组你的渲染逻辑有没有处理数组。另外 Next.js 的Image组件需要配置remotePatterns如果图片域名不在白名单里会渲染失败。在next.config.js里加上module.exports { images: { remotePatterns: [ { protocol: https, hostname: **.taotoken.net }, { protocol: https, hostname: **.example.com }, ], }, };把example.com换成你实际的图片 CDN 域名。这个坑很隐蔽因为 Network 里图片请求是 200但页面上就是不显示。排查顺序建议固定下来先看 Network 状态码再看响应体结构最后看前端渲染逻辑。按这个顺序走大部分问题五分钟内能定位。6. 把闭环跑顺之后下一步可以做什么界面能跑通、请求能返回、图片能展示这个最小闭环就算完成了。接下来你可以按自己的需求往上加东西。比如把提示词历史做成可点击的标签点一下自动填充比如给生成结果加个「重新生成」按钮复用同一个提示词再发一次请求比如把上传的参考图真正传给后端做图生图而不是只在前端展示。如果你打算长期做这类 AI 绘画工具建议把请求层抽成独立的服务前端只负责交互密钥和模型调用都放服务端。这样既安全也方便你换模型或者加限流。TaoToken 的接入文档里有完整的参数说明遇到字段不确定的时候去翻一下比猜快。最后留一个实用技巧在PromptPanel里给发送按钮加个防抖或者用drawing状态锁住避免用户连点导致重复提交。这个细节在演示的时候特别重要连点两次生成两批图体验会很差。代码里已经用drawing做了拦截你确认一下disabled条件写对就行。