98 lines
4.1 KiB
TypeScript
98 lines
4.1 KiB
TypeScript
import { useState } from 'react';
|
|
import type { CSSProperties } from 'react';
|
|
import { Typography } from 'antd';
|
|
import type { WatermarkPosition } from '@atlas/contracts';
|
|
import type { WatermarkTemplateConfig } from '@atlas/contracts';
|
|
|
|
/** 九宫格位置选项,顺序与编辑器 3x3 网格一致 */
|
|
export const WATERMARK_POSITION_OPTIONS: Array<{
|
|
value: WatermarkPosition;
|
|
label: string;
|
|
row: 0 | 1 | 2;
|
|
col: 0 | 1 | 2;
|
|
}> = [
|
|
{ value: 'top_left', label: '左上角', row: 0, col: 0 },
|
|
{ value: 'top_center', label: '上居中', row: 0, col: 1 },
|
|
{ value: 'top_right', label: '右上角', row: 0, col: 2 },
|
|
{ value: 'middle_left', label: '左居中', row: 1, col: 0 },
|
|
{ value: 'center', label: '正居中', row: 1, col: 1 },
|
|
{ value: 'middle_right', label: '右居中', row: 1, col: 2 },
|
|
{ value: 'bottom_left', label: '左下角', row: 2, col: 0 },
|
|
{ value: 'bottom_center', label: '下居中', row: 2, col: 1 },
|
|
{ value: 'bottom_right', label: '右下角', row: 2, col: 2 },
|
|
];
|
|
|
|
export const watermarkPositionLabel = (position: string): string =>
|
|
WATERMARK_POSITION_OPTIONS.find((option) => option.value === position)?.label ?? position;
|
|
|
|
const POSITION_FLEX: Record<
|
|
WatermarkPosition,
|
|
Pick<CSSProperties, 'justifyContent' | 'alignItems'>
|
|
> = {
|
|
top_left: { justifyContent: 'flex-start', alignItems: 'flex-start' },
|
|
top_center: { justifyContent: 'center', alignItems: 'flex-start' },
|
|
top_right: { justifyContent: 'flex-end', alignItems: 'flex-start' },
|
|
middle_left: { justifyContent: 'flex-start', alignItems: 'center' },
|
|
center: { justifyContent: 'center', alignItems: 'center' },
|
|
middle_right: { justifyContent: 'flex-end', alignItems: 'center' },
|
|
bottom_left: { justifyContent: 'flex-start', alignItems: 'flex-end' },
|
|
bottom_center: { justifyContent: 'center', alignItems: 'flex-end' },
|
|
bottom_right: { justifyContent: 'flex-end', alignItems: 'flex-end' },
|
|
};
|
|
|
|
interface WatermarkPreviewProps {
|
|
/** 允许传入部分字段:编辑弹窗按当前表单值实时渲染 */
|
|
config: Partial<WatermarkTemplateConfig>;
|
|
width?: number;
|
|
height?: number;
|
|
}
|
|
|
|
/**
|
|
* 按模板 config 在内置演示底图上实时渲染水印效果(docs/api/watermarks-and-assets.md §3.1),
|
|
* 不调用服务端渲染;图片使用响应中的短时 preview_url,过期时提示重新读取。
|
|
*/
|
|
const WatermarkPreview = ({ config, width = 320, height = 420 }: WatermarkPreviewProps) => {
|
|
const [imageBroken, setImageBroken] = useState(false);
|
|
const position = config.position ?? 'bottom_right';
|
|
const flex = POSITION_FLEX[position] ?? POSITION_FLEX.bottom_right;
|
|
const previewUrl = config.image_asset?.preview_url;
|
|
|
|
return (
|
|
<div
|
|
className="watermark-preview"
|
|
style={{ width, height, ...flex, opacity: Number(config.opacity ?? '1') }}
|
|
>
|
|
{config.image_asset ? (
|
|
imageBroken || !previewUrl ? (
|
|
<Typography.Text type="secondary">图片预览已过期或不可用</Typography.Text>
|
|
) : (
|
|
<img
|
|
src={previewUrl}
|
|
alt="水印图片"
|
|
style={{
|
|
// scale_ratio 以底图短边为基准的视觉近似:1.0 ≈ 底图宽度的 60%
|
|
width: Math.round(Number(config.scale_ratio ?? '1') * width * 0.6),
|
|
maxWidth: '80%',
|
|
}}
|
|
onError={() => setImageBroken(true)}
|
|
/>
|
|
)
|
|
) : (
|
|
<span
|
|
style={{
|
|
fontSize: config.font_size_px ?? 24,
|
|
fontWeight: 600,
|
|
color: '#111827',
|
|
whiteSpace: 'nowrap',
|
|
textShadow: '0 0 2px rgba(255,255,255,0.8)',
|
|
}}
|
|
>
|
|
{config.text_content || '文字水印'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default WatermarkPreview;
|