fix(checkin): prevent visual flicker when loading check-in component

- Add initialLoaded state to track first data load completion
- Set isCollapsed to null initially, determined after data loads
- Show loading state on button and description text before data arrives
- Remove auto-collapse effect that caused visual flicker
- Add i18n translations for loading states (en/fr/ja/ru/vi/zh)

Fixes issue where component would collapse/expand after data loads,
causing visual flicker when navigating to personal settings page.
This commit is contained in:
RedwindA
2026-01-03 00:43:52 +08:00
parent 3402d09ed0
commit a0328b2f5e
7 changed files with 45 additions and 22 deletions

View File

@@ -53,8 +53,10 @@ const CheckinCalendar = ({ t, status }) => {
const [currentMonth, setCurrentMonth] = useState( const [currentMonth, setCurrentMonth] = useState(
new Date().toISOString().slice(0, 7), new Date().toISOString().slice(0, 7),
); );
// 折叠状态:如果已签到则默认折叠 // 初始加载状态,用于避免折叠状态闪烁
const [isCollapsed, setIsCollapsed] = useState(true); const [initialLoaded, setInitialLoaded] = useState(false);
// 折叠状态null 表示未确定(等待首次加载)
const [isCollapsed, setIsCollapsed] = useState(null);
// 创建日期到额度的映射,方便快速查找 // 创建日期到额度的映射,方便快速查找
const checkinRecordsMap = useMemo(() => { const checkinRecordsMap = useMemo(() => {
@@ -77,17 +79,31 @@ const CheckinCalendar = ({ t, status }) => {
// 获取签到状态 // 获取签到状态
const fetchCheckinStatus = async (month) => { const fetchCheckinStatus = async (month) => {
const isFirstLoad = !initialLoaded;
setLoading(true); setLoading(true);
try { try {
const res = await API.get(`/api/user/checkin?month=${month}`); const res = await API.get(`/api/user/checkin?month=${month}`);
const { success, data, message } = res.data; const { success, data, message } = res.data;
if (success) { if (success) {
setCheckinData(data); setCheckinData(data);
// 首次加载时,根据签到状态设置折叠状态
if (isFirstLoad) {
setIsCollapsed(data.stats?.checked_in_today ?? false);
setInitialLoaded(true);
}
} else { } else {
showError(message || t('获取签到状态失败')); showError(message || t('获取签到状态失败'));
if (isFirstLoad) {
setIsCollapsed(false);
setInitialLoaded(true);
}
} }
} catch (error) { } catch (error) {
showError(t('获取签到状态失败')); showError(t('获取签到状态失败'));
if (isFirstLoad) {
setIsCollapsed(false);
setInitialLoaded(true);
}
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -121,15 +137,6 @@ const CheckinCalendar = ({ t, status }) => {
} }
}, [status?.checkin_enabled, currentMonth]); }, [status?.checkin_enabled, currentMonth]);
// 当签到状态加载完成后,根据是否已签到设置折叠状态
useEffect(() => {
if (checkinData.stats?.checked_in_today) {
setIsCollapsed(true);
} else {
setIsCollapsed(false);
}
}, [checkinData.stats?.checked_in_today]);
// 如果签到功能未启用,不显示组件 // 如果签到功能未启用,不显示组件
if (!status?.checkin_enabled) { if (!status?.checkin_enabled) {
return null; return null;
@@ -200,11 +207,13 @@ const CheckinCalendar = ({ t, status }) => {
)} )}
</div> </div>
<div className='text-xs text-gray-500 dark:text-gray-400'> <div className='text-xs text-gray-500 dark:text-gray-400'>
{checkinData.stats?.checked_in_today {!initialLoaded
? t('今日已签到,累计签到') + ? t('正在加载签到状态...')
` ${checkinData.stats?.total_checkins || 0} ` + : checkinData.stats?.checked_in_today
t('天') ? t('今日已签到,累计签到') +
: t('每日签到可获得随机额度奖励')} ` ${checkinData.stats?.total_checkins || 0} ` +
t('天')
: t('每日签到可获得随机额度奖励')}
</div> </div>
</div> </div>
</div> </div>
@@ -213,18 +222,20 @@ const CheckinCalendar = ({ t, status }) => {
theme='solid' theme='solid'
icon={<Gift size={16} />} icon={<Gift size={16} />}
onClick={doCheckin} onClick={doCheckin}
loading={checkinLoading} loading={checkinLoading || !initialLoaded}
disabled={checkinData.stats?.checked_in_today} disabled={!initialLoaded || checkinData.stats?.checked_in_today}
className='!bg-green-600 hover:!bg-green-700' className='!bg-green-600 hover:!bg-green-700'
> >
{checkinData.stats?.checked_in_today {!initialLoaded
? t('今日已签到') ? t('加载中...')
: t('立即签到')} : checkinData.stats?.checked_in_today
? t('今日已签到')
: t('立即签到')}
</Button> </Button>
</div> </div>
{/* 可折叠内容 */} {/* 可折叠内容 */}
<Collapsible isOpen={!isCollapsed} keepDOM> <Collapsible isOpen={isCollapsed === false} keepDOM>
{/* 签到统计 */} {/* 签到统计 */}
<div className='grid grid-cols-3 gap-3 mb-4 mt-4'> <div className='grid grid-cols-3 gap-3 mb-4 mt-4'>
<div className='text-center p-2.5 bg-slate-50 dark:bg-slate-800 rounded-lg'> <div className='text-center p-2.5 bg-slate-50 dark:bg-slate-800 rounded-lg'>

View File

@@ -2192,6 +2192,8 @@
"每日签到可获得随机额度奖励": "Daily check-in rewards random quota", "每日签到可获得随机额度奖励": "Daily check-in rewards random quota",
"今日已签到": "Checked in today", "今日已签到": "Checked in today",
"立即签到": "Check in now", "立即签到": "Check in now",
"加载中...": "Loading...",
"正在加载签到状态...": "Loading check-in status...",
"获取签到状态失败": "Failed to get check-in status", "获取签到状态失败": "Failed to get check-in status",
"签到成功!获得": "Check-in successful! Received", "签到成功!获得": "Check-in successful! Received",
"签到失败": "Check-in failed", "签到失败": "Check-in failed",

View File

@@ -2241,6 +2241,8 @@
"每日签到可获得随机额度奖励": "L'enregistrement quotidien récompense un quota aléatoire", "每日签到可获得随机额度奖励": "L'enregistrement quotidien récompense un quota aléatoire",
"今日已签到": "Enregistré aujourd'hui", "今日已签到": "Enregistré aujourd'hui",
"立即签到": "S'enregistrer maintenant", "立即签到": "S'enregistrer maintenant",
"加载中...": "Chargement...",
"正在加载签到状态...": "Chargement du statut d'enregistrement...",
"获取签到状态失败": "Échec de la récupération du statut d'enregistrement", "获取签到状态失败": "Échec de la récupération du statut d'enregistrement",
"签到成功!获得": "Enregistrement réussi ! Reçu", "签到成功!获得": "Enregistrement réussi ! Reçu",
"签到失败": "Échec de l'enregistrement", "签到失败": "Échec de l'enregistrement",

View File

@@ -2140,6 +2140,8 @@
"每日签到可获得随机额度奖励": "毎日のチェックインでランダムなクォータ報酬を獲得できます", "每日签到可获得随机额度奖励": "毎日のチェックインでランダムなクォータ報酬を獲得できます",
"今日已签到": "本日チェックイン済み", "今日已签到": "本日チェックイン済み",
"立即签到": "今すぐチェックイン", "立即签到": "今すぐチェックイン",
"加载中...": "読み込み中...",
"正在加载签到状态...": "チェックイン状態を読み込み中...",
"获取签到状态失败": "チェックイン状態の取得に失敗しました", "获取签到状态失败": "チェックイン状態の取得に失敗しました",
"签到成功!获得": "チェックイン成功!獲得", "签到成功!获得": "チェックイン成功!獲得",
"签到失败": "チェックインに失敗しました", "签到失败": "チェックインに失敗しました",

View File

@@ -2251,6 +2251,8 @@
"每日签到可获得随机额度奖励": "Ежедневная регистрация награждает случайной квотой", "每日签到可获得随机额度奖励": "Ежедневная регистрация награждает случайной квотой",
"今日已签到": "Зарегистрирован сегодня", "今日已签到": "Зарегистрирован сегодня",
"立即签到": "Зарегистрироваться сейчас", "立即签到": "Зарегистрироваться сейчас",
"加载中...": "Загрузка...",
"正在加载签到状态...": "Загрузка статуса регистрации...",
"获取签到状态失败": "Не удалось получить статус регистрации", "获取签到状态失败": "Не удалось получить статус регистрации",
"签到成功!获得": "Регистрация успешна! Получено", "签到成功!获得": "Регистрация успешна! Получено",
"签到失败": "Регистрация не удалась", "签到失败": "Регистрация не удалась",

View File

@@ -2751,6 +2751,8 @@
"每日签到可获得随机额度奖励": "Đăng nhập hàng ngày để nhận phần thưởng hạn mức ngẫu nhiên", "每日签到可获得随机额度奖励": "Đăng nhập hàng ngày để nhận phần thưởng hạn mức ngẫu nhiên",
"今日已签到": "Đã đăng nhập hôm nay", "今日已签到": "Đã đăng nhập hôm nay",
"立即签到": "Đăng nhập ngay", "立即签到": "Đăng nhập ngay",
"加载中...": "Đang tải...",
"正在加载签到状态...": "Đang tải trạng thái đăng nhập...",
"获取签到状态失败": "Không thể lấy trạng thái đăng nhập", "获取签到状态失败": "Không thể lấy trạng thái đăng nhập",
"签到成功!获得": "Đăng nhập thành công! Đã nhận", "签到成功!获得": "Đăng nhập thành công! Đã nhận",
"签到失败": "Đăng nhập thất bại", "签到失败": "Đăng nhập thất bại",

View File

@@ -2218,6 +2218,8 @@
"每日签到可获得随机额度奖励": "每日签到可获得随机额度奖励", "每日签到可获得随机额度奖励": "每日签到可获得随机额度奖励",
"今日已签到": "今日已签到", "今日已签到": "今日已签到",
"立即签到": "立即签到", "立即签到": "立即签到",
"加载中...": "加载中...",
"正在加载签到状态...": "正在加载签到状态...",
"获取签到状态失败": "获取签到状态失败", "获取签到状态失败": "获取签到状态失败",
"签到成功!获得": "签到成功!获得", "签到成功!获得": "签到成功!获得",
"签到失败": "签到失败", "签到失败": "签到失败",