3166467518
HQ 上传底图与码栏;合伙人单选写入 partner_account.activity_poster_id,用户管理下次登录仍显示同一张图。 Co-authored-by: Cursor <cursoragent@cursor.com>
111 lines
3.6 KiB
TypeScript
111 lines
3.6 KiB
TypeScript
import { useCallback, useRef } from 'react';
|
||
import { DEFAULT_ACTIVITY_POSTER_QR_SLOT, type ActivityPosterQrSlot } from '@dukang/shared-types';
|
||
|
||
type Props = {
|
||
imageUrl?: string;
|
||
value?: ActivityPosterQrSlot;
|
||
onChange?: (slot: ActivityPosterQrSlot) => void;
|
||
};
|
||
|
||
export default function ActivityPosterQrSlotEditor({ imageUrl, value, onChange }: Props) {
|
||
const boxRef = useRef<HTMLDivElement>(null);
|
||
const dragRef = useRef<{
|
||
mode: 'move' | 'resize';
|
||
startX: number;
|
||
startY: number;
|
||
start: ActivityPosterQrSlot;
|
||
} | null>(null);
|
||
const slot = value ?? DEFAULT_ACTIVITY_POSTER_QR_SLOT;
|
||
|
||
const apply = useCallback((next: ActivityPosterQrSlot) => {
|
||
const size = Math.min(50, Math.max(5, next.qrSizePct));
|
||
const box = boxRef.current;
|
||
const ratio = box && box.clientHeight > 0 ? box.clientWidth / box.clientHeight : 1;
|
||
const heightPct = size * ratio;
|
||
const x = Math.min(100 - size, Math.max(0, next.qrXPct));
|
||
const y = Math.min(Math.max(0, 100 - heightPct), Math.max(0, next.qrYPct));
|
||
onChange?.({
|
||
qrXPct: Number(x.toFixed(2)),
|
||
qrYPct: Number(y.toFixed(2)),
|
||
qrSizePct: Number(size.toFixed(2)),
|
||
});
|
||
}, [onChange]);
|
||
|
||
function onPointerDown(mode: 'move' | 'resize', e: React.PointerEvent) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||
dragRef.current = { mode, startX: e.clientX, startY: e.clientY, start: { ...slot } };
|
||
}
|
||
|
||
function onPointerMove(e: React.PointerEvent) {
|
||
const drag = dragRef.current;
|
||
const box = boxRef.current;
|
||
if (!drag || !box) return;
|
||
const dx = ((e.clientX - drag.startX) / box.clientWidth) * 100;
|
||
const dy = ((e.clientY - drag.startY) / box.clientHeight) * 100;
|
||
if (drag.mode === 'move') {
|
||
apply({
|
||
...drag.start,
|
||
qrXPct: drag.start.qrXPct + dx,
|
||
qrYPct: drag.start.qrYPct + dy,
|
||
});
|
||
} else {
|
||
apply({ ...drag.start, qrSizePct: drag.start.qrSizePct + dx });
|
||
}
|
||
}
|
||
|
||
function endDrag() {
|
||
dragRef.current = null;
|
||
}
|
||
|
||
if (!imageUrl) {
|
||
return <div style={{ color: 'rgba(0,0,0,0.45)' }}>请先上传活动图,再拖拽定位方形码栏</div>;
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<div
|
||
ref={boxRef}
|
||
style={{ position: 'relative', width: '100%', maxWidth: 420, userSelect: 'none', touchAction: 'none' }}
|
||
onPointerMove={onPointerMove}
|
||
onPointerUp={endDrag}
|
||
onPointerCancel={endDrag}
|
||
>
|
||
<img src={imageUrl} alt="活动图预览" style={{ width: '100%', display: 'block' }} draggable={false} />
|
||
<div
|
||
style={{
|
||
position: 'absolute',
|
||
left: `${slot.qrXPct}%`,
|
||
top: `${slot.qrYPct}%`,
|
||
width: `${slot.qrSizePct}%`,
|
||
aspectRatio: '1',
|
||
border: '2px dashed #1677ff',
|
||
background: 'rgba(22,119,255,0.14)',
|
||
cursor: 'move',
|
||
boxSizing: 'border-box',
|
||
}}
|
||
onPointerDown={(e) => onPointerDown('move', e)}
|
||
>
|
||
<div
|
||
style={{
|
||
position: 'absolute',
|
||
right: -6,
|
||
bottom: -6,
|
||
width: 14,
|
||
height: 14,
|
||
background: '#1677ff',
|
||
borderRadius: 2,
|
||
cursor: 'nwse-resize',
|
||
}}
|
||
onPointerDown={(e) => onPointerDown('resize', e)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div style={{ marginTop: 8, color: 'rgba(0,0,0,0.45)', fontSize: 12 }}>
|
||
拖拽移动码栏,拉右下角调整大小(边长相对图宽 {slot.qrSizePct}%)
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|