'use client'; import { useEffect, useState, useRef } from 'react'; import { motion } from 'framer-motion'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; import { DatePicker, NumericInput } from './Common'; import { isNumber } from 'lodash'; import { CloseIcon } from './Icons'; dayjs.extend(utc); dayjs.extend(timezone); const DEFAULT_TZ = 'Asia/Shanghai'; const getBrowserTimeZone = () => { if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) { const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; return tz || DEFAULT_TZ; } return DEFAULT_TZ; }; const TZ = getBrowserTimeZone(); dayjs.tz.setDefault(TZ); const nowInTz = () => dayjs().tz(TZ); const formatDate = (input) => dayjs.tz(input, TZ).format('YYYY-MM-DD'); const CYCLES = [ { value: 'daily', label: '每日' }, { value: 'weekly', label: '每周' }, { value: 'biweekly', label: '每两周' }, { value: 'monthly', label: '每月' } ]; const WEEKDAY_OPTIONS = [ { value: 1, label: '周一' }, { value: 2, label: '周二' }, { value: 3, label: '周三' }, { value: 4, label: '周四' }, { value: 5, label: '周五' } ]; const computeFirstDate = (cycle, weeklyDay, monthlyDay) => { const today = nowInTz().startOf('day'); if (cycle === 'weekly' || cycle === 'biweekly') { const todayDay = today.day(); // 0-6, 1=周一 let target = isNumber(weeklyDay) ? weeklyDay : todayDay; if (target < 1 || target > 5) { // 如果当前是周末且未设定,默认周一 target = 1; } let candidate = today; for (let i = 0; i < 14; i += 1) { if (candidate.day() === target && !candidate.isBefore(today)) { break; } candidate = candidate.add(1, 'day'); } return candidate.format('YYYY-MM-DD'); } if (cycle === 'monthly') { const baseDay = today.date(); const day = isNumber(monthlyDay) && monthlyDay >= 1 && monthlyDay <= 28 ? monthlyDay : Math.min(28, baseDay); let candidate = today.date(day); if (candidate.isBefore(today)) { candidate = today.add(1, 'month').date(day); } return candidate.format('YYYY-MM-DD'); } return formatDate(today); }; export default function DcaModal({ fund, plan, onClose, onConfirm }) { const [amount, setAmount] = useState(''); const [feeRate, setFeeRate] = useState('0'); const [cycle, setCycle] = useState('monthly'); const [enabled, setEnabled] = useState(true); const [weeklyDay, setWeeklyDay] = useState(() => { const d = nowInTz().day(); return d >= 1 && d <= 5 ? d : 1; }); const [monthlyDay, setMonthlyDay] = useState(() => { const d = nowInTz().date(); return d >= 1 && d <= 28 ? d : 1; }); const [firstDate, setFirstDate] = useState(() => computeFirstDate('monthly', null, null)); const monthlyDayRef = useRef(null); useEffect(() => { if (!plan) { // 新建定投时,以当前默认 weeklyDay/monthlyDay 计算一次首扣日期 setFirstDate(computeFirstDate('monthly', weeklyDay, monthlyDay)); return; } if (plan.amount != null) { setAmount(String(plan.amount)); } if (plan.feeRate != null) { setFeeRate(String(plan.feeRate)); } if (typeof plan.enabled === 'boolean') { setEnabled(plan.enabled); } if (isNumber(plan.weeklyDay)) { setWeeklyDay(plan.weeklyDay); } if (isNumber(plan.monthlyDay)) { setMonthlyDay(plan.monthlyDay); } if (plan.cycle && CYCLES.some(c => c.value === plan.cycle)) { setCycle(plan.cycle); setFirstDate(plan.firstDate || computeFirstDate(plan.cycle, plan.weeklyDay, plan.monthlyDay)); } else { setFirstDate(plan.firstDate || computeFirstDate('monthly', null, null)); } }, [plan]); useEffect(() => { setFirstDate(computeFirstDate(cycle, weeklyDay, monthlyDay)); }, [cycle, weeklyDay, monthlyDay]); useEffect(() => { if (cycle !== 'monthly') return; if (monthlyDayRef.current) { try { monthlyDayRef.current.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } catch {} } }, [cycle, monthlyDay]); const handleSubmit = (e) => { e.preventDefault(); const amt = parseFloat(amount); const rate = parseFloat(feeRate); if (!fund?.code) return; if (!amt || amt <= 0) return; if (isNaN(rate) || rate < 0) return; if (!cycle) return; if ((cycle === 'weekly' || cycle === 'biweekly') && (weeklyDay < 1 || weeklyDay > 5)) return; if (cycle === 'monthly' && (monthlyDay < 1 || monthlyDay > 28)) return; onConfirm?.({ type: 'dca', fundCode: fund.code, fundName: fund.name, amount: amt, feeRate: rate, cycle, firstDate, weeklyDay: cycle === 'weekly' || cycle === 'biweekly' ? weeklyDay : null, monthlyDay: cycle === 'monthly' ? monthlyDay : null, enabled }); }; const isValid = () => { const amt = parseFloat(amount); const rate = parseFloat(feeRate); if (!fund?.code || !cycle || !firstDate) return false; if (!(amt > 0) || isNaN(rate) || rate < 0) return false; if ((cycle === 'weekly' || cycle === 'biweekly') && (weeklyDay < 1 || weeklyDay > 5)) return false; if (cycle === 'monthly' && (monthlyDay < 1 || monthlyDay > 28)) return false; return true; }; return ( e.stopPropagation()} style={{ maxWidth: '420px', maxHeight: '90vh', display: 'flex', flexDirection: 'column', }} >
🔁 定投
{fund?.name}
#{fund?.code}
{CYCLES.map((opt) => ( ))}
{(cycle === 'weekly' || cycle === 'biweekly') && (
{WEEKDAY_OPTIONS.map((opt) => ( ))}
)} {cycle === 'monthly' && (
{Array.from({ length: 28 }).map((_, idx) => { const day = idx + 1; const active = monthlyDay === day; return ( ); })}
)}
{firstDate}
* 基于当前日期和所选周期/扣款日自动计算:每日=当天;每周/每两周=从今天起最近的所选工作日;每月=从今天起最近的所选日期(1-28日)。
); }