# Conflicts: # src/components/common/PublishShareModal.test.tsx # src/components/common/PublishShareModal.tsx # src/index.test.ts
272 lines
8.5 KiB
TypeScript
272 lines
8.5 KiB
TypeScript
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>
|
||
);
|
||
}
|