feat(web-water): 水印设置页面开发

This commit is contained in:
R524809
2026-09-04 15:57:16 +08:00
parent 009082d0f4
commit e277754588
15 changed files with 1514 additions and 80 deletions
@@ -0,0 +1,97 @@
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;