| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656 |
- import { useEffect, useRef, useState, type TouchEvent } from 'react';
- import { useNavigate } from 'react-router-dom';
- import { message } from 'antd';
- import {
- setStorage,
- STORAGE_KEYS,
- getUserId,
- getVisitedRoutes,
- setCurrentBackgroundImageId,
- setCurrentBackgroundImageUrl,
- syncBackgroundImageToStorage,
- setHasSharedAvatar,
- } from '@/utils/storage';
- import { updateUserUIState, claimShareReward } from '@/utils/api';
- import { px2vmin, px2vw } from '@/utils/viewport';
- import { getOssUrl } from '@/utils/oss';
- const backButton = getOssUrl('backbutn.png');
- const shareProgressBar = getOssUrl('share-progress-bar.png');
- const avatarAnimaZone = getOssUrl('avatar-anima-zone-copy.png');
- const shareBtn = getOssUrl('share-btn.png');
- const closeBtn = getOssUrl('close-btn.png');
- // const wechatIcon = getOssUrl('wechat.png'); // 已注释,未使用
- // const qqIcon = getOssUrl('QQ.png'); // 已注释,未使用
- // const saveIcon = getOssUrl('save.png'); // 已注释,未使用
- const saveBtnImage = 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/share-done-claim-gift.png';
- const claimGiftBtnImage = 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/claim-the-gift.png';
- const chevronLeftImage = 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/Chevron-L.png';
- const chevronRightImage = 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/Chevron-R.png';
- import { shareImages as fallbackShareImages } from '@/data/shareImages';
- import { getShareImages, type ShareImageItem } from '@/utils/api';
- export const FirstLoginRewardPage = () => {
- const navigate = useNavigate();
- const [showShareModal, setShowShareModal] = useState(false);
- const [shareImages, setShareImages] = useState<ShareImageItem[]>([]);
- const [activeShareIndex, setActiveShareIndex] = useState(0);
- const touchStartXRef = useRef<number | null>(null);
- useEffect(() => {
- // 记录用户当前所在页面
- setStorage(STORAGE_KEYS.CURRENT_PAGE, 'first-login-reward');
- const visited = getVisitedRoutes();
- const userId = getUserId();
- if (userId) {
- updateUserUIState(userId, {
- current_page: 'first-login-reward',
- visited_routes: visited,
- }).catch(() => {
- // 同步失败不影响前端流程
- });
- }
- }, []);
- useEffect(() => {
- const fetchShareImages = async () => {
- try {
- const res = await getShareImages();
- if (res.success && res.data?.length) {
- setShareImages(res.data);
- setActiveShareIndex(0);
- return;
- }
- } catch (error) {
- // fallback below
- }
- setShareImages(fallbackShareImages);
- setActiveShareIndex(0);
- };
- fetchShareImages();
- }, []);
- // 预加载分享图片
- useEffect(() => {
- if (shareImages.length === 0) {
- return;
- }
- const preloadImages = () => {
- shareImages.forEach((item) => {
- if (item.url) {
- const img = new Image();
- img.src = item.url;
- }
- });
- };
- preloadImages();
- }, [shareImages]);
- const handleBack = () => {
- navigate('/chatbot');
- };
- const handleShareClick = () => {
- if (!shareImages.length) {
- return;
- }
- setShowShareModal(true);
- };
- const handleShareClose = () => {
- setShowShareModal(false);
- setActiveShareIndex(0);
- };
- // 领取分享奖励
- const handleSaveImage = async () => {
- try {
- const userId = getUserId();
- if (!userId) {
- message.error('用户未登录,请先登录');
- return;
- }
- message.loading('正在领取分享奖励...', 0);
- // 调用分享奖励接口
- const result = await claimShareReward(userId);
- message.destroy();
- if (result.success && result.data) {
- // 显式更新 localStorage 中的背景图片相关变量
- const backgroundImageId = result.data.background_image_id || 'happynewyear';
- const backgroundImageMetadata = result.data.background_image_metadata || { opacity: 0.5 };
-
- // 同步背景图片到 localStorage(包括 ID、URL 和 metadata)
- syncBackgroundImageToStorage(backgroundImageId, backgroundImageMetadata);
-
- // 确保背景图片 ID 和 URL 都已更新
- setCurrentBackgroundImageId(backgroundImageId);
-
- // 获取背景图片 URL(从映射中获取)
- const { getBackgroundImageUrlById } = await import('@/utils/storage');
- const backgroundImageUrl = getBackgroundImageUrlById(backgroundImageId);
- if (backgroundImageUrl) {
- setCurrentBackgroundImageUrl(backgroundImageUrl);
- }
-
- // 更新分享头像状态:更新 localStorage
- // 注意:数据库中的 has_shared_avatar 已在 claimShareReward 接口中更新
- setHasSharedAvatar(true);
-
- // 额外同步一次确保数据一致性(claimShareReward 接口已更新,这里作为双重保障)
- try {
- await updateUserUIState(userId, {
- has_shared_avatar: true,
- });
- console.log('✅ 分享头像状态已同步到数据库');
- } catch (error) {
- console.error('⚠️ 同步分享头像状态到数据库失败(claimShareReward 接口已更新,不影响主流程):', error);
- // 即使同步失败,claimShareReward 接口已经更新了数据库,不影响主流程
- }
-
- message.success('分享奖励已领取!新年背景已解锁');
- // 跳转到分享成功页面
- setTimeout(() => {
- navigate('/share-success');
- }, 1000);
- } else {
- message.error(result.message || '领取分享奖励失败,请重试');
- }
- } catch (error) {
- console.error('领取分享奖励失败:', error);
- message.destroy();
- message.error('领取分享奖励失败,请重试');
- }
- };
- const handleTouchStart = (event: TouchEvent<HTMLDivElement>) => {
- touchStartXRef.current = event.touches[0]?.clientX ?? null;
- };
- const handleTouchEnd = (event: TouchEvent<HTMLDivElement>) => {
- const startX = touchStartXRef.current;
- if (startX === null) {
- return;
- }
- if (!shareImages.length) {
- return;
- }
- const endX = event.changedTouches[0]?.clientX ?? startX;
- const deltaX = endX - startX;
- const threshold = 30;
- if (deltaX > threshold && activeShareIndex > 0) {
- setActiveShareIndex((prev) => Math.max(prev - 1, 0));
- } else if (deltaX < -threshold && activeShareIndex < shareImages.length - 1) {
- setActiveShareIndex((prev) => Math.min(prev + 1, shareImages.length - 1));
- }
- touchStartXRef.current = null;
- };
- const handlePreviousImage = () => {
- if (activeShareIndex > 0) {
- setActiveShareIndex((prev) => prev - 1);
- }
- };
- const handleNextImage = () => {
- if (activeShareIndex < shareImages.length - 1) {
- setActiveShareIndex((prev) => prev + 1);
- }
- };
- useEffect(() => {
- if (showShareModal) {
- const previousOverflow = document.body.style.overflow;
- document.body.style.overflow = 'hidden';
- return () => {
- document.body.style.overflow = previousOverflow;
- };
- }
- return undefined;
- }, [showShareModal]);
- return (
- <div>
- {/* 顶部返回按钮 */}
- <button
- type="button"
- onClick={handleBack}
- style={{
- position: 'absolute',
- left: px2vmin(16),
- top: px2vmin(56),
- width: px2vmin(48),
- height: px2vmin(48),
- padding: 0,
- border: 'none',
- background: 'none',
- }}
- aria-label="Back"
- >
- <img
- src={backButton}
- alt="Back"
- style={{
- width: '100%',
- height: '100%',
- display: 'block',
- }}
- />
- </button>
- {/* 顶部进度条 */}
- <img
- src={shareProgressBar}
- alt="share progress bar"
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(61),
- width: px2vmin(195),
- transform: 'translateX(-50%)',
- }}
- />
- {/* 中部头像动画区域 */}
- <img
- src={avatarAnimaZone}
- alt="avatar anima zone"
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(192),
- width: px2vmin(343),
- transform: 'translateX(-50%)',
- }}
- />
- {!showShareModal && (
- <>
- {/* 标题文案 */}
- <div
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(504),
- transform: 'translateX(-50%)',
- color: '#FFFFFF',
- fontSize: px2vmin(24),
- fontWeight: 'bold',
- whiteSpace: 'nowrap',
- }}
- >
- Congratulation!
- </div>
- {/* 描述文案 */}
- <div
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(552),
- transform: 'translateX(-50%)',
- color: '#FFFFFF',
- fontSize: px2vmin(14),
- textAlign: 'center',
- width: px2vmin(230),
- }}
- >
- 哇,是一顶小帽子,谢谢你!要到新年了,快来分享我的新装扮并解锁限定新年背景吧
- </div>
- {/* 分享按钮 */}
- <button
- type="button"
- onClick={handleShareClick}
- style={{
- position: 'absolute',
- left: '50%',
- top: px2vmin(656),
- transform: 'translateX(-50%)',
- border: 'none',
- padding: 0,
- background: 'none',
- width: px2vmin(279),
- height: px2vmin(56),
- }}
- >
- <img
- src={shareBtn}
- alt="Share"
- style={{
- width: '100%',
- height: '100%',
- display: 'block',
- }}
- />
- </button>
- </>
- )}
- {showShareModal && (
- <>
- {/* 分享图片区域 - 在弹窗上方显示 */}
- <div
- style={{
- position: 'fixed',
- inset: 0,
- zIndex: 9997,
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'flex-start',
- paddingTop: px2vmin(118),
- pointerEvents: 'none',
- }}
- >
- <div
- style={{
- position: 'relative',
- width: px2vw(375),
- display: 'flex',
- justifyContent: 'center',
- }}
- >
- {shareImages.length > 0 && (
- <div
- style={{
- position: 'relative',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- }}
- >
- {/* 图片容器 */}
- <div
- onTouchStart={handleTouchStart}
- onTouchEnd={handleTouchEnd}
- style={{
- position: 'relative',
- width: px2vmin(311),
- display: 'flex',
- justifyContent: 'center',
- overflow: 'visible',
- pointerEvents: 'auto',
- }}
- >
- {/* 左侧按钮 */}
- {activeShareIndex > 0 && (
- <button
- type="button"
- onClick={handlePreviousImage}
- style={{
- position: 'absolute',
- left: px2vmin(-12),
- top: '50%',
- transform: 'translateY(-50%)',
- zIndex: 10001,
- border: 'none',
- padding: 0,
- background: 'none',
- width: px2vmin(24),
- height: px2vmin(24),
- cursor: 'pointer',
- pointerEvents: 'auto',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- }}
- aria-label="Previous image"
- >
- <img
- src={chevronLeftImage}
- alt="Previous"
- style={{
- width: '100%',
- height: '100%',
- display: 'block',
- }}
- />
- </button>
- )}
- <button
- type="button"
- onClick={handleShareClose}
- style={{
- position: 'absolute',
- right: 0,
- top: 0,
- transform: 'translate(-10%, -130%)',
- width: px2vmin(30),
- height: px2vmin(30),
- padding: 0,
- border: 'none',
- background: 'none',
- pointerEvents: 'auto',
- zIndex: 10000,
- }}
- aria-label="Close share modal"
- >
- <img
- src={closeBtn}
- alt="Close"
- style={{ width: '100%', height: '100%', display: 'block' }}
- />
- </button>
- <div
- style={{
- position: 'relative',
- width: '100%',
- display: 'flex',
- justifyContent: 'center',
- overflow: 'visible',
- }}
- >
- {activeShareIndex > 0 && (
- <img
- src={shareImages[activeShareIndex - 1]?.url}
- alt="Previous share preview"
- aria-hidden
- style={{
- position: 'absolute',
- height: 'auto',
- maxWidth: '100%',
- opacity: 1,
- pointerEvents: 'none',
- zIndex: 0,
- }}
- />
- )}
- {activeShareIndex < shareImages.length - 1 && (
- <img
- src={shareImages[activeShareIndex + 1]?.url}
- alt="Next share preview"
- aria-hidden
- style={{
- position: 'absolute',
- height: 'auto',
- maxWidth: '100%',
- opacity: 1,
- pointerEvents: 'none',
- zIndex: 0,
- }}
- />
- )}
- <img
- src={shareImages[activeShareIndex]?.url}
- alt="Share preview"
- style={{
- width: '100%',
- height: 'auto',
- display: 'block',
- borderRadius: px2vmin(12),
- boxShadow: 'none',
- position: 'relative',
- zIndex: 1,
- }}
- />
- </div>
- {/* 右侧按钮 */}
- {activeShareIndex < shareImages.length - 1 && (
- <button
- type="button"
- onClick={handleNextImage}
- style={{
- position: 'absolute',
- right: px2vmin(-12),
- top: '50%',
- transform: 'translateY(-50%)',
- zIndex: 10001,
- border: 'none',
- padding: 0,
- background: 'none',
- width: px2vmin(24),
- height: px2vmin(24),
- cursor: 'pointer',
- pointerEvents: 'auto',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- }}
- aria-label="Next image"
- >
- <img
- src={chevronRightImage}
- alt="Next"
- style={{
- width: '100%',
- height: '100%',
- display: 'block',
- }}
- />
- </button>
- )}
- </div>
- </div>
- )}
- {shareImages.length > 0 && (
- <div
- style={{
- position: 'absolute',
- top: px2vmin(514),
- left: '50%',
- transform: 'translateX(-50%)',
- display: 'flex',
- alignItems: 'center',
- gap: px2vmin(8),
- pointerEvents: 'auto',
- }}
- >
- {shareImages.map((item, index) => {
- const isActive = index === activeShareIndex;
- return (
- <button
- key={item.id}
- type="button"
- aria-label={`Select share image ${index + 1}`}
- onClick={() => setActiveShareIndex(index)}
- style={{
- width: isActive ? px2vmin(24) : px2vmin(6),
- height: px2vmin(6),
- borderRadius: isActive ? px2vmin(20) : px2vmin(6),
- background: isActive ? 'rgba(169, 132, 228, 1)' : 'rgba(232, 222, 248, 1)',
- border: 'none',
- padding: 0,
- }}
- />
- );
- })}
- </div>
- )}
- </div>
- </div>
- {/* 底部弹窗 */}
- {shareImages.length > 0 && (
- <div
- style={{
- position: 'fixed',
- bottom: 0,
- left: '50%',
- transform: 'translateX(-50%)',
- width: px2vw(375),
- height: px2vmin(162),
- backgroundColor: '#34313D',
- borderTopLeftRadius: px2vmin(32),
- borderTopRightRadius: px2vmin(32),
- zIndex: 9999,
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'center',
- paddingTop: px2vmin(12),
- paddingBottom: px2vmin(12),
- }}
- >
- {/* 顶部拖拽指示条 */}
- <div
- style={{
- width: px2vmin(32),
- height: px2vmin(4),
- backgroundColor: '#79747E',
- borderRadius: px2vmin(4),
- flexShrink: 0,
- }}
- />
- {/* 提示文字和按钮 */}
- <div
- style={{
- flex: 1,
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'center',
- justifyContent: 'center',
- width: '100%',
- }}
- >
- {/* 提示文字 */}
- <div
- style={{
- color: '#FFFFFF',
- textAlign: 'center',
- lineHeight: px2vmin(14),
- marginBottom: px2vmin(18),
- }}
- >
- 长按上方图片保存到相册后领取奖励
- </div>
- {/* 领取奖励按钮 */}
- <button
- type="button"
- onClick={handleSaveImage}
- style={{
- border: 'none',
- padding: 0,
- background: 'none',
- // width: px2vmin(118),
- height: px2vmin(35),
- cursor: 'pointer',
- }}
- >
- <img
- src={claimGiftBtnImage}
- alt="Claim Gift"
- style={{
- height: '100%',
- display: 'block',
- }}
- />
- </button>
- </div>
- </div>
- )}
- </>
- )}
- </div>
- );
- };
|