Files
Genarrative/src/components/common/PublishShareModal.tsx
kdletters fe30396544 合并泥点弹窗透明修复
# Conflicts:
#	src/components/common/PublishShareModal.test.tsx
#	src/components/common/PublishShareModal.tsx
#	src/index.test.ts
2026-06-12 15:35:19 +08:00

272 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Check, Copy, Download, Grid3X3, Link2 } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { resolveAssetReadUrl } from '../../services/assetReadUrlService';
import { isWechatMiniProgramWebViewRuntime } from '../../services/authService';
import { copyTextToClipboard } from '../../services/clipboard';
import {
canUseWechatMiniProgramShareGrid,
openWechatMiniProgramShareGridPage,
} from '../../services/wechatMiniProgramShareGrid';
import { useAuthUi } from '../auth/AuthUiContext';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
import { downloadPublishShareCardImage } from './publishShareCardImage';
import {
buildPublishShareCopyUrl,
type PublishShareModalPayload,
} from './publishShareModalModel';
import { PlatformUtilityInfoModal } from './PlatformUtilityInfoModal';
type PublishShareModalProps = {
open: boolean;
payload: PublishShareModalPayload | null;
onClose: () => void;
};
type ActionState = 'idle' | 'success' | 'failed';
function normalizePayloadTitle(payload: PublishShareModalPayload | null) {
return payload?.title.trim() || '我的作品';
}
function resolvePayloadCoverImageSrc(payload: PublishShareModalPayload | null) {
return (
payload?.coverImageSrc?.trim() ||
payload?.fallbackCoverImageSrc?.trim() ||
''
);
}
function resolvePayloadWorkTypeLabel(payload: PublishShareModalPayload | null) {
return payload?.workTypeLabel?.trim() || '互动作品';
}
/**
* 发布完成后的通用分享弹窗。
* 分享事实仍来自公开作品号与 stage弹窗只负责把它表现成可复制、可下载的分享卡。
*/
export function PublishShareModal({
open,
payload,
onClose,
}: PublishShareModalProps) {
const platformTheme = useAuthUi()?.platformTheme ?? 'light';
const [copyState, setCopyState] = useState<ActionState>('idle');
const [downloadState, setDownloadState] = useState<ActionState>('idle');
const [gridState, setGridState] = useState<ActionState>('idle');
const resetTimerRef = useRef<number | null>(null);
const shareCopyUrl = useMemo(
() =>
payload
? buildPublishShareCopyUrl(payload, {
miniProgramRuntime: isWechatMiniProgramWebViewRuntime(),
})
: '',
[payload],
);
const title = normalizePayloadTitle(payload);
const coverImageSrc = resolvePayloadCoverImageSrc(payload);
const workTypeLabel = resolvePayloadWorkTypeLabel(payload);
const showMiniProgramGridButton =
canUseWechatMiniProgramShareGrid() && Boolean(coverImageSrc);
useEffect(
() => () => {
if (resetTimerRef.current !== null) {
window.clearTimeout(resetTimerRef.current);
}
},
[],
);
useEffect(() => {
setCopyState('idle');
setDownloadState('idle');
setGridState('idle');
}, [payload?.publicWorkCode]);
const scheduleStateReset = () => {
if (resetTimerRef.current !== null) {
window.clearTimeout(resetTimerRef.current);
}
resetTimerRef.current = window.setTimeout(() => {
resetTimerRef.current = null;
setCopyState('idle');
setDownloadState('idle');
setGridState('idle');
}, 1400);
};
const copyShareLink = () => {
if (!shareCopyUrl) {
return;
}
void copyTextToClipboard(shareCopyUrl).then((copied) => {
setCopyState(copied ? 'success' : 'failed');
scheduleStateReset();
});
};
const resolveMiniProgramGridCover = async () => {
if (!coverImageSrc) {
return '';
}
return await resolveAssetReadUrl(coverImageSrc, {
expireSeconds: 600,
}).catch(() => coverImageSrc);
};
const downloadShareCard = () => {
if (!payload) {
return;
}
setDownloadState('idle');
void downloadPublishShareCardImage(
{
...payload,
title,
workTypeLabel,
coverImageSrc,
},
coverImageSrc,
)
.then((downloaded) => {
setDownloadState(downloaded ? 'success' : 'failed');
scheduleStateReset();
})
.catch(() => {
setDownloadState('failed');
scheduleStateReset();
});
};
const openMiniProgramGridDownload = () => {
if (!payload || !coverImageSrc) {
return;
}
setGridState('idle');
void resolveMiniProgramGridCover()
.then((resolvedCoverImageSrc) =>
openWechatMiniProgramShareGridPage({
imageUrl: resolvedCoverImageSrc,
title,
publicWorkCode: payload.publicWorkCode,
}),
)
.then((opened) => {
setGridState(opened ? 'success' : 'failed');
scheduleStateReset();
})
.catch(() => {
setGridState('failed');
scheduleStateReset();
});
};
return (
<PlatformUtilityInfoModal
open={open && Boolean(payload)}
title="分享给朋友"
onClose={onClose}
platformTheme={platformTheme}
panelClassName="rounded-[1.75rem]"
footerClassName="border-t-0 px-4 pb-5 pt-0 sm:px-5"
footer={
<div
className={`grid w-full gap-3 ${
showMiniProgramGridButton ? 'grid-cols-1 sm:grid-cols-3' : 'grid-cols-2'
}`}
>
<button
type="button"
onClick={copyShareLink}
disabled={!shareCopyUrl}
className="platform-button platform-button--primary min-h-11 justify-center gap-2 text-sm disabled:cursor-not-allowed disabled:opacity-55"
>
{copyState === 'success' ? (
<Check className="h-4 w-4" />
) : (
<Link2 className="h-4 w-4" />
)}
{copyState === 'success'
? '已复制'
: copyState === 'failed'
? '复制失败'
: '复制链接'}
</button>
<button
type="button"
onClick={downloadShareCard}
disabled={!payload}
className="platform-button platform-button--secondary min-h-11 justify-center gap-2 text-sm disabled:cursor-not-allowed disabled:opacity-55"
>
{downloadState === 'success' ? (
<Check className="h-4 w-4" />
) : (
<Download className="h-4 w-4" />
)}
{downloadState === 'success'
? '已下载'
: downloadState === 'failed'
? '下载失败'
: '下载卡片'}
</button>
{showMiniProgramGridButton ? (
<button
type="button"
onClick={openMiniProgramGridDownload}
className="platform-button platform-button--secondary min-h-11 justify-center gap-2 text-sm"
>
{gridState === 'success' ? (
<Check className="h-4 w-4" />
) : (
<Grid3X3 className="h-4 w-4" />
)}
{gridState === 'success'
? '已打开'
: gridState === 'failed'
? '打开失败'
: '九宫切图'}
</button>
) : null}
</div>
}
>
<section
className="overflow-hidden rounded-lg border border-[var(--platform-subpanel-border)] bg-white/78 shadow-[0_18px_42px_rgba(127,85,57,0.12)]"
aria-label="分享卡片"
>
<div className="relative aspect-square overflow-hidden bg-[linear-gradient(135deg,#f4c38b,#e7b5b7_48%,#9bbfd1)]">
{coverImageSrc ? (
<ResolvedAssetImage
src={coverImageSrc}
alt={title}
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center text-6xl font-black text-white/84">
{Array.from(title)[0] ?? '陶'}
</div>
)}
</div>
<div className="space-y-3 px-4 py-4">
<div className="inline-flex max-w-full items-center rounded-full bg-[var(--platform-neutral-bg)] px-3 py-1 text-xs font-black text-[var(--platform-accent-text)]">
<span className="truncate">{workTypeLabel}</span>
</div>
<h3 className="line-clamp-2 text-lg font-black leading-snug text-[var(--platform-text-strong)]">
{title}
</h3>
<div className="flex min-w-0 items-center gap-2 text-xs font-bold text-[var(--platform-text-muted)]">
<Copy className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">{payload?.publicWorkCode}</span>
</div>
</div>
</section>
</PlatformUtilityInfoModal>
);
}