news 2026/2/9 2:42:39

通义万相 (Wan) 2.6 API 可视化调用面板

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
通义万相 (Wan) 2.6 API 可视化调用面板

通义万相图像生成模型支持图像编辑、图文混排输出,满足多样化生成与集成需求。

以下是一个基于 React 的通义万相 (Wan) API 可视化调用面板

效果图:

由于目前通义万相(Wan 2.1/2.0)的视频生成通常是异步任务(提交任务 -> 轮询状态 -> 获取结果),这个面板完整实现了这套全流程逻辑

核心功能:

  1. 配置管理:输入 API Key(存储在本地浏览器内存中,刷新即焚,保证安全)。
  2. 参数控制:支持模型选择(预置了 Wan 2.1 等)、提示词、分辨率、时长等参数调节。
  3. 任务流处理:自动处理“提交任务”与“轮询进度”的交互逻辑。
  4. 调试友好:右侧实时显示发送的 JSON 请求体和 API 返回的原始日志,方便排查问题。
id: wan-api-dashboard name: Tongyi Wan API Dashboard type: tsx content: |- import React, { useState, useEffect, useRef } from 'react'; import { Play, Image as ImageIcon, Settings, Terminal, Loader2, AlertCircle, CheckCircle2, Copy, Eye, EyeOff } from 'lucide-react'; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Badge } from "@/components/ui/badge"; const WanDashboard = () => { // Configuration State const [apiKey, setApiKey] = useState(''); const [showKey, setShowKey] = useState(false); // Generation State const [prompt, setPrompt] = useState('A cinematic shot of a futuristic city with flying cars, cyberpunk style, neon lights, 4k resolution'); const [negativePrompt, setNegativePrompt] = useState('low quality, blurry, distorted'); const [model, setModel] = useState('wan2.1-t2v-plus'); const [resolution, setResolution] = useState('1280*720'); const [duration, setDuration] = useState('5'); // Process State const [status, setStatus] = useState('idle'); // idle, submitting, polling, success, error const [taskId, setTaskId] = useState(''); const [progress, setProgress] = useState(0); const [resultUrl, setResultUrl] = useState(''); const [logs, setLogs] = useState([]); const [errorMsg, setErrorMsg] = useState(''); // Refs for polling const pollInterval = useRef(null); const addLog = (type, message, data = null) => { const timestamp = new Date().toLocaleTimeString(); setLogs(prev => [{ time: timestamp, type, message, data }, ...prev]); }; // Cleanup on unmount useEffect(() => { return () => { if (pollInterval.current) clearInterval(pollInterval.current); }; }, []); const handleGenerate = async () => { if (!apiKey) { setErrorMsg('Please enter your DashScope API Key first.'); return; } setStatus('submitting'); setErrorMsg(''); setResultUrl(''); setLogs([]); setProgress(0); // 1. Construct Request const headers = { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'X-DashScope-Async': 'enable' // Important for video generation }; const payload = { model: model, input: { prompt: prompt, negative_prompt: negativePrompt }, parameters: { size: resolution, duration: parseInt(duration), // seed: Math.floor(Math.random() * 1000000) // Optional } }; addLog('req', 'Submitting Task...', payload); try { // NOTE: In a real browser environment, calling DashScope directly might trigger CORS errors. // This code assumes the environment supports it or a proxy is used. // For the artifact demo, we will simulate the flow if it fails due to CORS, // but the code structure is correct for a backend/proxy setup. const submitResponse = await fetch('https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation/generation', { method: 'POST', headers: headers, body: JSON.stringify(payload) }); if (!submitResponse.ok) { const errorData = await submitResponse.json(); throw new Error(errorData.message || `HTTP Error: ${submitResponse.status}`); } const submitData = await submitResponse.json(); addLog('res', 'Task Submitted Successfully', submitData); if (submitData.output && submitData.output.task_id) { const newTaskId = submitData.output.task_id; setTaskId(newTaskId); setStatus('polling'); startPolling(newTaskId, headers); } else { throw new Error('No task_id received'); } } catch (err) { console.error(err); setStatus('error'); setErrorMsg(err.message); addLog('err', 'Submission Failed', err.message); // CORS Hint for user if (err.message.includes('Failed to fetch') || err.message.includes('NetworkError')) { addLog('info', 'CORS WARNING: Direct browser calls to DashScope are often blocked. You may need a backend proxy or a browser extension to allow CORS.'); } } }; const startPolling = (tid, headers) => { let attempts = 0; pollInterval.current = setInterval(async () => { attempts++; try { const pollUrl = `https://dashscope.aliyuncs.com/api/v1/tasks/${tid}`; const pollRes = await fetch(pollUrl, { method: 'GET', headers: headers }); if (!pollRes.ok) throw new Error(`Polling Error: ${pollRes.status}`); const pollData = await pollRes.json(); const taskStatus = pollData.output.task_status; addLog('poll', `Status: ${taskStatus} (Attempt ${attempts})`, pollData); if (taskStatus === 'SUCCEEDED') { clearInterval(pollInterval.current); setStatus('success'); if (pollData.output.video_url) { setResultUrl(pollData.output.video_url); } addLog('success', 'Generation Complete!', pollData.output); } else if (taskStatus === 'FAILED' || taskStatus === 'CANCELED') { clearInterval(pollInterval.current); setStatus('error'); setErrorMsg(pollData.output.message || 'Task Failed'); } else { // RUNNING or PENDING // Simulate progress since API doesn't always return percentage setProgress(prev => Math.min(prev + 5, 95)); } } catch (err) { addLog('err', 'Polling Error', err.message); // Don't stop polling immediately on network glitch, but stop after too many fails if (attempts > 60) { clearInterval(pollInterval.current); setStatus('error'); setErrorMsg('Polling timed out'); } } }, 3000); // Poll every 3 seconds }; const copyToClipboard = (text) => { navigator.clipboard.writeText(text); }; return ( <div className="min-h-screen bg-slate-50 p-4 md:p-8 font-sans text-slate-900"> <div className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-3 gap-6"> {/* Header */} <div className="lg:col-span-3 flex items-center justify-between mb-2"> <div> <h1 className="text-2xl font-bold flex items-center gap-2"> <Play className="h-6 w-6 text-purple-600" /> Tongyi Wan API Studio </h1> <p className="text-slate-500 text-sm">Visual interface for Alibaba Cloud Wan Video Generation Models</p> </div> <div className="flex gap-2"> <Badge variant="outline" className="bg-white">v1.0.0</Badge> </div> </div> {/* Left Column: Controls */} <div className="lg:col-span-1 space-y-6"> {/* API Key Config */} <Card className="border-slate-200 shadow-sm"> <CardHeader className="pb-3"> <CardTitle className="text-sm font-medium uppercase tracking-wider text-slate-500 flex items-center gap-2"> <Settings className="h-4 w-4" /> Configuration </CardTitle> </CardHeader> <CardContent className="space-y-4"> <div className="space-y-2"> <Label htmlFor="apiKey">DashScope API Key</Label> <div className="relative"> <Input id="apiKey" type={showKey ? "text" : "password"} placeholder="sk-..." value={apiKey} onChange={(e) => setApiKey(e.target.value)} className="pr-10" /> <button onClick={() => setShowKey(!showKey)} className="absolute right-2 top-2.5 text-slate-400 hover:text-slate-600" > {showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} </button> </div> <p className="text-xs text-slate-400"> Key is stored in memory only. Never shared. </p> </div> </CardContent> </Card> {/* Generation Controls */} <Card className="border-slate-200 shadow-sm h-fit"> <CardHeader className="pb-3"> <CardTitle className="text-sm font-medium uppercase tracking-wider text-slate-500 flex items-center gap-2"> <Play className="h-4 w-4" /> Parameters </CardTitle> </CardHeader> <CardContent className="space-y-4"> <div className="space-y-2"> <Label>Model Version</Label> <Select value={model} onValueChange={setModel}> <SelectTrigger> <SelectValue placeholder="Select Model" /> </SelectTrigger> <SelectContent> <SelectItem value="wan2.1-t2v-plus">wan2.1-t2v-plus (Flagship)</SelectItem> <SelectItem value="wan2.1-t2v-turbo">wan2.1-t2v-turbo (Fast)</SelectItem> <SelectItem value="wan2.0-t2v-turbo">wan2.0-t2v-turbo</SelectItem> </SelectContent> </Select> </div> <div className="space-y-2"> <Label>Resolution</Label> <Select value={resolution} onValueChange={setResolution}> <SelectTrigger> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="1280*720">1280x720 (Landscape)</SelectItem> <SelectItem value="720*1280">720x1280 (Portrait)</SelectItem> <SelectItem value="1024*1024">1024x1024 (Square)</SelectItem> </SelectContent> </Select> </div> <div className="space-y-2"> <Label>Duration (Seconds)</Label> <Select value={duration} onValueChange={setDuration}> <SelectTrigger> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="5">5 Seconds</SelectItem> </SelectContent> </Select> </div> <div className="space-y-2"> <Label>Prompt</Label> <Textarea placeholder="Describe your video..." className="min-h-[100px] resize-none" value={prompt} onChange={(e) => setPrompt(e.target.value)} /> </div> <div className="space-y-2"> <Label>Negative Prompt</Label> <Input placeholder="What to avoid..." value={negativePrompt} onChange={(e) => setNegativePrompt(e.target.value)} /> </div> <Button className="w-full bg-purple-600 hover:bg-purple-700 text-white" onClick={handleGenerate} disabled={status === 'submitting' || status === 'polling'} > {status === 'submitting' || status === 'polling' ? ( <><Loader2 className="mr-2 h-4 w-4 animate-spin" /> Generating...</> ) : ( <><Play className="mr-2 h-4 w-4" /> Generate Video</> )} </Button> </CardContent> </Card> </div> {/* Right Column: Preview & Logs */} <div className="lg:col-span-2 space-y-6"> {/* Preview Area */} <Card className="border-slate-200 shadow-sm min-h-[400px] flex flex-col"> <CardHeader className="pb-3 border-b border-slate-100"> <div className="flex justify-between items-center"> <CardTitle className="text-sm font-medium uppercase tracking-wider text-slate-500 flex items-center gap-2"> <ImageIcon className="h-4 w-4" /> Preview </CardTitle> {status === 'polling' && ( <Badge variant="secondary" className="animate-pulse">Processing... {progress}%</Badge> )} {status === 'success' && ( <Badge className="bg-green-500 hover:bg-green-600">Completed</Badge> )} </div> </CardHeader> <CardContent className="flex-1 flex items-center justify-center bg-slate-100/50 p-0 relative overflow-hidden"> {status === 'idle' && ( <div className="text-center text-slate-400"> <Play className="h-12 w-12 mx-auto mb-2 opacity-20" /> <p>Ready to generate</p> </div> )} {(status === 'submitting' || status === 'polling') && ( <div className="text-center space-y-4"> <div className="relative h-24 w-24 mx-auto"> <div className="absolute inset-0 border-4 border-purple-200 rounded-full"></div> <div className="absolute inset-0 border-4 border-purple-600 rounded-full border-t-transparent animate-spin"></div> </div> <div> <p className="font-medium text-slate-700">AI is dreaming...</p> <p className="text-sm text-slate-400">Task ID: {taskId}</p> </div> </div> )} {status === 'error' && ( <div className="text-center text-red-500 p-6"> <AlertCircle className="h-12 w-12 mx-auto mb-2" /> <p className="font-medium">Generation Failed</p> <p className="text-sm mt-2 bg-red-50 p-2 rounded border border-red-100">{errorMsg}</p> </div> )} {status === 'success' && resultUrl && ( <div className="w-full h-full flex flex-col"> <video controls autoPlay loop className="w-full h-full object-contain bg-black" src={resultUrl} /> </div> )} </CardContent> {status === 'success' && resultUrl && ( <CardFooter className="bg-white border-t border-slate-100 p-3 flex justify-end"> <Button variant="outline" size="sm" onClick={() => window.open(resultUrl, '_blank')}> Download Video </Button> </CardFooter> )} </Card> {/* Logs Console */} <Card className="border-slate-200 shadow-sm flex-1 flex flex-col"> <CardHeader className="pb-3 bg-slate-900 text-slate-200 rounded-t-lg"> <CardTitle className="text-sm font-mono flex items-center gap-2"> <Terminal className="h-4 w-4" /> API Console / Logs </CardTitle> </CardHeader> <CardContent className="p-0 bg-slate-950"> <ScrollArea className="h-[250px] w-full p-4 font-mono text-xs"> {logs.length === 0 ? ( <span className="text-slate-600">// Waiting for requests...</span> ) : ( logs.map((log, i) => ( <div key={i} className="mb-3 border-b border-slate-800/50 pb-2 last:border-0"> <div className="flex gap-2 mb-1"> <span className="text-slate-500">[{log.time}]</span> <span className={`font-bold ${ log.type === 'req' ? 'text-blue-400' : log.type === 'res' ? 'text-green-400' : log.type === 'err' ? 'text-red-400' : 'text-yellow-400' }`}> {log.type.toUpperCase()} </span> <span className="text-slate-300">{log.message}</span> </div> {log.data && ( <pre className="text-slate-500 overflow-x-auto bg-slate-900/50 p-2 rounded"> {JSON.stringify(log.data, null, 2)} </pre> )} </div> )) )} </ScrollArea> </CardContent> </Card> </div> </div> </div> ); }; export default WanDashboard;

使用说明与注意事项

  1. 关于 CORS(跨域)报错

    • 现象:如果你直接在浏览器运行上述代码并点击生成,可能会在控制台看到Access-Control-Allow-Origin相关的红色报错。
    • 原因:阿里云 DashScope API 出于安全考虑,默认不允许纯浏览器前端直接发起调用(防止你的 API Key 暴露在公网)。
    • 解决方案
      • 本地测试:你可以安装 Chrome 插件(如 “Allow CORS”)临时绕过限制进行测试。
      • 正式使用:你需要一个简单的后端(Node.js/Python)作为中转,或者使用 Next.js 的 API Route。
  2. 模型版本

    • 代码中预置了wan2.1-t2v-plus,这是目前的旗舰视频模型。如果官方发布了更新的版本(如你提到的 2.6),你只需在左侧下拉菜单选择或直接修改代码中的model参数即可。
  3. 如何获取 API Key

    • 访问 阿里云百炼控制台。
    • 开通 DashScope 服务并创建一个 API Key。
    • 将 Key 粘贴到面板左上角的配置框中即可。

这个面板展示了不使用任何第三方套壳工具,仅用标准的 Web 技术(React + fetch)即可完成与大模型的深度交互。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/5 23:00:47

bsdiff/bspatch:高效二进制差异补丁技术深度解析

bsdiff/bspatch&#xff1a;高效二进制差异补丁技术深度解析 【免费下载链接】bsdiff bsdiff and bspatch are libraries for building and applying patches to binary files. 项目地址: https://gitcode.com/gh_mirrors/bs/bsdiff 你是否曾为大型软件更新包下载耗时过…

作者头像 李华
网站建设 2026/2/8 7:43:36

高温环境下PCB线宽与电流对照表修正方法探讨

高温环境下如何科学修正PCB线宽与电流关系&#xff1f;工程师不可忽视的热设计实战指南你有没有遇到过这样的情况&#xff1a;明明按照标准PCB线宽与电流对照表选了走线宽度&#xff0c;结果样机一上电&#xff0c;大电流路径附近的铜皮就开始发烫&#xff0c;甚至在高温老化测…

作者头像 李华
网站建设 2026/1/29 19:32:28

学术论文免费获取神器:三分钟搞定Sci-Hub X Now完整部署

学术论文免费获取神器&#xff1a;三分钟搞定Sci-Hub X Now完整部署 【免费下载链接】sci-hub-now 项目地址: https://gitcode.com/gh_mirrors/sc/sci-hub-now 还在为昂贵的论文下载费用发愁吗&#xff1f;想不想拥有一个能让你随时随地免费获取学术文献的神奇工具&…

作者头像 李华
网站建设 2026/1/29 14:04:58

UDS多帧传输在CANoe中的模拟与验证

UDS多帧传输在CANoe中的实战模拟与深度验证从一个诊断失败说起&#xff1a;为什么我们需要关注多帧&#xff1f;某日&#xff0c;一位工程师在刷写ECU软件时发现&#xff0c;当请求读取某个大尺寸DID&#xff08;数据标识符&#xff09;时&#xff0c;响应总是被截断或直接超时…

作者头像 李华
网站建设 2026/2/7 21:05:32

AI数独照片解题:终极免费智能工具完整指南

AI_Sudoku是一款革命性的智能数独解题工具&#xff0c;它能从普通照片中自动识别数独题目并快速给出准确答案。这款终极AI助手结合了先进的计算机视觉和深度学习技术&#xff0c;为数独爱好者提供了前所未有的便捷解题体验。无论你是数独新手还是资深玩家&#xff0c;这款免费工…

作者头像 李华
网站建设 2026/2/6 18:58:08

终极快速AI图像生成:ComfyUI工作流完整解决方案

在当今快节奏的数字内容创作领域&#xff0c;快速AI图像生成已成为创意工作者的必备技能。借助ComfyUI工作流&#xff0c;即使是新手用户也能在几分钟内创作出专业级图像素材。 【免费下载链接】Qwen-Image-Edit-Rapid-AIO 项目地址: https://ai.gitcode.com/hf_mirrors/Phr…

作者头像 李华