Files
codex-bot 38ec492ca3
wiki CI and deployment / verify-and-deploy (push) Successful in 16m28s
Publish reference layouts as API-authored text and image notes
2026-10-26 17:20:57 +08:00

25 lines
2.8 KiB
TypeScript

import React from 'react';
import type {CSSProperties} from 'react';
type Part={id:string;kind:string;role:string;box:number[];asset:string;text?:string|null;direction?:string;mask?:string;underlay?:string};
export type Layout={id:string;width:number;height:number;background:string;elements:Part[]};
// The manifest is copied from the post's JSON block. This file is consumer code, not a Wiki route.
export function LayoutPoster({id,layout,text={},images={},assetBase=''}:{id:string;layout:Layout;text?:Record<string,string>;images?:Record<string,string>;assetBase?:string}){
if(layout.id!==id)throw Error('版面 ID 不一致');
for(const[kind,values]of [['text',text],['image',images]] as const)for(const key of Object.keys(values))if(!layout.elements.some(e=>key===id+'/'+e.id&&kind===e.kind))throw Error('构件类型或所属版面不一致:'+key);
const src=(url:string)=>url.startsWith('/')?assetBase+url:url;
return <figure aria-label={id} style={{margin:0,width:'min(100%,672px)',containerType:'inline-size'}}>
<div style={{position:'relative',aspectRatio:`${layout.width}/${layout.height}`,isolation:'isolate',background:'#f4f3ee'}}>
<img src={src(layout.background)} alt="" style={{position:'absolute',inset:0,width:'100%',height:'100%'}}/>
{layout.elements.map(e=>{
const[x,y,w,h]=e.box,key=id+'/'+e.id,position:CSSProperties={position:'absolute',left:x/layout.width*100+'%',top:y/layout.height*100+'%',width:w/layout.width*100+'%',height:h/layout.height*100+'%',zIndex:e.kind==='text'?3:e.kind==='decoration'?2:1};
if(e.kind==='text'&&Object.hasOwn(text,key)){
if(e.direction==='curved')throw Error('弧形文字须使用本设计专用 SVG 路径,不能直接改成直排');
const vertical=e.direction==='vertical'||e.direction==='rotated',lines=text[key].split('\n'),units=Math.max(1,...lines.map(line=>Array.from(line).reduce((n,c)=>n+(/[\u3400-\u9fff]/.test(c)?1:.56),0))),size=vertical?Math.min(w,h/Math.max(1,text[key].length)):Math.min(h/(lines.length*1.1),w/units);
return <React.Fragment key={key}>{e.underlay&&<img src={src(e.underlay)} alt="" style={position}/>}<span style={{...position,fontSize:size/layout.width*100+'cqw',lineHeight:1.1,whiteSpace:'pre-wrap',writingMode:vertical?'vertical-rl':undefined,textOrientation:e.direction==='rotated'?'sideways':undefined,transform:e.direction==='angled'?'rotate(-12deg)':undefined,fontFamily:'serif',color:'#222'}}>{text[key]}</span></React.Fragment>;
}
return <img key={key} data-component={key} data-kind={e.kind} src={e.kind==='image'&&images[key]?images[key]:src(e.asset)} alt={e.kind==='text'?e.text||e.role+'(未辨识原字形)':''} style={{...position,objectFit:'cover',maskImage:e.kind==='image'&&images[key]&&e.mask?`url("${src(e.mask)}")`:undefined,maskSize:'100% 100%'}}/>;
})}
</div>
</figure>;
}