283e6651aa
Co-authored-by: Cursor <cursoragent@cursor.com>
249 lines
8.9 KiB
TypeScript
249 lines
8.9 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import type { ActivityPosterItem, PartnerAssocSummary } from '@dukang/shared-types';
|
|
import { request } from '../lib/api';
|
|
import {
|
|
copyActivityCopy,
|
|
downloadBlob,
|
|
fetchActivityPosterImage,
|
|
fetchAssocQrcodeImage,
|
|
getActivityPosterSelection,
|
|
listActivityPosters,
|
|
saveActivityPosterSelection,
|
|
} from '../lib/activity-posters';
|
|
import { toastError, toastSuccess } from '../lib/toast';
|
|
import { blobToDataUrl, isHttpImageUrl, previewSaveableImage } from '../lib/wechat-save-image';
|
|
import { isWechatEnv } from '../lib/weixin';
|
|
|
|
const NONE_ID = 'none';
|
|
|
|
type Props = {
|
|
initialId?: string | null;
|
|
};
|
|
|
|
export default function ActivityPosterPicker({ initialId }: Props) {
|
|
const [items, setItems] = useState<ActivityPosterItem[]>([]);
|
|
const [qrcodeUrl, setQrcodeUrl] = useState<string | null>(null);
|
|
const [partnerId, setPartnerId] = useState<string>('');
|
|
const [selectedId, setSelectedId] = useState<string>(NONE_ID);
|
|
const [previewUrls, setPreviewUrls] = useState<Record<string, string>>({});
|
|
const [listLoading, setListLoading] = useState(true);
|
|
const [previewLoading, setPreviewLoading] = useState(false);
|
|
const [downloading, setDownloading] = useState(false);
|
|
const blobsRef = useRef(new Map<string, Blob>());
|
|
const urlsRef = useRef<string[]>([]);
|
|
|
|
useEffect(() => () => {
|
|
urlsRef.current.forEach((url) => URL.revokeObjectURL(url));
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
Promise.all([
|
|
listActivityPosters(),
|
|
request<PartnerAssocSummary>('PARTNER_H5', '/partner/assoc', { silent: true }),
|
|
getActivityPosterSelection().catch(() => ({ posterId: null })),
|
|
])
|
|
.then(([list, assoc, selection]) => {
|
|
if (cancelled) return;
|
|
setItems(list);
|
|
setQrcodeUrl(assoc.qrcodeUrl);
|
|
setPartnerId(assoc.partnerId);
|
|
const saved = selection.posterId && list.some((item) => item.id === selection.posterId)
|
|
? selection.posterId
|
|
: null;
|
|
const fromQuery = initialId && list.some((item) => item.id === initialId) ? initialId : null;
|
|
const next = saved || fromQuery || NONE_ID;
|
|
setSelectedId(next);
|
|
if (!saved && fromQuery) {
|
|
void saveActivityPosterSelection(fromQuery).catch((err) => {
|
|
toastError(err instanceof Error ? err.message : '保存失败');
|
|
});
|
|
}
|
|
})
|
|
.catch((e) => toastError(e instanceof Error ? e.message : '加载活动图失败'))
|
|
.finally(() => {
|
|
if (!cancelled) setListLoading(false);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [initialId]);
|
|
|
|
useEffect(() => {
|
|
if (selectedId === NONE_ID || previewUrls[selectedId] || blobsRef.current.has(selectedId)) return;
|
|
let cancelled = false;
|
|
setPreviewLoading(true);
|
|
fetchActivityPosterImage(selectedId)
|
|
.then((blob) => {
|
|
if (cancelled) return;
|
|
blobsRef.current.set(selectedId, blob);
|
|
const url = URL.createObjectURL(blob);
|
|
urlsRef.current.push(url);
|
|
setPreviewUrls((prev) => ({ ...prev, [selectedId]: url }));
|
|
})
|
|
.catch((e) => {
|
|
if (!cancelled) toastError(e instanceof Error ? e.message : '预览失败');
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setPreviewLoading(false);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [selectedId, previewUrls]);
|
|
|
|
const selected = items.find((item) => item.id === selectedId) ?? null;
|
|
const previewUrl = selectedId !== NONE_ID ? previewUrls[selectedId] : undefined;
|
|
|
|
async function downloadSelected() {
|
|
setDownloading(true);
|
|
try {
|
|
if (selectedId === NONE_ID) {
|
|
if (isWechatEnv() && isHttpImageUrl(qrcodeUrl)) {
|
|
await previewSaveableImage(qrcodeUrl);
|
|
return;
|
|
}
|
|
const blob = await fetchAssocQrcodeImage();
|
|
if (isWechatEnv()) {
|
|
setQrcodeUrl(await blobToDataUrl(blob));
|
|
toastSuccess('请长按上方图片保存到相册');
|
|
return;
|
|
}
|
|
downloadBlob(blob, `partner-assoc-${partnerId || 'qr'}.png`);
|
|
toastSuccess('已开始下载');
|
|
return;
|
|
}
|
|
if (!selected) return;
|
|
let blob = blobsRef.current.get(selected.id);
|
|
if (!blob) {
|
|
blob = await fetchActivityPosterImage(selected.id);
|
|
blobsRef.current.set(selected.id, blob);
|
|
}
|
|
if (isWechatEnv()) {
|
|
let url = previewUrls[selected.id];
|
|
if (!url || url.startsWith('blob:')) {
|
|
url = await blobToDataUrl(blob);
|
|
setPreviewUrls((prev) => ({ ...prev, [selected.id]: url }));
|
|
}
|
|
toastSuccess('请长按上方图片保存到相册');
|
|
return;
|
|
}
|
|
downloadBlob(blob, `activity-poster-${selected.id}.png`);
|
|
toastSuccess('已开始下载');
|
|
} catch (e) {
|
|
toastError(e instanceof Error ? e.message : '下载失败');
|
|
} finally {
|
|
setDownloading(false);
|
|
}
|
|
}
|
|
|
|
async function selectPoster(id: string) {
|
|
const prev = selectedId;
|
|
setSelectedId(id);
|
|
try {
|
|
await saveActivityPosterSelection(id === NONE_ID ? null : id);
|
|
} catch (e) {
|
|
setSelectedId(prev);
|
|
toastError(e instanceof Error ? e.message : '保存失败');
|
|
}
|
|
}
|
|
|
|
function renderRadioRow(id: string, label: string) {
|
|
return (
|
|
<label className="partner-activity-radio-row">
|
|
<span className="headline-md">{label}</span>
|
|
<input
|
|
type="radio"
|
|
name="activity-poster"
|
|
checked={selectedId === id}
|
|
onChange={() => void selectPoster(id)}
|
|
/>
|
|
</label>
|
|
);
|
|
}
|
|
|
|
if (listLoading) {
|
|
return <div className="empty">加载活动图…</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="partner-activity-picker">
|
|
<section className="partner-bill-card" style={{ overflow: 'hidden' }}>
|
|
{renderRadioRow(NONE_ID, '无')}
|
|
{selectedId === NONE_ID && (
|
|
<div className="partner-activity-panel">
|
|
{qrcodeUrl ? (
|
|
<div className="partner-activity-preview partner-activity-preview--qr">
|
|
<img className="partner-longpress-img" src={qrcodeUrl} alt="关联码" />
|
|
</div>
|
|
) : (
|
|
<p className="body-md text-muted" style={{ marginBottom: 12 }}>关联码尚未生成</p>
|
|
)}
|
|
<button
|
|
type="button"
|
|
className="partner-btn-primary"
|
|
disabled={downloading || !qrcodeUrl}
|
|
onClick={() => void downloadSelected()}
|
|
>
|
|
{downloading ? '下载中…' : '下载二维码'}
|
|
</button>
|
|
{isWechatEnv() && (
|
|
<p className="label-md text-muted" style={{ marginTop: 12 }}>
|
|
微信内请长按上方图片保存
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
{items.map((item) => (
|
|
<section key={item.id} className="partner-bill-card" style={{ marginTop: 12, overflow: 'hidden' }}>
|
|
{renderRadioRow(item.id, item.title)}
|
|
{selectedId === item.id && (
|
|
<div className="partner-activity-panel">
|
|
<div className="partner-activity-preview">
|
|
<img className="partner-longpress-img" src={previewUrl || item.imageUrl} alt={item.title} />
|
|
{previewLoading && !previewUrl && (
|
|
<div className="partner-activity-preview-mask">正在贴入二维码…</div>
|
|
)}
|
|
</div>
|
|
{item.copyText ? (
|
|
<p className="body-md" style={{ whiteSpace: 'pre-wrap', margin: '12px 0' }}>{item.copyText}</p>
|
|
) : (
|
|
<p className="body-md text-muted" style={{ margin: '12px 0' }}>暂无推广文案</p>
|
|
)}
|
|
<div style={{ display: 'flex', gap: 12 }}>
|
|
<button
|
|
type="button"
|
|
className="partner-btn-outline"
|
|
onClick={() => {
|
|
void copyActivityCopy(item.copyText)
|
|
.then(() => toastSuccess('文案已复制'))
|
|
.catch((e) => toastError(e instanceof Error ? e.message : '复制失败'));
|
|
}}
|
|
>
|
|
复制文案
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="partner-btn-primary"
|
|
style={{ flex: 1, width: 'auto', padding: 0, height: 48 }}
|
|
disabled={downloading || previewLoading}
|
|
onClick={() => void downloadSelected()}
|
|
>
|
|
{downloading || previewLoading ? '生成中…' : '下载活动图'}
|
|
</button>
|
|
</div>
|
|
{previewUrl && isWechatEnv() && (
|
|
<p className="label-md text-muted" style={{ marginTop: 12 }}>
|
|
微信内请长按上方图片保存
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</section>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|