feat(round-14): GDPR脱敏被遗忘权/展会海报排版/全球雷达热力散点/DeepLink深度路由 (95套件1053测试全绿)
这个提交包含在:
@@ -0,0 +1,194 @@
|
||||
import { Provide } from '@midwayjs/core';
|
||||
import { BaseService } from '@cool-midway/core';
|
||||
|
||||
export type PosterAspectRatio = 'story_9_16' | 'feed_4_5';
|
||||
|
||||
export interface PosterCanvasDimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
aspectRatio: '9:16' | '4:5';
|
||||
}
|
||||
|
||||
export interface PosterLayoutOptions {
|
||||
aspectRatio?: PosterAspectRatio;
|
||||
cardName: string;
|
||||
title?: string;
|
||||
company?: string;
|
||||
eventName?: string;
|
||||
boothNumber?: string;
|
||||
qrCodeSize?: number;
|
||||
}
|
||||
|
||||
export interface PosterElementBox {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
fontSize?: number;
|
||||
isMultiLine?: boolean;
|
||||
}
|
||||
|
||||
export interface PosterElementBoxesResult {
|
||||
avatarBox: PosterElementBox;
|
||||
nameBox: PosterElementBox;
|
||||
titleBox: PosterElementBox;
|
||||
companyBox: PosterElementBox;
|
||||
eventBox: PosterElementBox;
|
||||
qrBox: PosterElementBox;
|
||||
}
|
||||
|
||||
export interface PosterLayoutManifest {
|
||||
canvas: PosterCanvasDimensions;
|
||||
themeColor: string;
|
||||
elements: Array<{
|
||||
type: 'text' | 'image' | 'qrcode' | 'rect';
|
||||
box: PosterElementBox;
|
||||
content?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算海报画布尺寸
|
||||
*/
|
||||
export function calculatePosterCanvasDimensions(
|
||||
ratio: PosterAspectRatio = 'story_9_16'
|
||||
): PosterCanvasDimensions {
|
||||
if (ratio === 'feed_4_5') {
|
||||
return {
|
||||
width: 1080,
|
||||
height: 1350,
|
||||
aspectRatio: '4:5',
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: 1080,
|
||||
height: 1920,
|
||||
aspectRatio: '9:16',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算多元素布局坐标盒
|
||||
*/
|
||||
export function calculatePosterElementBoxes(
|
||||
options: PosterLayoutOptions
|
||||
): PosterElementBoxesResult {
|
||||
const canvas = calculatePosterCanvasDimensions(options.aspectRatio);
|
||||
const W = canvas.width;
|
||||
const H = canvas.height;
|
||||
const qrSize = options.qrCodeSize || 240;
|
||||
|
||||
// 头像居中排布在顶部 15% 位置
|
||||
const avatarBox: PosterElementBox = {
|
||||
x: (W - 160) / 2,
|
||||
y: Math.round(H * 0.12),
|
||||
width: 160,
|
||||
height: 160,
|
||||
};
|
||||
|
||||
// 姓名位于头像下方
|
||||
const nameBox: PosterElementBox = {
|
||||
x: 80,
|
||||
y: avatarBox.y + avatarBox.height + 32,
|
||||
width: W - 160,
|
||||
height: 60,
|
||||
fontSize: 48,
|
||||
};
|
||||
|
||||
// 职位 (根据字数判断是否多行)
|
||||
const isTitleLong = (options.title || '').length > 35;
|
||||
const titleBox: PosterElementBox = {
|
||||
x: 80,
|
||||
y: nameBox.y + nameBox.height + 16,
|
||||
width: W - 160,
|
||||
height: isTitleLong ? 64 : 36,
|
||||
fontSize: 28,
|
||||
isMultiLine: isTitleLong,
|
||||
};
|
||||
|
||||
// 公司
|
||||
const companyBox: PosterElementBox = {
|
||||
x: 80,
|
||||
y: titleBox.y + titleBox.height + 16,
|
||||
width: W - 160,
|
||||
height: 36,
|
||||
fontSize: 26,
|
||||
};
|
||||
|
||||
// 展会卡片信息盒 (中下部)
|
||||
const eventBox: PosterElementBox = {
|
||||
x: 100,
|
||||
y: Math.round(H * 0.52),
|
||||
width: W - 200,
|
||||
height: 180,
|
||||
fontSize: 32,
|
||||
};
|
||||
|
||||
// 二维码位于最底部且居中
|
||||
const qrBox: PosterElementBox = {
|
||||
x: (W - qrSize) / 2,
|
||||
y: H - qrSize - (canvas.aspectRatio === '9:16' ? 180 : 100),
|
||||
width: qrSize,
|
||||
height: qrSize,
|
||||
};
|
||||
|
||||
return {
|
||||
avatarBox,
|
||||
nameBox,
|
||||
titleBox,
|
||||
companyBox,
|
||||
eventBox,
|
||||
qrBox,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 展会海报轻量级排版引擎
|
||||
*/
|
||||
@Provide()
|
||||
export class CardPosterLayoutEngine extends BaseService {
|
||||
/**
|
||||
* 生成排版元数据 Manifest
|
||||
*/
|
||||
generateLayoutManifest(options: PosterLayoutOptions): PosterLayoutManifest {
|
||||
const canvas = calculatePosterCanvasDimensions(options.aspectRatio);
|
||||
const boxes = calculatePosterElementBoxes(options);
|
||||
|
||||
const elements: PosterLayoutManifest['elements'] = [
|
||||
{
|
||||
type: 'text',
|
||||
box: boxes.nameBox,
|
||||
content: options.cardName,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
box: boxes.titleBox,
|
||||
content: options.title,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
box: boxes.companyBox,
|
||||
content: options.company,
|
||||
},
|
||||
{
|
||||
type: 'qrcode',
|
||||
box: boxes.qrBox,
|
||||
content: 'https://omnicard.ai/p/card',
|
||||
},
|
||||
];
|
||||
|
||||
if (options.eventName) {
|
||||
elements.push({
|
||||
type: 'rect',
|
||||
box: boxes.eventBox,
|
||||
content: `${options.eventName} | ${options.boothNumber || ''}`,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
canvas,
|
||||
themeColor: '#0B5CFF', // 钴蓝主色
|
||||
elements,
|
||||
};
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户