考虑到这个主要组件
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import axiosInstance from '../../../axiosConfig';
import './wheel.css';
import { getUserInfo } from '../../../user_info';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
import MemoizedWheel from './MemoizedWheel';
import { useUser } from '../../../UserContext';
import WheelAdmin from './WheelAdmin';
const LoadingAnimation = () => (
<div className="loading-animation">
<div className="spinner"></div>
<p>Loading...</p>
</div>
);
const ButtonLoadingAnimation = () => (
<div className="button-loading-animation">
<div className="dot-pulse"></div>
</div>
);
export default function WheelComponent({
availablePrizes,
hasPlayedEnough,
isLoading,
error,
onSuccessfulSpin
}) {
const { user } = useUser();
console.log('Wheel');
const [prizes, setPrizes] = useState(availablePrizes);
const [mustSpin, setMustSpin] = useState(false);
const [prizeNumber, setPrizeNumber] = useState(0);
const [showPrize, setShowPrize] = useState(false);
const [apiMessage, setApiMessage] = useState('');
const [awards, setAwards] = useState([]);
const [isAwardsTableVisible, setIsAwardsTableVisible] = useState(false);
const [awardsError, setAwardsError] = useState(null);
const [isTableLoading, setIsTableLoading] = useState(false);
const [showFireworks, setShowFireworks] = useState(false);
const [availableSpins, setAvailableSpins] = useState(0);
const [cooldownTime, setCooldownTime] = useState(0);
const [countdown, setCountdown] = useState(null);
const [spinError, setSpinError] = useState(null);
const awardsRef = useRef(null);
const isSpinning = useRef(false);
const imageSizeMultiplier = 0.37;
const wheelSizeMultiplier = 1.3;
const imageOrientation = 'portrait';
const tableRowVariants = {
hidden: { opacity: 0, y: 20 },
visible: (i) => ({
opacity: 1,
y: 0,
transition: {
delay: i * 0.1,
duration: 0.3,
},
}),
exit: { opacity: 0, y: -20, transition: { duration: 0.2 } },
};
const sortedAwards = useMemo(() => {
// console.log('Sorting awards:', awards);
return [...awards].sort((a, b) => new Date(b.given_time) - new Date(a.given_time));
}, [awards]);
const fetchPrizes = useCallback(async () => {
setIsLoading(true);
try {
const prizesResponse = await axiosInstance.get('/wheel-awards/available-prizes/');
const fetchedPrizes = prizesResponse.data.map(item => ({
id: item.id,
image_uri: { uri: item.image_uri },
style: { backgroundColor: item.style },
text: item.text,
weight: item.weight,
vnum: item.vnum,
count: item.count
}));
setPrizes(fetchedPrizes);
setError(null);
} catch (err) {
console.error('Error fetching prizes:', err);
setError('Failed to load prizes. Please try again later.');
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
if (!prizes || prizes.length === 0) {
fetchPrizes();
}
}, [prizes, fetchPrizes]);
useEffect(() => {
const checkSpinData = async () => {
const storedSpins = localStorage.getItem('availableSpins');
if (storedSpins) {
const spins = parseInt(storedSpins, 10);
if (spins > 0) {
setAvailableSpins(spins);
setCooldownTime(0);
setCountdown(null);
localStorage.removeItem('wheelCooldownEndTime');
} else {
await fetchSpinData();
}
} else {
await fetchSpinData();
}
};
const fetchSpinData = async () => {
try {
const ip = await fetch('https://api.ipify.org?format=json')
.then(response => response.json())
.then(data => data.ip);
const spinsResponse = await axiosInstance.get(`/wheel-awards/user-spin/?ip=${ip}`);
const { number_of_spins, last_spin_date, spin_refresh } = spinsResponse.data;
setAvailableSpins(number_of_spins);
localStorage.setItem('availableSpins', number_of_spins.toString());
if (number_of_spins > 0) {
setCooldownTime(0);
setCountdown(null);
localStorage.removeItem('wheelCooldownEndTime');
} else {
const lastSpinTime = new Date(last_spin_date).getTime();
const currentTime = Date.now();
const elapsedTime = (currentTime - lastSpinTime) / 1000;
const remainingCooldown = Math.max(0, spin_refresh - elapsedTime);
setCooldownTime(Math.ceil(remainingCooldown));
const endTime = Date.now() + remainingCooldown * 1000;
localStorage.setItem('wheelCooldownEndTime', endTime.toString());
}
} catch (error) {
console.error('Error fetching spin data:', error);
}
};
checkSpinData();
}, []);
useEffect(() => {
let timer;
if (showPrize) {
timer = setTimeout(() => {
setShowPrize(false);
}, 122250);
}
return () => clearTimeout(timer);
}, [showPrize]);
useEffect(() => {
// console.log('Awards state updated:', awards);
}, [awards]);
useEffect(() => {
let intervalId;
const updateCountdown = () => {
const endTime = parseInt(localStorage.getItem('wheelCooldownEndTime'), 10);
if (!endTime) return;
const remaining = Math.max(0, Math.ceil((endTime - Date.now()) / 1000));
setCountdown(remaining);
if (remaining === 0) {
localStorage.removeItem('wheelCooldownEndTime');
setAvailableSpins(1);
localStorage.setItem('availableSpins', '1');
}
};
if (availableSpins === 0 && cooldownTime > 0) {
updateCountdown();
intervalId = setInterval(updateCountdown, 1000);
}
return () => {
if (intervalId) clearInterval(intervalId);
};
}, [availableSpins, cooldownTime]);
const fetchAwards = useCallback(async () => {
setIsTableLoading(true);
try {
const response = await axiosInstance.get('/wheel-awards/awards-list/');
console.log('Awards API response:', response.data);
setAwards(response.data);
setAwardsError(null);
} catch (error) {
console.error('Error fetching awards:', error);
setAwardsError('Failed to load awards data. Please try again later.');
} finally {
setIsTableLoading(false);
}
}, []);
const handleSpinClick = useCallback(async () => {
if (isSpinning.current || availableSpins === 0 || !hasPlayedEnough) return;
isSpinning.current = true;
setSpinError(null);
try {
const userInfo = getUserInfo();
const response = await axiosInstance.post('/wheel-awards/get-prize/', {
ip_address: userInfo.ip,
country: userInfo.country,
unique_id: userInfo.unque_id
});
const { prizeNumber, selectedPrize } = response.data;
setPrizeNumber(prizeNumber);
setMustSpin(true);
setShowPrize(false);
const newSpinCount = availableSpins - 1;
setAvailableSpins(newSpinCount);
localStorage.setItem('availableSpins', newSpinCount.toString());
if (newSpinCount === 0) {
const ip = await fetch('https://api.ipify.org?format=json')
.then(response => response.json())
.then(data => data.ip);
const spinsResponse = await axiosInstance.get(`/wheel-awards/user-spin/?ip=${ip}`);
const { last_spin_date, spin_refresh } = spinsResponse.data;
const lastSpinTime = new Date(last_spin_date).getTime();
const currentTime = Date.now();
const elapsedTime = (currentTime - lastSpinTime) / 1000;
const remainingCooldown = Math.max(0, spin_refresh - elapsedTime);
setCooldownTime(Math.ceil(remainingCooldown));
const endTime = Date.now() + remainingCooldown * 1000;
localStorage.setItem('wheelCooldownEndTime', endTime.toString());
}
onSuccessfulSpin();
} catch (error) {
console.error('Error fetching prize:', error);
setSpinError("Something went wrong while spinning. Please contact an administrator or try again.");
setMustSpin(false);
} finally {
isSpinning.current = false;
}
}, [availableSpins, hasPlayedEnough, onSuccessfulSpin]);
const dataWithImageSize = useMemo(() => prizes.map((item) => ({
...item,
image: {
...item.image_uri,
sizeMultiplier: imageSizeMultiplier,
landscape: imageOrientation === 'landscape',
},
})), [prizes, imageSizeMultiplier, imageOrientation]);
const toggleAwardsTable = useCallback(() => {
setIsAwardsTableVisible(!isAwardsTableVisible);
if (!isAwardsTableVisible && awards.length === 0) {
fetchAwards();
}
if (!isAwardsTableVisible) {
setTimeout(() => {
awardsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
}
}, [isAwardsTableVisible, awards.length, fetchAwards]);
const handleStopSpinning = useCallback(() => {
isSpinning.current = false;
setMustSpin(false);
setShowPrize(true);
setShowFireworks(true);
fetchAwards();
setTimeout(() => {
setShowFireworks(false);
}, 3300);
}, [fetchAwards]);
const formatTime = (totalSeconds) => {
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const parts = [];
if (hours > 0) parts.push(`${hours}h`);
if (minutes > 0 || hours > 0) parts.push(`${minutes}m`);
parts.push(`${seconds}s`);
return parts.join(' ');
};
if (error) {
return <div className="error-message">{error}</div>;
}
if (isLoading || prizes.length === 0) {
return <LoadingAnimation />;
}
return (
<>
<h1 className="wheel-title">Wheel of fortune</h1>
<div className='wheel-main-container'>
<div className='wheel' style={{ transform: `scale(${wheelSizeMultiplier})` }}>
<MemoizedWheel
mustSpin={mustSpin}
prizeNumber={prizeNumber}
data={dataWithImageSize}
onStopSpinning={handleStopSpinning}
/>
<AnimatePresence>
{showPrize ? (
<div className="prize-display-container">
<motion.div
key="prize-display"
className="prize-display"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="green"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="circle-border"
>
<circle cx="12" cy="12" r="10"></circle>
</svg>
<img className='prize-img' src={prizes[prizeNumber].image_uri.uri} alt={`Prize ${prizeNumber + 1}`} />
<p className='prize-text'>{prizes[prizeNumber].text}</p>
<p className='prize-text'>{prizes[prizeNumber].count}</p>
</motion.div>
</div>
) : (
<motion.div
key="prize-display-none"
className="prize-display-none"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="feather feather-help-circle"
>
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 1 1 5.82 1c0 2-3 3-3 4.5"></path>
<line x1="12" y1="17" x2="12" y2="17"></line>
</svg>
</motion.div>
)}
</AnimatePresence>
</div>
<div className="spin-button-container">
{apiMessage === "Only two accounts allowed per network" && (
<div className="error-container">
<p className="dup-acc-err-msg">You have already spun the wheel on another account!</p>
</div>
)}
<button
className='spin-button'
onClick={handleSpinClick}
disabled={isSpinning.current || mustSpin || availableSpins === 0 || !hasPlayedEnough}
>
{isSpinning.current || mustSpin ? 'Spinning...' :
availableSpins > 0 ? `SPIN (${availableSpins} spins left)` :
countdown !== null ? `Next spin in ${formatTime(countdown)}` :
'SPIN'}
</button>
{!hasPlayedEnough && (
<p className="play-time-message">You have to play at least 30 minutes to access the wheel.</p>
)}
{spinError && (
<div className="spin-error-message">
{spinError}
</div>
)}
</div>
{user && user.isAdmin && <WheelAdmin prizes={prizes} setPrizes={setPrizes} />}
<div className="awards-table-container" ref={awardsRef}>
<h2 className="awards-title" onClick={toggleAwardsTable}>
Your Awards
<span className="arrow-container">
<FontAwesomeIcon
icon={faChevronDown}
className={`arrow ${isAwardsTableVisible ? 'up' : ''}`}
/>
</span>
</h2>
<AnimatePresence>
{isAwardsTableVisible && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3 }}
>
{isTableLoading ? (
<LoadingAnimation />
) : awardsError ? (
<div className="awards-error">{awardsError}</div>
) : awards.length > 0 ? (
<table className="awards-table">
<thead>
<tr>
<th>Prize</th>
<th>Item Name</th>
<th>Count</th>
<th>Date</th>
<th>Assigned in game</th>
</tr>
</thead>
<tbody>
<AnimatePresence>
{sortedAwards.map((award, index) => (
<motion.tr
key={index}
custom={index}
variants={tableRowVariants}
initial="hidden"
animate="visible"
exit="exit"
>
<td className='prize-image' data-label="Image">
<img src={award.prize_image} alt={award.prize_text} className="award-image" />
</td>
<td data-label="Prize Text">{award.prize_text}</td>
<td data-label="Count">{award.count}</td>
<td data-label="Date">{new Date(award.given_time).toLocaleString()}</td>
<td data-label="Is Item Given">{award.is_item_given ? 'Yes' : 'No'}</td>
</motion.tr>
))}
</AnimatePresence>
</tbody>
</table>
) : (
<p>No awards to display.</p>
)}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
{showFireworks && (
<div className="fireworks-container">
<div className="firework"></div>
<div className="firework"></div>
<div className="firework"></div>
<div className="firework"></div>
<div className="firework"></div>
<div className="firework"></div>
<div className="firework"></div>
<div className="firework"></div>
<div className="firework"></div>
<div className="firework"></div>
<div className="firework"></div>
<div className="firework"></div>
<div className="firework"></div>
<div className="firework"></div>
<div className="firework"></div>
</div>
)}
</>
);
}
和实际的车轮补偿
import React, { memo } from 'react';
import { Wheel } from 'react-custom-roulette';
const MemoizedWheel = memo(({ mustSpin, prizeNumber, data, onStopSpinning }) => (
<Wheel
mustStartSpinning={mustSpin}
prizeNumber={prizeNumber}
data={data}
onStopSpinning={onStopSpinning}
outerBorderColor="transparent"
outerBorderWidth={10}
innerRadius={40}
innerBorderColor="#30261a"
innerBorderWidth={0}
radiusLineColor="#30261a"
radiusLineWidth={2}
spinDuration={0.8}
fontFamily="Arial"
fontSize={16}
perpendicularText={false}
textDistance={75}
pointerProps={{
style: {
position: 'absolute',
transform: 'translateX(-75%) translateY(89%) rotate(225deg)',
width: '0',
height: '0',
borderLeft: '20px solid transparent',
borderRight: '20px solid transparent',
borderBottom: '40px solid #f0ad4e',
},
}}
/>
));
export default MemoizedWheel;
和样本数据集
[{"id":1,"image_uri":"https://i.imgur.com/hB2eSLx.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Epic Exchalibur Sword","weight":1,"vnum":19000,"count":1,"disable":false},{"id":2,"image_uri":"https://i.imgur.com/AYPh2Rh.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Epic Sun Sword","weight":1,"vnum":19020,"count":1,"disable":false},{"id":3,"image_uri":"https://i.imgur.com/mTPRdtK.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Epic Drone Sword","weight":1,"vnum":19010,"count":1,"disable":false},{"id":4,"image_uri":"https://i.imgur.com/6eEKT6R.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Epic Naga Bell","weight":5,"vnum":19050,"count":1,"disable":false},{"id":5,"image_uri":"https://i.imgur.com/HEdv58z.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Epic Furry Bow","weight":5,"vnum":19040,"count":1,"disable":false},{"id":6,"image_uri":"https://i.imgur.com/PbsG609.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Epic Aqua Daggers","weight":3,"vnum":19030,"count":1,"disable":false},{"id":7,"image_uri":"https://imgur.com/5CgIyyg.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Ghost Fang Blade","weight":80,"vnum":150,"count":1,"disable":false},{"id":8,"image_uri":"https://imgur.com/8SdWahU.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Poison Sword","weight":15,"vnum":180,"count":1,"disable":false},{"id":9,"image_uri":"https://imgur.com/SnP8Kj9.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Dragon Knife","weight":80,"vnum":1100,"count":1,"disable":false},{"id":10,"image_uri":"https://imgur.com/PJq7TpL.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Chakrams","weight":65,"vnum":1130,"count":1,"disable":false},{"id":11,"image_uri":"https://imgur.com/I28XExS.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Yellow Dragon Bow","weight":80,"vnum":2140,"count":1,"disable":false},{"id":12,"image_uri":"https://imgur.com/jtKeOoW.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Crow Steel Bow","weight":65,"vnum":2170,"count":1,"disable":false},{"id":13,"image_uri":"https://imgur.com/NQJgr7q.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Partizan","weight":80,"vnum":3130,"count":1,"disable":false},{"id":14,"image_uri":"https://imgur.com/AtUASaK.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Soul Stealing Blade","weight":80,"vnum":3150,"count":1,"disable":false},{"id":15,"image_uri":"https://imgur.com/cVKO4mi.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Grudge Sword","weight":65,"vnum":3160,"count":1,"disable":false},{"id":16,"image_uri":"https://imgur.com/xENUAxi.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Thunder Bird Bell","weight":80,"vnum":5090,"count":1,"disable":false},{"id":17,"image_uri":"https://imgur.com/ifW0Lmp.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Bamboo Bell","weight":65,"vnum":5120,"count":1,"disable":false},{"id":18,"image_uri":"https://imgur.com/aOenn72.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Salvation Fan+0","weight":80,"vnum":7140,"count":1,"disable":false},{"id":19,"image_uri":"https://imgur.com/lecJB5q.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Battle Sword","weight":80,"vnum":140,"count":1,"disable":false},{"id":20,"image_uri":"https://imgur.com/baQrCRU.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Magic Metal","weight":90,"vnum":30064,"count":1,"disable":false},{"id":21,"image_uri":"https://imgur.com/uN0Bdf3.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Piece Of Crystal","weight":6,"vnum":30204,"count":1,"disable":false},{"id":22,"image_uri":"https://imgur.com/ChRiDE2.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"White Pearl","weight":20,"vnum":27992,"count":1,"disable":false},{"id":23,"image_uri":"https://imgur.com/Z1oxrjU.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Blue Pearl","weight":15,"vnum":27993,"count":1,"disable":false},{"id":24,"image_uri":"https://imgur.com/6jSG7YX.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Red Pearl","weight":15,"vnum":27994,"count":1,"disable":false},{"id":26,"image_uri":"https://imgur.com/uOhl6ag.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Black Steel Armour+0","weight":80,"vnum":11290,"count":1,"disable":false},{"id":27,"image_uri":"https://imgur.com/OjE6Tn4.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Black Wind Suit+0","weight":80,"vnum":11490,"count":1,"disable":false},{"id":28,"image_uri":"https://imgur.com/T88JcL4.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Magic Plate Armour+0","weight":80,"vnum":11690,"count":1,"disable":false},{"id":29,"image_uri":"https://imgur.com/4B9D92Q.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Black Clothing+0","weight":80,"vnum":11890,"count":1,"disable":false},{"id":30,"image_uri":"https://imgur.com/mfXXS4X.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Water Gem","weight":6,"vnum":90005,"count":1,"disable":false},{"id":35,"image_uri":"https://imgur.com/a5GQCTc.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Rare Add Bonus 6-8","weight":1,"vnum":71051,"count":1,"disable":false},{"id":36,"image_uri":"https://imgur.com/l0HbfEH.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Change Bonus x100","weight":55,"vnum":71084,"count":100,"disable":false},{"id":37,"image_uri":"https://imgur.com/57uWoBO.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Nahan Armour+0","weight":5,"vnum":11200,"count":1,"disable":false},{"id":38,"image_uri":"https://imgur.com/UL5CpLk.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Tanma Armour+0","weight":5,"vnum":11400,"count":1,"disable":false},{"id":39,"image_uri":"https://imgur.com/JVyYetM.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Jinhon Armour+0","weight":5,"vnum":11600,"count":1,"disable":false},{"id":40,"image_uri":"https://imgur.com/bS5HIsa.png","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"11800","weight":5,"vnum":11800,"count":1,"disable":false},{"id":41,"image_uri":"https://imgur.com/HLeHOEv","style":"linear-gradient(to right, #ff416c, #ff4b2b)","text":"Strong against HH Stone","weight":5,"vnum":50512,"count":1,"disable":false}]
那么这里,如果这个数组len大于35,那么轮盘就不会显示任何奖品了?
没有找到任何,也在沙盒轮上进行了测试,默认,有超过 35 个奖品可以显示,并且工作没有问题,有什么关于我可能做错的原因的建议吗?
因此,如果图像 url 不好,例如没有返回要显示的图像,则整个轮子决定不渲染