Expose Studio cover rendering API and add three source-based layout tools
studio CI and deployment / verify-and-deploy (push) Successful in 3m51s

This commit is contained in:
2026-10-26 20:26:17 +08:00
parent 7f37aa4c93
commit 4ba7eb7db7
12 changed files with 455 additions and 21 deletions
+18 -11
View File
@@ -1,19 +1,26 @@
import {createElement} from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import {serveApiDocumentation} from '../../../lib/api-documentation';
import type {NextApiRequest,NextApiResponse} from 'next';
import {ideas,ideaMarkdown} from '../../../content/ideas';
import {Ref17Cover} from '../../../components/Ref17Cover';
import {selections} from '../../../components/SourceCover';
import {renderCover} from '../../../lib/render-cover';
import {tutorialPost} from '../../../content/tool-links';
export default function handler(req:NextApiRequest,res:NextApiResponse){
if(serveApiDocumentation(req,res,'/studio/api/export/{slug}'))return;
if(req.method!=='GET')return res.status(405).end();
const idea=ideas.find(i=>i.slug===req.query.slug);if(!idea)return res.status(404).end();
if(req.query.format==='markdown'){res.setHeader('Content-Type','text/markdown; charset=utf-8');return res.send(ideaMarkdown(idea));}
if(req.query.format==='svg'){
const raw=req.query.slide??'0',index=typeof raw==='string'&&/^\d+$/.test(raw)?Number(raw):-1;
if(!idea.template||!idea.slides[index])return res.status(400).json({error:'SVG requires a template and a valid zero-based slide index.'});
res.setHeader('Content-Type','image/svg+xml; charset=utf-8');res.setHeader('Content-Disposition',`inline; filename="${idea.slug}-${index}.svg"`);
return res.send(renderToStaticMarkup(createElement(Ref17Cover,{template:idea.template,slide:idea.slides[index]})));
if(!['GET','POST'].includes(req.method||'')){res.setHeader('Allow','GET, POST');return res.status(405).end();}
const slug=String(req.query.slug||''),idea=ideas.find(i=>i.slug===slug);if(!idea)return res.status(404).end();
if(req.method==='POST'||req.query.format==='svg'){
if(req.query.format!==undefined&&!['svg','json'].includes(String(req.query.format)))return res.status(400).json({error:'渲染格式必须为 svg 或 json'});
try{
const raw=req.query.slide??'0';if(req.method==='GET'&&(typeof raw!=='string'||!/^\d+$/.test(raw)))throw Error('无效 slide');
const input=req.method==='POST'?req.body:idea.tool?{layoutId:req.query.layoutId??selections[0].id}:{slide:Number(raw)};
const result=renderCover(slug,input);res.setHeader('Cache-Control','no-store');
res.setHeader('Content-Disposition',`attachment; filename="${slug}.${req.query.format==='json'?'json':'svg'}"`);
if(req.query.format==='json')return res.json(result.data);
res.setHeader('Content-Type','image/svg+xml; charset=utf-8');return res.send(renderToStaticMarkup(result.node));
}catch(error){return res.status(400).json({error:error instanceof Error?error.message:'无效参数'});}
}
res.setHeader('Content-Disposition',`attachment; filename="${idea.slug}.json"`);return res.json({version:idea.template?2:1,...idea});
if(req.query.format==='markdown'){res.setHeader('Content-Type','text/markdown; charset=utf-8');return res.send(ideaMarkdown(idea)+'\n\n使用教程:'+tutorialPost+(idea.tool?'\n\n```json\n'+JSON.stringify(selections,null,2)+'\n```':''));}
res.setHeader('Content-Disposition',`attachment; filename="${idea.slug}.json"`);return res.json({version:idea.tool?3:idea.template?2:1,...idea,...(idea.tool?{layouts:selections}:{}),tutorialPost});
}
export const config={api:{bodyParser:{sizeLimit:'32kb'}}};