import { useCallback, useEffect, useMemo, useState } from 'react' import { Button, DotLoading, Empty, Segmented, Toast } from 'antd-mobile' import { Celebrate } from '../../components/RingProgress.jsx' import { checkinApi } from '../../api/checkin.js' import useRefreshOnVisible from '../../hooks/useRefreshOnVisible.js' import { toDateStr, addDays, addMonths, monthGrid, weekdayLabels, dayLabel, fmtMonth, } from './date.js' // 日历视图: 月历网格 + 今天/昨天/明天 三视图 + 日期标记 (F-CL-*) export default function CalendarView({ date, onDateChange }) { const today = toDateStr() const [month, setMonth] = useState(() => date.slice(0, 7)) const [cal, setCal] = useState(null) // 月历数据 const [detail, setDetail] = useState(null) // 选中日详情 const [err, setErr] = useState('') const [bursts, setBursts] = useState({}) const loadCal = useCallback((silent) => { if (!silent) setErr('') checkinApi.calendar(month) .then(setCal) .catch((e) => { if (!silent) setErr(e.message) }) }, [month]) const loadDetail = useCallback((silent) => { checkinApi.day(date).then(setDetail).catch(() => {}) }, [date]) useEffect(() => { loadCal(false) }, [loadCal]) useEffect(() => { setDetail(null); loadDetail(false) }, [loadDetail]) useRefreshOnVisible(() => { loadCal(true); loadDetail(true) }) // 切换日期时, 若跨月则同步月份 useEffect(() => { const m = date.slice(0, 7) setMonth((cur) => (cur === m ? cur : m)) }, [date]) const days = useMemo(() => monthGrid(month), [month]) const byDate = useMemo(() => { const map = new Map() ;(cal?.days || []).forEach((d) => map.set(d.date, d)) return map }, [cal]) const quickValue = useMemo(() => { if (date === today) return 0 if (date === addDays(today, -1)) return -1 if (date === addDays(today, 1)) return 1 return null }, [date, today]) async function checkFromCalendar(item) { if (!item.canCheck) return const snapshot = detail setDetail((d) => ({ ...d, items: d.items.map((x) => { if (x.id !== item.id) return x const done = x.done + 1 const countOnDate = (x.countOnDate || 0) + 1 return { ...x, done, countOnDate, percent: Math.min(1, done / x.target), completed: done >= x.target, over: Math.max(0, done - x.target), dayDone: countOnDate >= x.expectedPerDay, } }), })) try { const res = await checkinApi.check(item.id, date) const fresh = await checkinApi.day(date) setDetail(fresh) if (res.item.completed) setBursts((m) => ({ ...m, [item.id]: (m[item.id] || 0) + 1 })) loadCal(true) } catch (e) { setDetail(snapshot) Toast.show({ content: e.message || '打卡失败' }) } } const items = detail?.items || [] const isFuture = !!detail?.isFuture return (