7.6 KiB
27 套排版 · Studio 参数 API 与保存教程
把这篇 post 交给 agent,即可选排版、预览、调整参数并保存可再次打开的效果。每套 ref-01…ref-27 都有独立页面,与原图的 T(文字)、I(图位)、D(装饰)构件对应。
先看效果,再通过 API 调整
{{LAYOUTS}}
以上页面都可以修改文字、图位颜色或图片、装饰颜色和显隐、纸色,也能输入完整参数 JSON。打开“显示 T/I/D 构件边界”核对位置;“原图与构件对应表”可查看原图及坐标。页面下方可找到已保存的此排版效果。
Agent 调用顺序
- 读取 全部 27 套排版目录与接口,选择 ID。
- GET
/studio/api/layouts/ref-01获取当前模板的真实构件 ID、坐标、方向与可辨识文字。也可以先读 渲染接口说明。不要把其他模板的 T/I/D ID 混用。 - POST 同地址传入参数,得到 SVG。渲染不会自动保存;迭代参数直到效果合适。
- POST
/api/studio/designs保存新效果,返回design.id、design.url与design.revision。通过design.url可直接看效果。 - GET
/api/studio/designs/{id}读取上次参数;PATCH 同地址携带 revision 更新同一 ID。要保留两个不同版本,使用 POST 保存两个新 ID。
所有 API 与 Wiki 共用现有登录 Cookie 或 Bearer JWT。浏览器修改请求必须同源。Studio 本身不配置数据库连接,保存由 Wiki 写入同一个 PostgreSQL。
参数示例:ref-01
{
"texts": {
"T01": {"value": "STUDY", "color": "#26362f", "scale": 1, "align": "start"},
"T02": {"value": "知识合集\n从记录到创作"}
},
"images": {
"I01": {"color": "#7f9188"},
"I02": {"color": "#b7c4bb"}
},
"decorations": {"D01": {"color": "#53645b", "hidden": false}},
"paper": "#f4f2eb"
}
文字最多 500 字、8 行,scale 范围 0.25–2,align 为 start/middle/end。文字 value 为空则移除这一组;删除其参数键则恢复原图字形。图片可以只改色,也可先通过 /api/upload 的 assetOnly=true 上传,再传入 assetId UUID 与 fit: cover/contain;不接受任意图片网址。装饰支持颜色与 hidden。参数在画板内裁切,未知字段、错误构件 ID、非法颜色和超限输入返回 400。请求最大 64KB。
原始参考来自低分辨率拼图。默认效果保留原图;未替换的构件继续使用原始分层,图位使用原始形状蒙版。可替换文字生成 SVG 文字;无法辨识的小字标为 unreadable,保留原图层而不编造文字。纸色作用于原有底层,不替代图位色块。页面预览通过同站媒体读取图片;API 导出时会使用当前登录读取 Wiki 有尺寸上限的 WebP 预览,并内嵌到 SVG,使下载结果不依赖外部图片链接。原始大图仍保留在 Wiki,导出使用最长 960 像素的预览。
HTML / JavaScript:渲染与保存
<div id="preview"></div>
<script type="module">
const layoutId = 'ref-01';
const parameters = {texts:{T01:{value:'STUDY'}},images:{I01:{color:'#7f9188'}}};
const response = await fetch(`/studio/api/layouts/${layoutId}`, {
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(parameters)
});
if (!response.ok) throw new Error(await response.text());
const previewUrl = URL.createObjectURL(await response.blob());
const image = new Image(); image.src = previewUrl;
document.querySelector('#preview').replaceChildren(image);
// 预览完成后可 URL.revokeObjectURL(previewUrl)。
const saved = await fetch('/api/studio/designs', {
method:'POST', headers:{'Content-Type':'application/json'},
body:JSON.stringify({layoutId,name:'知识合集第一版',parameters})
});
if (!saved.ok) throw new Error(await saved.text());
const {design} = await saved.json();
console.log(design.id, design.url, design.revision);
</script>
React:参数变化重新预览
import {useEffect,useState} from 'react';
export function Preview(){
const [title,setTitle]=useState('STUDY'),[src,setSrc]=useState(''),[error,setError]=useState('');
useEffect(()=>{
const controller=new AbortController();let url='';
fetch('/studio/api/layouts/ref-01',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({texts:{T01:{value:title}}}),signal:controller.signal})
.then(async r=>{if(!r.ok)throw Error(await r.text());return r.blob()})
.then(blob=>{if(!controller.signal.aborted){url=URL.createObjectURL(blob);setSrc(url);setError('')}})
.catch(e=>{if(!controller.signal.aborted)setError(e.message)});
return()=>{controller.abort();if(url)URL.revokeObjectURL(url)};
},[title]);
return <><input value={title} onChange={e=>setTitle(e.target.value)}/>{error&&<p>{error}</p>}{src&&<img src={src} alt="当前排版效果"/>}</>;
}
Next.js:客户端编辑组件
// app/layout-playground/page.tsx
'use client';
import {useState} from 'react';
export default function Playground(){
const [title,setTitle]=useState('STUDY'),[link,setLink]=useState(''),[status,setStatus]=useState('');
async function save(){
const response=await fetch('/api/studio/designs',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({layoutId:'ref-01',name:title||'排版效果',parameters:{texts:{T01:{value:title}}}})});
const data=await response.json();if(!response.ok){setStatus(data.error);return}setLink(data.design.url);setStatus('已保存');
}
return <><input value={title} onChange={e=>setTitle(e.target.value)}/><button onClick={save}>保存新效果</button><p>{status}</p>{link&&<a href={link}>打开效果</a>}</>;
}
这些前端示例应放在已登录的 PROMPT 同站环境。服务端 agent 使用既有 Bearer JWT,只在进程内读取凭据,勿把令牌写入 post、源码、日志或 URL。接口错误不可当作渲染结果处理。
按 ID 读取、修改、保留分支
const headers = {'Content-Type':'application/json'};
const read = await fetch(`/api/studio/designs/${id}`);
if (!read.ok) throw Error(await read.text());
const {design} = await read.json();
const parameters = {...design.parameters,texts:{...design.parameters.texts,T01:{value:'第二版'}}};
const update = await fetch(`/api/studio/designs/${id}`, {
method:'PATCH',headers,
body:JSON.stringify({name:'知识合集第二版',parameters,revision:design.revision})
});
if (update.status===409) throw Error('有人已经修改,请重新读取后合并');
if (!update.ok) throw Error(await update.text());
// 想保留旧版时,改用 POST /api/studio/designs,传 {layoutId:design.layoutId,name,parameters}。
PATCH 的 parameters 整体替换,因此先 GET 并合并未修改的构件;layoutId 不可改变。错误:400 参数不合法、401 未登录、404 不存在、409 修订冲突、413 超限。效果保存接口说明。列表可用 layoutId 筛选、limit=1..50;返回 nextCursor 后用 before=<nextCursor> 继续。
验收标准
- 27 个 ID 都有独立效果页、参数说明与渲染 API,来源链接与 T/I/D 构件一一对应。
- 默认效果展示原图;替换一组文字、一个图位或装饰后,只改变指定构件。
- API 渲染与界面使用相同模板、验证和渲染器;导出的 SVG 不包含辅助边界。
- 保存返回独立 UUID,刷新或再次访问效果 URL 能恢复参数;更新后 revision 增加,过期修订返回 409。
- 图片 assetId 必须是 Wiki 中已上传的图片;不接受未知构件或外站 URL。
- Wiki 与 Studio 共用现有登录和数据库,Studio 无独立持久化或数据库凭据。