Connect all 27 source layouts to parameterized Studio pages and shared design storage
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import {useId} from 'react';
|
||||
import type {Layout,Parameters} from '../lib/layout-parameters';
|
||||
export type LayerLayout=Omit<Layout,'elements'>&{source:string;background:string;tutorialUrl:string;sourceSha256:string;elements:(Layout['elements'][number]&{raster:string;shape?:string})[]};
|
||||
export function LayoutCanvas({layout,params,guides=false,imageSources={}}:{layout:LayerLayout;params:Parameters;guides?:boolean;imageSources?:Record<string,string>}){
|
||||
const uid=useId().replace(/[^a-z0-9]/gi,''),W=layout.width,H=layout.height,unchanged=!params.paper&&!Object.keys(params.texts).length&&!Object.keys(params.images).length&&!Object.keys(params.decorations).length;
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${W} ${H}`} width={W*6} height={H*6} role="img" aria-label={layout.id+' '+layout.name} data-layout-id={layout.id} style={{width:'100%',height:'auto',display:'block'}}><title>{layout.id+' · '+layout.name}</title><desc>对应原图的文字、图位和装饰。未替换构件保留原始低分辨率图层;替换文字为 SVG 文字。</desc>{unchanged?<image href={layout.source} width={W} height={H}/>:<><rect width={W} height={H} fill={params.paper||'#f4f2eb'}/><image href={layout.background} width={W} height={H}/>{params.paper&&<><defs><mask id={uid+'paper'}><image href={layout.background} width={W} height={H}/></mask></defs><rect width={W} height={H} fill={params.paper} mask={'url(#'+uid+'paper)'}/></>}{['image','decoration','text'].flatMap(kind=>layout.elements.filter(s=>s.kind===kind).map(s=>{
|
||||
const [x,y,w,h]=s.box,id=uid+s.id,t=params.texts[s.id],im=params.images[s.id],d=params.decorations[s.id];
|
||||
if(kind==='text'&&t){const lines=t.value.split('\n'),len=(v:string)=>Array.from(v).reduce((a,c)=>a+(/[\u3400-\u9fff]/.test(c)?1:.6),0),vertical=s.direction==='vertical',size=Math.min(vertical?w*.9:h/(lines.length*1.2),vertical?h/Math.max(1,Array.from(t.value).length):w/Math.max(1,...lines.map(len)))*(t.scale||1),align=t.align||'start';return <g key={s.id} data-slot={s.id} fill={t.color||'#26362f'} fontFamily="Arial,'Noto Sans CJK SC','PingFang SC',sans-serif"><defs><clipPath id={id+'clip'}><rect x={x} y={y} width={w} height={h}/></clipPath></defs><g clipPath={`url(#${id}clip)`}>{vertical?Array.from(t.value.replaceAll('\n','')).map((c,i)=><text key={i} x={x+w/2} y={y+size*(i+.85)} textAnchor="middle" fontSize={size}>{c}</text>):s.direction==='curved'?<><defs><path id={id+'path'} d={`M${x} ${y+h*.2} Q${x+w/2} ${y+h*1.7} ${x+w} ${y+h*.2}`}/></defs><text fontSize={size}><textPath href={'#'+id+'path'}>{t.value}</textPath></text></>:lines.map((l,i)=><text key={i} x={x+(align==='middle'?w/2:align==='end'?w:0)} y={y+size*(.88+i*1.2)} textAnchor={align} fontSize={size}>{l}</text>)}</g></g>}
|
||||
if(kind==='decoration'&&d?.hidden)return null;
|
||||
const fill=kind==='image'?im?.color:d?.color;
|
||||
if(fill||im?.assetId)return <g key={s.id} data-slot={s.id}><defs><mask id={id} maskUnits="userSpaceOnUse" x={x} y={y} width={w} height={h} style={{maskType:'alpha'}}><image href={s.shape||s.raster} x={x} y={y} width={w} height={h}/></mask></defs><g mask={'url(#'+id+')'}>{fill&&<rect x={x} y={y} width={w} height={h} fill={fill}/>} {im?.assetId&&<image href={imageSources[im.assetId]||'/api/media/'+im.assetId} x={x} y={y} width={w} height={h} preserveAspectRatio={im.fit==='contain'?'xMidYMid meet':'xMidYMid slice'}/>}</g></g>;
|
||||
return <image key={s.id} data-slot={s.id} href={s.raster} x={x} y={y} width={w} height={h}/>;
|
||||
}))}</>}{guides&&layout.elements.map(s=>{const[x,y,w,h]=s.box,color=s.kind==='text'?'#2765da':s.kind==='image'?'#d86b27':'#08875d';return <g key={s.id} aria-hidden="true"><rect x={x} y={y} width={w} height={h} fill="none" stroke={color} strokeWidth=".35"/><text x={x} y={y+3} fontSize="3" fill={color}>{s.id}</text></g>})}</svg>;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import {useEffect,useState} from 'react';
|
||||
import Head from 'next/head';
|
||||
import {LayoutCanvas,type LayerLayout} from './LayoutCanvas';
|
||||
import {parameters,type Parameters} from '../lib/layout-parameters';
|
||||
import layouts from '../content/templates/layouts.json';
|
||||
type Design={id:string;name:string;layoutId:string;parameters:Parameters;revision:number;url:string};
|
||||
export function LayoutWorkbench({layoutId,designId}:{layoutId?:string;designId?:string}){
|
||||
const [active,setActive]=useState(layoutId||''),[params,setParams]=useState<Parameters>({texts:{},images:{},decorations:{}}),[design,setDesign]=useState<Design|null>(null),[name,setName]=useState('新排版效果'),[status,setStatus]=useState(''),[guides,setGuides]=useState(false),[busy,setBusy]=useState(false),[raw,setRaw]=useState(''),[recent,setRecent]=useState<Design[]>([]);
|
||||
const layout=layouts.find(l=>l.id===active) as LayerLayout|undefined;
|
||||
useEffect(()=>{if(!designId)return;let live=true;fetch('/api/studio/designs/'+designId).then(async r=>{const d=await r.json();if(!r.ok)throw Error(d.error);if(live){setDesign(d.design);setActive(d.design.layoutId);setParams(d.design.parameters);setName(d.design.name)}}).catch(e=>{if(live)setStatus(e.message)});return()=>{live=false}},[designId]);
|
||||
useEffect(()=>{if(!active)return;let live=true;fetch('/api/studio/designs?layoutId='+active).then(async r=>{if(r.ok&&live)setRecent((await r.json()).designs)}).catch(()=>{});return()=>{live=false}},[active,design]);
|
||||
function patch(p:Partial<Parameters>){setParams(old=>({...old,...p}));setStatus('有未保存的修改')}
|
||||
async function save(update=false){if(!layout)return;setBusy(true);try{const body=update&&design?{name,parameters:params,revision:design.revision}:{name,layoutId:layout.id,parameters:params},r=await fetch('/api/studio/designs'+(update&&design?'/'+design.id:''),{method:update?'PATCH':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}),d=await r.json();if(!r.ok)throw Error(d.error);setDesign(d.design);setStatus('已保存效果 · '+d.design.id);window.history.replaceState(null,'',d.design.url)}catch(e){setStatus(e instanceof Error?e.message:'保存失败')}finally{setBusy(false)}}
|
||||
async function download(){if(!layout)return;setBusy(true);try{const r=await fetch('/studio/api/layouts/'+layout.id,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(params)});if(!r.ok)throw Error('渲染失败');const url=URL.createObjectURL(await r.blob()),a=document.createElement('a');a.href=url;a.download=(design?.id||layout.id)+'.svg';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000)}catch(e){setStatus(e instanceof Error?e.message:'导出失败')}finally{setBusy(false)}}
|
||||
if(!layout)return <main className="template-trial"><h1>读取排版效果</h1><p role="status">{status||'读取中…'}</p><a href="/explore/8eb8c76500580f7a4b56ba3c">返回使用教程</a></main>;
|
||||
return <main className="template-trial"><Head><title>{layout.id} · {name} · Studio</title></Head><header className="trial-nav"><a href="/explore/8eb8c76500580f7a4b56ba3c">← PROMPT · 27 套排版教程</a><a href={'/studio/api/layouts/'+layout.id+'?agent_mode=True'}>API 说明</a></header><section className="trial-intro"><div><small>STUDIO / {layout.id.toUpperCase()}</small><h1>{layout.name}</h1><p>每个构件对应原图。修改文字、图位和装饰,保存为独立 ID,可交给 agent 通过 API 继续调整。</p></div></section><div className="trial-workspace"><aside className="trial-controls"><label>排版<select value={active} onChange={e=>{window.location.href='/studio/layouts/'+e.target.value}}>{layouts.map(l=><option key={l.id} value={l.id}>{l.id} · {l.name}</option>)}</select></label><label>效果名称<input value={name} maxLength={120} onChange={e=>setName(e.target.value)}/></label>
|
||||
<details open><summary>文字 T · {layout.elements.filter(s=>s.kind==='text').length} 组</summary>{layout.elements.filter(s=>s.kind==='text').map(s=><label key={s.id}>{s.id} · {s.role}<small>{params.texts[s.id]?'替换文字':s.reading==='unreadable'?'原图小字无法辨认;未替换时保留原图层':'未替换时保留原图字形'}</small><textarea rows={2} maxLength={500} placeholder={s.text||'输入替换文字;留空可移除此文字'} value={params.texts[s.id]?.value??''} onChange={e=>patch({texts:{...params.texts,[s.id]:{...params.texts[s.id],value:e.target.value}}})}/><button onClick={()=>{const texts={...params.texts};delete texts[s.id];patch({texts})}}>恢复原图文字</button></label>)}</details>
|
||||
<details><summary>图位 I · {layout.elements.filter(s=>s.kind==='image').length} 组</summary>{layout.elements.filter(s=>s.kind==='image').map(s=><div key={s.id}><label>{s.id} · {s.role}<input type="color" value={params.images[s.id]?.color||'#83918a'} onChange={e=>patch({images:{...params.images,[s.id]:{...params.images[s.id],color:e.target.value}}})}/></label><label>已上传图片的 asset ID<input placeholder="UUID(可留空)" value={params.images[s.id]?.assetId||''} onChange={e=>{const image={...params.images[s.id]};if(e.target.value)image.assetId=e.target.value;else delete image.assetId;patch({images:{...params.images,[s.id]:image}})}}/></label><button onClick={()=>{const images={...params.images};delete images[s.id];patch({images})}}>恢复原图图位</button></div>)}</details>
|
||||
<details><summary>装饰 D · {layout.elements.filter(s=>s.kind==='decoration').length} 组</summary>{layout.elements.filter(s=>s.kind==='decoration').map(s=><div key={s.id}><label>{s.id} · {s.role}<input type="color" value={params.decorations[s.id]?.color||'#53645b'} onChange={e=>patch({decorations:{...params.decorations,[s.id]:{...params.decorations[s.id],color:e.target.value}}})}/></label><label><input type="checkbox" checked={params.decorations[s.id]?.hidden||false} onChange={e=>patch({decorations:{...params.decorations,[s.id]:{...params.decorations[s.id],hidden:e.target.checked}}})}/>隐藏此装饰</label><button onClick={()=>{const decorations={...params.decorations};delete decorations[s.id];patch({decorations})}}>恢复原装饰</button></div>)}</details>
|
||||
<label>纸色<input type="color" value={params.paper||'#f4f2eb'} onChange={e=>patch({paper:e.target.value})}/></label><label className="trial-switch"><input type="checkbox" checked={guides} onChange={e=>setGuides(e.target.checked)}/>显示 T/I/D 构件边界</label><details onToggle={e=>{if(e.currentTarget.open)setRaw(JSON.stringify(params,null,2))}}><summary>完整参数 JSON</summary><textarea aria-label="完整参数 JSON" rows={12} value={raw} onChange={e=>setRaw(e.target.value)}/><button onClick={()=>{try{setParams(parameters(layout,JSON.parse(raw)));setStatus('参数已应用,尚未保存')}catch(e){setStatus(e instanceof Error?e.message:'JSON 错误')}}}>应用参数</button></details>
|
||||
<div className="trial-actions"><button disabled={busy} className="trial-primary" onClick={()=>void save()}>保存新效果</button>{design&&<button disabled={busy} onClick={()=>void save(true)}>更新当前 ID</button>}<button disabled={busy} onClick={()=>void download()}>下载当前 SVG</button></div><p role="status">{status}</p>{design&&<p><a href={design.url}>打开已保存效果</a> · 修订 {design.revision}</p>}<p className="trial-help">原图来自低分辨率截图,未替换部分保留其像素质量。渲染不会自动保存;保存新效果生成新 ID,更新当前 ID 会检测修订冲突。</p><details><summary>已保存的此排版效果</summary>{recent.length?recent.map(d=><p key={d.id}><a href={d.url}>{d.name}</a> · r{d.revision}</p>):<p>还没有保存的效果</p>}</details></aside>
|
||||
<div><div className="selection-preview"><LayoutCanvas layout={layout} params={params} guides={guides}/></div><details className="trial-reference"><summary>原图与构件对应表</summary><div className="trial-reference-body"><img src={layout.source} alt={layout.name+'原图'}/><div><a href={layout.tutorialUrl}>原图分层文档 ↗</a><table><thead><tr><th>ID</th><th>构件</th><th>坐标</th></tr></thead><tbody>{layout.elements.map(s=><tr key={s.id}><td>{s.id}</td><td>{s.role}</td><td>{s.box.join(', ')}</td></tr>)}</tbody></table></div></div></details></div></div></main>;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
|
||||
import type {NextApiRequest} from 'next';
|
||||
import type {Parameters} from './layout-parameters';
|
||||
/** Only fetch bounded previews from the authenticated Wiki service, never caller URLs. */
|
||||
export async function embedLayoutImages(req:NextApiRequest,params:Parameters){
|
||||
const ids=[...new Set(Object.values(params.images).flatMap(v=>v.assetId?[v.assetId]:[]))],sources:Record<string,string>={};let total=0;
|
||||
const headers:Record<string,string>={};if(req.headers.authorization)headers.authorization=req.headers.authorization;if(req.headers.cookie)headers.cookie=req.headers.cookie;
|
||||
for(const id of ids){const url='http://waterfall-wiki:3000/api/preview?'+new URLSearchParams({source:'/api/media/'+id,width:'960'}),r=await fetch(url,{headers,signal:AbortSignal.timeout(15000),redirect:'error'});if(!r.ok||r.headers.get('content-type')?.split(';')[0]!=='image/webp')throw Error('图片预览不可访问,请检查图片 ID 与登录状态');const chunks:Buffer[]=[];const reader=r.body!.getReader();try{while(true){const {done,value}=await reader.read();if(done)break;total+=value.length;if(total>16*1024*1024)throw Error('图片预览总计超限');chunks.push(Buffer.from(value))}}finally{await reader.cancel()}sources[id]='data:image/webp;base64,'+Buffer.concat(chunks).toString('base64');}
|
||||
return sources;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export type Slot={id:string;kind:string;role:string;box:number[];direction:string;text:string|null;reading:string};
|
||||
export type Layout={id:string;name:string;width:number;height:number;elements:Slot[]};
|
||||
export type TextValue={value:string;color?:string;scale?:number;align?:'start'|'middle'|'end'};
|
||||
export type ImageValue={assetId?:string;color?:string;fit?:'cover'|'contain'};
|
||||
export type DecorationValue={color?:string;hidden?:boolean};
|
||||
export type Parameters={texts:Record<string,TextValue>;images:Record<string,ImageValue>;decorations:Record<string,DecorationValue>;paper?:string};
|
||||
export class LayoutParameterError extends Error{}
|
||||
export const uuid=(v:unknown):v is string=>typeof v==='string'&&/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(v);
|
||||
function fail(message:string):never{throw new LayoutParameterError(message)}
|
||||
function object(v:unknown):Record<string,unknown>{if(!v||typeof v!=='object'||Array.isArray(v))fail('参数必须为 JSON 对象');return v as Record<string,unknown>}
|
||||
function keys(v:Record<string,unknown>,allowed:string[]){if(Object.keys(v).some(k=>!allowed.includes(k)))fail('未知参数或构件 ID')}
|
||||
function color(v:unknown){if(typeof v!=='string'||!/^#[a-f0-9]{6}$/i.test(v))fail('颜色使用 #RRGGBB');return v}
|
||||
export function parameters(layout:Layout,input:unknown):Parameters{
|
||||
const raw=object(input);keys(raw,['texts','images','decorations','paper']);const result:Parameters={texts:{},images:{},decorations:{}};
|
||||
if(raw.paper!==undefined)result.paper=color(raw.paper);
|
||||
for(const [group,kind] of [['texts','text'],['images','image'],['decorations','decoration']] as const){
|
||||
const entries=object(raw[group]??{});keys(entries,layout.elements.filter(e=>e.kind===kind).map(e=>e.id));
|
||||
for(const [id,value] of Object.entries(entries)){const v=object(value);
|
||||
if(group==='texts'){keys(v,['value','color','scale','align']);if(typeof v.value!=='string'||Array.from(v.value).length>500||v.value.split('\n').length>8)fail('文字最多 500 字 / 8 行');const t:TextValue={value:v.value};if(v.color!==undefined)t.color=color(v.color);if(v.scale!==undefined){if(typeof v.scale!=='number'||!Number.isFinite(v.scale)||v.scale<.25||v.scale>2)fail('scale 范围 0.25–2');t.scale=v.scale}if(v.align!==undefined){if(!['start','middle','end'].includes(String(v.align)))fail('无效 align');t.align=v.align as TextValue['align']}result.texts[id]=t;
|
||||
}else if(group==='images'){keys(v,['assetId','color','fit']);const image:ImageValue={};if(v.assetId!==undefined){if(!uuid(v.assetId))fail('assetId 必须为已上传图片 UUID');image.assetId=v.assetId}if(v.color!==undefined)image.color=color(v.color);if(v.fit!==undefined){if(v.fit!=='cover'&&v.fit!=='contain')fail('fit 使用 cover 或 contain');image.fit=v.fit}result.images[id]=image;
|
||||
}else{keys(v,['color','hidden']);const d:DecorationValue={};if(v.color!==undefined)d.color=color(v.color);if(v.hidden!==undefined){if(typeof v.hidden!=='boolean')fail('hidden 必须为布尔值');d.hidden=v.hidden}result.decorations[id]=d}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
import type {NextApiRequest,NextApiResponse} from 'next';
|
||||
import {ideas} from '../../content/ideas';
|
||||
export default function handler(_:NextApiRequest,res:NextApiResponse){res.setHeader('Content-Type','text/markdown; charset=utf-8');res.send('# 创作站\n\n'+ideas.map(i=>`- [${i.title}](/studio/ideas/${i.slug}) · [正文](/studio/api/export/${i.slug}?format=markdown)`).join('\n'));}
|
||||
export default function handler(_:NextApiRequest,res:NextApiResponse){res.setHeader('Content-Type','text/markdown; charset=utf-8');res.send('# 创作站\n\n- [27 套排版目录与 API](/studio/api/layouts?agent_mode=True)\n- [按 ID 保存效果](/api/studio/designs?agent_mode=True)\n\n'+ideas.map(i=>`- [${i.title}](/studio/ideas/${i.slug}) · [正文](/studio/api/export/${i.slug}?format=markdown)`).join('\n'));}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type {NextApiRequest,NextApiResponse} from 'next';
|
||||
import {embedLayoutImages} from '../../../lib/layout-images';
|
||||
import {createElement} from 'react';
|
||||
import {renderToStaticMarkup} from 'react-dom/server';
|
||||
import layouts from '../../../content/templates/layouts.json';
|
||||
import {LayoutCanvas} from '../../../components/LayoutCanvas';
|
||||
import {parameters,LayoutParameterError} from '../../../lib/layout-parameters';
|
||||
export default async function handler(req:NextApiRequest,res:NextApiResponse){
|
||||
res.setHeader('Cache-Control','no-store');
|
||||
if(String(req.query.agent_mode).toLowerCase()==='true'&&['GET','HEAD'].includes(req.method||'')){res.setHeader('Content-Type','text/plain; charset=utf-8');if(req.method==='HEAD')return res.status(200).end();return res.send(`# 排版参数与渲染 API\n\nGET /studio/api/layouts/{id} 返回模板尺寸、T/I/D 构件 ID、坐标、原始文字可辨识状态与默认参数。id 为 ref-01 .. ref-27。\nGET 同地址 ?format=svg 返回原图对应效果。POST JSON 参数返回 SVG,不自动保存。?format=json 返回规范化参数。\n参数整体形状:{texts:{T01:{value:"标题",color:"#26362f",scale:1,align:"start"}},images:{I01:{color:"#788880",assetId:"已上传图片UUID",fit:"cover"}},decorations:{D01:{color:"#53645b",hidden:false}},paper:"#f4f2eb"}。全部可省略;构件 ID 必须属于当前模板。文字 value 可为空(移除),最多500字/8行;scale 0.25–2,align start/middle/end;fit cover/contain。图片先通过 /api/upload assetOnly=true 上传。\n未替换构件保留原始低分辨率图层;替换文字为 SVG 文字,未辨识小字不会编造。图位使用原始形状蒙版。paper 修改原底色层,不覆盖原图已有设计色块。\n文字、图像、装饰分组一一对应源图,不接受任意 URL、SVG 或未知构件。最大请求64KB。\n保存:POST /api/studio/designs {layoutId,name,parameters},返回 design.id/url/revision。再次打开 /studio/designs/{id};PATCH /api/studio/designs/{id} {name,parameters,revision} 更新。\n错误400参数不合法,404模板不存在,405方法不支持。\n目录:/studio/api/layouts?agent_mode=True\n`)}
|
||||
if(!['GET','HEAD','POST'].includes(req.method||'')){res.setHeader('Allow','GET, HEAD, POST');return res.status(405).end()}const layout=layouts.find(l=>l.id===req.query.id);if(!layout)return res.status(404).json({error:'排版不存在'});
|
||||
try{if(req.query.format!==undefined&&!['svg','json'].includes(String(req.query.format)))throw new LayoutParameterError('format 为 svg 或 json');const params=parameters(layout,req.method==='POST'?req.body:{});if(req.method==='HEAD')return res.status(200).end();if(req.query.format==='json')return res.json({layoutId:layout.id,parameters:params});if(req.method==='POST'||req.query.format==='svg'){res.setHeader('Content-Type','image/svg+xml; charset=utf-8');return res.send(renderToStaticMarkup(createElement(LayoutCanvas,{layout,params,imageSources:await embedLayoutImages(req,params)})))}return res.json({id:layout.id,name:layout.name,width:layout.width,height:layout.height,sourceSha256:layout.sourceSha256,tutorialUrl:layout.tutorialUrl,elements:layout.elements.map(({raster,shape,...s})=>s),parameters:params,url:'/studio/layouts/'+layout.id})}catch(e){return res.status(400).json({error:e instanceof Error?e.message:'参数不合法'})}
|
||||
}
|
||||
export const config={api:{bodyParser:{sizeLimit:'64kb'}}};
|
||||
@@ -0,0 +1,3 @@
|
||||
import type {NextApiRequest,NextApiResponse} from 'next';
|
||||
import layouts from '../../../content/templates/layouts.json';
|
||||
export default function handler(req:NextApiRequest,res:NextApiResponse){if(!['GET','HEAD'].includes(req.method||'')){res.setHeader('Allow','GET, HEAD');return res.status(405).end()}const entries=layouts.map(l=>({id:l.id,name:l.name,url:'/studio/layouts/'+l.id,api:'/studio/api/layouts/'+l.id,tutorialUrl:l.tutorialUrl}));res.setHeader('Cache-Control','no-store');if(req.method==='HEAD')return res.status(200).end();if(String(req.query.agent_mode).toLowerCase()==='true'){res.setHeader('Content-Type','text/plain; charset=utf-8');return res.send('# 27 套排版 API\n\n'+entries.map(e=>`${e.id} · ${e.name}\n效果页:${e.url}\n参数和渲染:${e.api}?agent_mode=True`).join('\n\n')+'\n\n效果保存:/api/studio/designs?agent_mode=True\nRouter:/explore/8eb8c76500580f7a4b56ba3c')}return res.json({version:1,layouts:entries,saveApi:'/api/studio/designs'})}
|
||||
@@ -0,0 +1,3 @@
|
||||
import {useRouter} from 'next/router';
|
||||
import {LayoutWorkbench} from '../../components/LayoutWorkbench';
|
||||
export default function DesignPage(){const router=useRouter();return typeof router.query.id==='string'?<LayoutWorkbench designId={router.query.id}/>:null}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type {GetStaticPaths,GetStaticProps} from 'next';
|
||||
import {LayoutWorkbench} from '../../components/LayoutWorkbench';
|
||||
import layouts from '../../content/templates/layouts.json';
|
||||
export default function LayoutPage({id}:{id:string}){return <LayoutWorkbench layoutId={id}/>}
|
||||
export const getStaticPaths:GetStaticPaths=async()=>({paths:layouts.map(l=>({params:{id:l.id}})),fallback:false});
|
||||
export const getStaticProps:GetStaticProps=async({params})=>layouts.some(l=>l.id===params?.id)?{props:{id:params?.id}}:{notFound:true};
|
||||
@@ -26,3 +26,5 @@
|
||||
@page ref17Proof{size:113mm 174mm;margin:0}
|
||||
@media print{.template-trial{padding:0;max-width:none}.trial-nav,.trial-intro,.trial-controls,.trial-reference,.proof-card figcaption{display:none}.trial-workspace,.trial-grid{display:block}.proof-card{page:ref17Proof;break-after:page;width:113mm;height:174mm}.proof-card:last-child{break-after:auto}.proof-select,.proof-active .proof-select{outline:none;box-shadow:none;width:113mm;height:174mm}.proof-select svg{width:113mm!important;height:174mm!important;print-color-adjust:exact}.proof-select svg g[aria-hidden=true]{display:none}}
|
||||
.selection-preview{max-width:540px;margin:auto;box-shadow:0 3px 15px #28362f14}.selection-thumbs{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:30px}.selection-thumbs button{padding:0;border-radius:0;border:1px solid #d8dcd5;background:#fff}.selection-thumbs button[aria-pressed=true]{outline:2px solid #617d65;outline-offset:4px}.selection-thumbs span{display:block;font-size:12px;padding:10px}
|
||||
|
||||
.trial-controls details{border-top:1px solid #dedfd8;padding:12px 0}.trial-controls summary{cursor:pointer;margin-bottom:12px}.trial-controls label small{display:block;font-weight:400;line-height:1.6;color:#687268}.trial-controls textarea{width:100%;box-sizing:border-box}.trial-controls>label input:not([type=checkbox]):not([type=color]){width:100%;box-sizing:border-box}.trial-actions button:disabled{opacity:.5;cursor:wait}
|
||||
|
||||
Reference in New Issue
Block a user