From 6c1ddc6add1b2e9824d0300a4780b70a3e1072f1 Mon Sep 17 00:00:00 2001 From: hzm <934585316@qq.com> Date: Sun, 8 Feb 2026 22:31:27 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E7=BB=84=E4=BB=B6=E6=8B=86?= =?UTF-8?q?=E5=88=86=E5=92=8C=E6=80=A7=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/fund.js | 406 ++++++++++++++++ app/components/Common.jsx | 286 ++++++++++++ app/components/Icons.jsx | 190 ++++++++ app/page.jsx | 956 ++------------------------------------ next.config.js | 1 + package-lock.json | 47 ++ package.json | 3 + 7 files changed, 968 insertions(+), 921 deletions(-) create mode 100644 app/api/fund.js create mode 100644 app/components/Common.jsx create mode 100644 app/components/Icons.jsx diff --git a/app/api/fund.js b/app/api/fund.js new file mode 100644 index 0000000..b3c9f21 --- /dev/null +++ b/app/api/fund.js @@ -0,0 +1,406 @@ +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; + +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.tz.setDefault('Asia/Shanghai'); + +const TZ = 'Asia/Shanghai'; +const nowInTz = () => dayjs().tz(TZ); +const toTz = (input) => (input ? dayjs.tz(input, TZ) : nowInTz()); + +export const loadScript = (url) => { + return new Promise((resolve, reject) => { + if (typeof document === 'undefined' || !document.body) return resolve(); + const script = document.createElement('script'); + script.src = url; + script.async = true; + const cleanup = () => { + if (document.body.contains(script)) document.body.removeChild(script); + }; + script.onload = () => { + cleanup(); + resolve(); + }; + script.onerror = () => { + cleanup(); + reject(new Error('数据加载失败')); + }; + document.body.appendChild(script); + }); +}; + +export const fetchFundNetValue = async (code, date) => { + if (typeof window === 'undefined') return null; + const url = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${code}&page=1&per=1&sdate=${date}&edate=${date}`; + try { + await loadScript(url); + if (window.apidata && window.apidata.content) { + const content = window.apidata.content; + if (content.includes('暂无数据')) return null; + const rows = content.split(''); + for (const row of rows) { + if (row.includes(`${date}`)) { + const cells = row.match(/]*>(.*?)<\/td>/g); + if (cells && cells.length >= 2) { + const valStr = cells[1].replace(/<[^>]+>/g, ''); + const val = parseFloat(valStr); + return isNaN(val) ? null : val; + } + } + } + } + return null; + } catch (e) { + return null; + } +}; + +export const fetchSmartFundNetValue = async (code, startDate) => { + const today = nowInTz().startOf('day'); + let current = toTz(startDate).startOf('day'); + for (let i = 0; i < 30; i++) { + if (current.isAfter(today)) break; + const dateStr = current.format('YYYY-MM-DD'); + const val = await fetchFundNetValue(code, dateStr); + if (val !== null) { + return { date: dateStr, value: val }; + } + current = current.add(1, 'day'); + } + return null; +}; + +export const fetchFundDataFallback = async (c) => { + if (typeof window === 'undefined' || typeof document === 'undefined') { + throw new Error('无浏览器环境'); + } + return new Promise(async (resolve, reject) => { + const searchCallbackName = `SuggestData_fallback_${Date.now()}`; + const searchUrl = `https://fundsuggest.eastmoney.com/FundSearch/api/FundSearchAPI.ashx?m=1&key=${encodeURIComponent(c)}&callback=${searchCallbackName}&_=${Date.now()}`; + let fundName = ''; + try { + await new Promise((resSearch, rejSearch) => { + window[searchCallbackName] = (data) => { + if (data && data.Datas && data.Datas.length > 0) { + const found = data.Datas.find(d => d.CODE === c); + if (found) { + fundName = found.NAME || found.SHORTNAME || ''; + } + } + delete window[searchCallbackName]; + resSearch(); + }; + const script = document.createElement('script'); + script.src = searchUrl; + script.async = true; + script.onload = () => { + if (document.body.contains(script)) document.body.removeChild(script); + }; + script.onerror = () => { + if (document.body.contains(script)) document.body.removeChild(script); + delete window[searchCallbackName]; + rejSearch(new Error('搜索接口失败')); + }; + document.body.appendChild(script); + setTimeout(() => { + if (window[searchCallbackName]) { + delete window[searchCallbackName]; + resSearch(); + } + }, 3000); + }); + } catch (e) { + } + const tUrl = `https://qt.gtimg.cn/q=jj${c}`; + const tScript = document.createElement('script'); + tScript.src = tUrl; + tScript.onload = () => { + const v = window[`v_jj${c}`]; + if (v && v.length > 5) { + const p = v.split('~'); + const name = fundName || p[1] || `未知基金(${c})`; + const dwjz = p[5]; + const zzl = parseFloat(p[7]); + const jzrq = p[8] ? p[8].slice(0, 10) : ''; + if (dwjz) { + resolve({ + code: c, + name: name, + dwjz: dwjz, + gsz: null, + gztime: null, + jzrq: jzrq, + gszzl: null, + zzl: !isNaN(zzl) ? zzl : null, + noValuation: true, + holdings: [] + }); + } else { + reject(new Error('未能获取到基金数据')); + } + } else { + reject(new Error('未能获取到基金数据')); + } + if (document.body.contains(tScript)) document.body.removeChild(tScript); + }; + tScript.onerror = () => { + if (document.body.contains(tScript)) document.body.removeChild(tScript); + reject(new Error('基金数据加载失败')); + }; + document.body.appendChild(tScript); + }); +}; + +export const fetchFundData = async (c) => { + if (typeof window === 'undefined' || typeof document === 'undefined') { + throw new Error('无浏览器环境'); + } + return new Promise(async (resolve, reject) => { + const gzUrl = `https://fundgz.1234567.com.cn/js/${c}.js?rt=${Date.now()}`; + const scriptGz = document.createElement('script'); + scriptGz.src = gzUrl; + const originalJsonpgz = window.jsonpgz; + window.jsonpgz = (json) => { + window.jsonpgz = originalJsonpgz; + if (!json || typeof json !== 'object') { + fetchFundDataFallback(c).then(resolve).catch(reject); + return; + } + const gszzlNum = Number(json.gszzl); + const gzData = { + code: json.fundcode, + name: json.name, + dwjz: json.dwjz, + gsz: json.gsz, + gztime: json.gztime, + jzrq: json.jzrq, + gszzl: Number.isFinite(gszzlNum) ? gszzlNum : json.gszzl + }; + const tencentPromise = new Promise((resolveT) => { + const tUrl = `https://qt.gtimg.cn/q=jj${c}`; + const tScript = document.createElement('script'); + tScript.src = tUrl; + tScript.onload = () => { + const v = window[`v_jj${c}`]; + if (v) { + const p = v.split('~'); + resolveT({ + dwjz: p[5], + zzl: parseFloat(p[7]), + jzrq: p[8] ? p[8].slice(0, 10) : '' + }); + } else { + resolveT(null); + } + if (document.body.contains(tScript)) document.body.removeChild(tScript); + }; + tScript.onerror = () => { + if (document.body.contains(tScript)) document.body.removeChild(tScript); + resolveT(null); + }; + document.body.appendChild(tScript); + }); + const holdingsPromise = new Promise((resolveH) => { + const holdingsUrl = `https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code=${c}&topline=10&year=&month=&_=${Date.now()}`; + loadScript(holdingsUrl).then(async () => { + let holdings = []; + const html = window.apidata?.content || ''; + const headerRow = (html.match(/[\s\S]*?<\/thead>/i) || [])[0] || ''; + const headerCells = (headerRow.match(/([\s\S]*?)<\/th>/gi) || []).map(th => th.replace(/<[^>]*>/g, '').trim()); + let idxCode = -1, idxName = -1, idxWeight = -1; + headerCells.forEach((h, i) => { + const t = h.replace(/\s+/g, ''); + if (idxCode < 0 && (t.includes('股票代码') || t.includes('证券代码'))) idxCode = i; + if (idxName < 0 && (t.includes('股票名称') || t.includes('证券名称'))) idxName = i; + if (idxWeight < 0 && (t.includes('占净值比例') || t.includes('占比'))) idxWeight = i; + }); + const rows = html.match(//i) || []; + const dataRows = rows.length ? rows[0].match(//gi) || [] : html.match(//gi) || []; + for (const r of dataRows) { + const tds = (r.match(/([\s\S]*?)<\/td>/gi) || []).map(td => td.replace(/<[^>]*>/g, '').trim()); + if (!tds.length) continue; + let code = ''; + let name = ''; + let weight = ''; + if (idxCode >= 0 && tds[idxCode]) { + const m = tds[idxCode].match(/(\d{6})/); + code = m ? m[1] : tds[idxCode]; + } else { + const codeIdx = tds.findIndex(txt => /^\d{6}$/.test(txt)); + if (codeIdx >= 0) code = tds[codeIdx]; + } + if (idxName >= 0 && tds[idxName]) { + name = tds[idxName]; + } else if (code) { + const i = tds.findIndex(txt => txt && txt !== code && !/%$/.test(txt)); + name = i >= 0 ? tds[i] : ''; + } + if (idxWeight >= 0 && tds[idxWeight]) { + const wm = tds[idxWeight].match(/([\d.]+)\s*%/); + weight = wm ? `${wm[1]}%` : tds[idxWeight]; + } else { + const wIdx = tds.findIndex(txt => /\d+(?:\.\d+)?\s*%/.test(txt)); + weight = wIdx >= 0 ? tds[wIdx].match(/([\d.]+)\s*%/)?.[1] + '%' : ''; + } + if (code || name || weight) { + holdings.push({ code, name, weight, change: null }); + } + } + holdings = holdings.slice(0, 10); + const needQuotes = holdings.filter(h => /^\d{6}$/.test(h.code) || /^\d{5}$/.test(h.code)); + if (needQuotes.length) { + try { + const tencentCodes = needQuotes.map(h => { + const cd = String(h.code || ''); + if (/^\d{6}$/.test(cd)) { + const pfx = cd.startsWith('6') || cd.startsWith('9') ? 'sh' : ((cd.startsWith('4') || cd.startsWith('8')) ? 'bj' : 'sz'); + return `s_${pfx}${cd}`; + } + if (/^\d{5}$/.test(cd)) { + return `s_hk${cd}`; + } + return null; + }).filter(Boolean).join(','); + if (!tencentCodes) { + resolveH(holdings); + return; + } + const quoteUrl = `https://qt.gtimg.cn/q=${tencentCodes}`; + await new Promise((resQuote) => { + const scriptQuote = document.createElement('script'); + scriptQuote.src = quoteUrl; + scriptQuote.onload = () => { + needQuotes.forEach(h => { + const cd = String(h.code || ''); + let varName = ''; + if (/^\d{6}$/.test(cd)) { + const pfx = cd.startsWith('6') || cd.startsWith('9') ? 'sh' : ((cd.startsWith('4') || cd.startsWith('8')) ? 'bj' : 'sz'); + varName = `v_s_${pfx}${cd}`; + } else if (/^\d{5}$/.test(cd)) { + varName = `v_s_hk${cd}`; + } else { + return; + } + const dataStr = window[varName]; + if (dataStr) { + const parts = dataStr.split('~'); + if (parts.length > 5) { + h.change = parseFloat(parts[5]); + } + } + }); + if (document.body.contains(scriptQuote)) document.body.removeChild(scriptQuote); + resQuote(); + }; + scriptQuote.onerror = () => { + if (document.body.contains(scriptQuote)) document.body.removeChild(scriptQuote); + resQuote(); + }; + document.body.appendChild(scriptQuote); + }); + } catch (e) { + } + } + resolveH(holdings); + }).catch(() => resolveH([])); + }); + Promise.all([tencentPromise, holdingsPromise]).then(([tData, holdings]) => { + if (tData) { + if (tData.jzrq && (!gzData.jzrq || tData.jzrq >= gzData.jzrq)) { + gzData.dwjz = tData.dwjz; + gzData.jzrq = tData.jzrq; + gzData.zzl = tData.zzl; + } + } + resolve({ ...gzData, holdings }); + }); + }; + scriptGz.onerror = () => { + window.jsonpgz = originalJsonpgz; + if (document.body.contains(scriptGz)) document.body.removeChild(scriptGz); + reject(new Error('基金数据加载失败')); + }; + document.body.appendChild(scriptGz); + setTimeout(() => { + if (document.body.contains(scriptGz)) document.body.removeChild(scriptGz); + }, 5000); + }); +}; + +export const searchFunds = async (val) => { + if (!val.trim()) return []; + if (typeof window === 'undefined' || typeof document === 'undefined') return []; + const callbackName = `SuggestData_${Date.now()}`; + const url = `https://fundsuggest.eastmoney.com/FundSearch/api/FundSearchAPI.ashx?m=1&key=${encodeURIComponent(val)}&callback=${callbackName}&_=${Date.now()}`; + return new Promise((resolve, reject) => { + window[callbackName] = (data) => { + let results = []; + if (data && data.Datas) { + results = data.Datas.filter(d => + d.CATEGORY === 700 || + d.CATEGORY === '700' || + d.CATEGORYDESC === '基金' + ); + } + delete window[callbackName]; + resolve(results); + }; + const script = document.createElement('script'); + script.src = url; + script.async = true; + script.onload = () => { + if (document.body.contains(script)) document.body.removeChild(script); + }; + script.onerror = () => { + if (document.body.contains(script)) document.body.removeChild(script); + delete window[callbackName]; + reject(new Error('搜索请求失败')); + }; + document.body.appendChild(script); + }); +}; + +export const fetchShanghaiIndexDate = async () => { + if (typeof window === 'undefined' || typeof document === 'undefined') return null; + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = `https://qt.gtimg.cn/q=sh000001&_t=${Date.now()}`; + script.onload = () => { + const data = window.v_sh000001; + let dateStr = null; + if (data) { + const parts = data.split('~'); + if (parts.length > 30) { + dateStr = parts[30].slice(0, 8); + } + } + if (document.body.contains(script)) document.body.removeChild(script); + resolve(dateStr); + }; + script.onerror = () => { + if (document.body.contains(script)) document.body.removeChild(script); + reject(new Error('指数数据加载失败')); + }; + document.body.appendChild(script); + }); +}; + +export const fetchLatestRelease = async () => { + const res = await fetch('https://api.github.com/repos/hzm0321/real-time-fund/releases/latest'); + if (!res.ok) return null; + const data = await res.json(); + return { + tagName: data.tag_name, + body: data.body || '' + }; +}; + +export const submitFeedback = async (formData) => { + const response = await fetch('https://api.web3forms.com/submit', { + method: 'POST', + body: formData + }); + return response.json(); +}; diff --git a/app/components/Common.jsx b/app/components/Common.jsx new file mode 100644 index 0000000..c397934 --- /dev/null +++ b/app/components/Common.jsx @@ -0,0 +1,286 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import zhifubaoImg from "../assets/zhifubao.jpg"; +import weixinImg from "../assets/weixin.jpg"; +import { CalendarIcon, MinusIcon, PlusIcon } from './Icons'; + +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.tz.setDefault('Asia/Shanghai'); + +const TZ = 'Asia/Shanghai'; +const nowInTz = () => dayjs().tz(TZ); +const toTz = (input) => (input ? dayjs.tz(input, TZ) : nowInTz()); +const formatDate = (input) => toTz(input).format('YYYY-MM-DD'); + +export function DatePicker({ value, onChange }) { + const [isOpen, setIsOpen] = useState(false); + const [currentMonth, setCurrentMonth] = useState(() => value ? toTz(value) : nowInTz()); + + useEffect(() => { + const close = () => setIsOpen(false); + if (isOpen) window.addEventListener('click', close); + return () => window.removeEventListener('click', close); + }, [isOpen]); + + const year = currentMonth.year(); + const month = currentMonth.month(); + + const handlePrevMonth = (e) => { + e.stopPropagation(); + setCurrentMonth(currentMonth.subtract(1, 'month').startOf('month')); + }; + + const handleNextMonth = (e) => { + e.stopPropagation(); + setCurrentMonth(currentMonth.add(1, 'month').startOf('month')); + }; + + const handleSelect = (e, day) => { + e.stopPropagation(); + const dateStr = formatDate(`${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`); + + const today = nowInTz().startOf('day'); + const selectedDate = toTz(dateStr).startOf('day'); + + if (selectedDate.isAfter(today)) return; + + onChange(dateStr); + setIsOpen(false); + }; + + const daysInMonth = currentMonth.daysInMonth(); + const firstDayOfWeek = currentMonth.startOf('month').day(); + + const days = []; + for (let i = 0; i < firstDayOfWeek; i++) days.push(null); + for (let i = 1; i <= daysInMonth; i++) days.push(i); + + return ( +
e.stopPropagation()}> +
setIsOpen(!isOpen)} + style={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '0 12px', + height: '40px', + background: 'rgba(0,0,0,0.2)', + borderRadius: '8px', + cursor: 'pointer', + border: '1px solid transparent', + transition: 'all 0.2s' + }} + > + {value || '选择日期'} + +
+ + + {isOpen && ( + +
+ + {year}年 {month + 1}月 + +
+ +
+ {['日', '一', '二', '三', '四', '五', '六'].map(d => ( +
{d}
+ ))} + {days.map((d, i) => { + if (!d) return
; + const dateStr = formatDate(`${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`); + const isSelected = value === dateStr; + const today = nowInTz().startOf('day'); + const current = toTz(dateStr).startOf('day'); + const isToday = current.isSame(today); + const isFuture = current.isAfter(today); + + return ( +
!isFuture && handleSelect(e, d)} + style={{ + height: 28, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: '13px', + borderRadius: '6px', + cursor: isFuture ? 'not-allowed' : 'pointer', + background: isSelected ? 'var(--primary)' : isToday ? 'rgba(255,255,255,0.1)' : 'transparent', + color: isFuture ? 'var(--muted)' : isSelected ? '#000' : 'var(--text)', + fontWeight: isSelected || isToday ? 600 : 400, + opacity: isFuture ? 0.3 : 1 + }} + onMouseEnter={(e) => { + if (!isSelected && !isFuture) e.currentTarget.style.background = 'rgba(255,255,255,0.1)'; + }} + onMouseLeave={(e) => { + if (!isSelected && !isFuture) e.currentTarget.style.background = isToday ? 'rgba(255,255,255,0.1)' : 'transparent'; + }} + > + {d} +
+ ); + })} +
+ + )} + +
+ ); +} + +export function DonateTabs() { + const [method, setMethod] = useState('wechat'); + + return ( +
+
+ + +
+ +
+ {method === 'alipay' ? ( + 支付宝收款码 + ) : ( + 微信收款码 + )} +
+
+ ); +} + +export function NumericInput({ value, onChange, step = 1, min = 0, placeholder }) { + const decimals = String(step).includes('.') ? String(step).split('.')[1].length : 0; + const fmt = (n) => Number(n).toFixed(decimals); + const inc = () => { + const v = parseFloat(value); + const base = isNaN(v) ? 0 : v; + const next = base + step; + onChange(fmt(next)); + }; + const dec = () => { + const v = parseFloat(value); + const base = isNaN(v) ? 0 : v; + const next = Math.max(min, base - step); + onChange(fmt(next)); + }; + return ( +
+ onChange(e.target.value)} + placeholder={placeholder} + style={{ width: '100%', paddingRight: 56 }} + /> +
+ + +
+
+ ); +} + +export function Stat({ label, value, delta }) { + const dir = delta > 0 ? 'up' : delta < 0 ? 'down' : ''; + return ( +
+ {label} + {value} +
+ ); +} diff --git a/app/components/Icons.jsx b/app/components/Icons.jsx new file mode 100644 index 0000000..0699f9a --- /dev/null +++ b/app/components/Icons.jsx @@ -0,0 +1,190 @@ +'use client'; + +export function PlusIcon(props) { + return ( + + + + ); +} + +export function UpdateIcon(props) { + return ( + + + + + + ); +} + +export function TrashIcon(props) { + return ( + + + + + + + ); +} + +export function SettingsIcon(props) { + return ( + + + + + ); +} + +export function CloudIcon(props) { + return ( + + + + ); +} + +export function RefreshIcon(props) { + return ( + + + + + + + ); +} + +export function ChevronIcon(props) { + return ( + + + + ); +} + +export function SortIcon(props) { + return ( + + + + ); +} + +export function UserIcon(props) { + return ( + + + + + ); +} + +export function LogoutIcon(props) { + return ( + + + + + + ); +} + +export function LoginIcon(props) { + return ( + + + + + + ); +} + +export function MailIcon(props) { + return ( + + + + + ); +} + +export function GridIcon(props) { + return ( + + + + + + + ); +} + +export function CloseIcon(props) { + return ( + + + + ); +} + +export function ExitIcon(props) { + return ( + + + + ); +} + +export function ListIcon(props) { + return ( + + + + ); +} + +export function DragIcon(props) { + return ( + + + + ); +} + +export function FolderPlusIcon(props) { + return ( + + + + ); +} + +export function StarIcon({ filled, ...props }) { + return ( + + + + ); +} + +export function CalendarIcon(props) { + return ( + + + + + + + ); +} + +export function MinusIcon(props) { + return ( + + + + ); +} diff --git a/app/page.jsx b/app/page.jsx index 02b4f9c..c503600 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -4,15 +4,16 @@ import { useEffect, useRef, useState, useMemo, useLayoutEffect, useCallback } fr import { motion, AnimatePresence, Reorder } from 'framer-motion'; import { createAvatar } from '@dicebear/core'; import { glass } from '@dicebear/collection'; -import Announcement from "./components/Announcement"; -import zhifubaoImg from "./assets/zhifubao.jpg"; -import weixinImg from "./assets/weixin.jpg"; -import githubImg from "./assets/github.svg"; -import { supabase } from './lib/supabase'; -import packageJson from '../package.json'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; +import Announcement from "./components/Announcement"; +import { DatePicker, DonateTabs, NumericInput, Stat } from "./components/Common"; +import { ChevronIcon, CloseIcon, CloudIcon, DragIcon, ExitIcon, GridIcon, ListIcon, LoginIcon, LogoutIcon, MailIcon, PlusIcon, RefreshIcon, SettingsIcon, SortIcon, StarIcon, TrashIcon, UpdateIcon, UserIcon } from "./components/Icons"; +import githubImg from "./assets/github.svg"; +import { supabase } from './lib/supabase'; +import { fetchFundData, fetchLatestRelease, fetchShanghaiIndexDate, fetchSmartFundNetValue, searchFunds, submitFeedback } from './api/fund'; +import packageJson from '../package.json'; dayjs.extend(utc); dayjs.extend(timezone); @@ -23,536 +24,6 @@ const nowInTz = () => dayjs().tz(TZ); const toTz = (input) => (input ? dayjs.tz(input, TZ) : nowInTz()); const formatDate = (input) => toTz(input).format('YYYY-MM-DD'); -// 全局 JSONP/Script 加载辅助函数 -const loadScript = (url) => { - return new Promise((resolve, reject) => { - if (typeof document === 'undefined') return resolve(); - const script = document.createElement('script'); - script.src = url; - script.async = true; - script.onload = () => { - if (document.body.contains(script)) document.body.removeChild(script); - resolve(); - }; - script.onerror = () => { - if (document.body.contains(script)) document.body.removeChild(script); - reject(new Error('加载失败')); - }; - document.body.appendChild(script); - }); -}; - -// 获取指定日期的基金净值 -const fetchFundNetValue = async (code, date) => { - // 使用东方财富 F10 接口获取历史净值 HTML - const url = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${code}&page=1&per=1&sdate=${date}&edate=${date}`; - try { - await loadScript(url); - if (window.apidata && window.apidata.content) { - const content = window.apidata.content; - if (content.includes('暂无数据')) return null; - - // 解析 HTML 表格 - // 格式: 日期单位净值... - const rows = content.split(''); - for (const row of rows) { - if (row.includes(`${date}`)) { - // 找到对应日期的行,提取单元格 - const cells = row.match(/]*>(.*?)<\/td>/g); - if (cells && cells.length >= 2) { - // 第二列是单位净值 (cells[1]) - const valStr = cells[1].replace(/<[^>]+>/g, ''); - const val = parseFloat(valStr); - return isNaN(val) ? null : val; - } - } - } - } - return null; - } catch (e) { - console.error('获取净值失败', e); - return null; - } -}; - -const fetchSmartFundNetValue = async (code, startDate) => { - const today = nowInTz().startOf('day'); - - let current = toTz(startDate).startOf('day'); - - for (let i = 0; i < 30; i++) { - if (current.isAfter(today)) break; - - const dateStr = current.format('YYYY-MM-DD'); - const val = await fetchFundNetValue(code, dateStr); - if (val !== null) { - return { date: dateStr, value: val }; - } - - current = current.add(1, 'day'); - } - return null; -}; - -function PlusIcon(props) { - return ( - - - - ); -} - -function UpdateIcon(props) { - return ( - - - - - - ); -} - -function TrashIcon(props) { - return ( - - - - - - - ); -} - -function SettingsIcon(props) { - return ( - - - - - ); -} - -function CloudIcon(props) { - return ( - - - - ); -} - -function RefreshIcon(props) { - return ( - - - - - - - ); -} - -function ChevronIcon(props) { - return ( - - - - ); -} - -function SortIcon(props) { - return ( - - - - ); -} - -function UserIcon(props) { - return ( - - - - - ); -} - -function LogoutIcon(props) { - return ( - - - - - - ); -} - -function LoginIcon(props) { - return ( - - - - - - ); -} - -function MailIcon(props) { - return ( - - - - - ); -} - -function GridIcon(props) { - return ( - - - - - - - ); -} - -function CloseIcon(props) { - return ( - - - - ); -} - -function ExitIcon(props) { - return ( - - - - ); -} - -function ListIcon(props) { - return ( - - - - ); -} - -function DragIcon(props) { - return ( - - - - ); -} - -function FolderPlusIcon(props) { - return ( - - - - ); -} - -function StarIcon({ filled, ...props }) { - return ( - - - - ); -} - -function CalendarIcon(props) { - return ( - - - - - - - ); -} - -function DatePicker({ value, onChange }) { - const [isOpen, setIsOpen] = useState(false); - const [currentMonth, setCurrentMonth] = useState(() => value ? toTz(value) : nowInTz()); - - // 点击外部关闭 - useEffect(() => { - const close = () => setIsOpen(false); - if (isOpen) window.addEventListener('click', close); - return () => window.removeEventListener('click', close); - }, [isOpen]); - - const year = currentMonth.year(); - const month = currentMonth.month(); - - const handlePrevMonth = (e) => { - e.stopPropagation(); - setCurrentMonth(currentMonth.subtract(1, 'month').startOf('month')); - }; - - const handleNextMonth = (e) => { - e.stopPropagation(); - setCurrentMonth(currentMonth.add(1, 'month').startOf('month')); - }; - - const handleSelect = (e, day) => { - e.stopPropagation(); - const dateStr = formatDate(`${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`); - - const today = nowInTz().startOf('day'); - const selectedDate = toTz(dateStr).startOf('day'); - - if (selectedDate.isAfter(today)) return; - - onChange(dateStr); - setIsOpen(false); - }; - - // 生成日历数据 - const daysInMonth = currentMonth.daysInMonth(); - const firstDayOfWeek = currentMonth.startOf('month').day(); - - const days = []; - for (let i = 0; i < firstDayOfWeek; i++) days.push(null); - for (let i = 1; i <= daysInMonth; i++) days.push(i); - - return ( -
e.stopPropagation()}> -
setIsOpen(!isOpen)} - style={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: '0 12px', - height: '40px', - background: 'rgba(0,0,0,0.2)', - borderRadius: '8px', - cursor: 'pointer', - border: '1px solid transparent', - transition: 'all 0.2s' - }} - > - {value || '选择日期'} - -
- - - {isOpen && ( - -
- - {year}年 {month + 1}月 - -
- -
- {['日', '一', '二', '三', '四', '五', '六'].map(d => ( -
{d}
- ))} - {days.map((d, i) => { - if (!d) return
; - const dateStr = formatDate(`${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`); - const isSelected = value === dateStr; - const today = nowInTz().startOf('day'); - const current = toTz(dateStr).startOf('day'); - const isToday = current.isSame(today); - const isFuture = current.isAfter(today); - - return ( -
!isFuture && handleSelect(e, d)} - style={{ - height: 28, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - fontSize: '13px', - borderRadius: '6px', - cursor: isFuture ? 'not-allowed' : 'pointer', - background: isSelected ? 'var(--primary)' : isToday ? 'rgba(255,255,255,0.1)' : 'transparent', - color: isFuture ? 'var(--muted)' : isSelected ? '#000' : 'var(--text)', - fontWeight: isSelected || isToday ? 600 : 400, - opacity: isFuture ? 0.3 : 1 - }} - onMouseEnter={(e) => { - if (!isSelected && !isFuture) e.currentTarget.style.background = 'rgba(255,255,255,0.1)'; - }} - onMouseLeave={(e) => { - if (!isSelected && !isFuture) e.currentTarget.style.background = isToday ? 'rgba(255,255,255,0.1)' : 'transparent'; - }} - > - {d} -
- ); - })} -
- - )} - -
- ); -} - -function DonateTabs() { - const [method, setMethod] = useState('wechat'); // alipay, wechat - - return ( -
-
- - -
- -
- {method === 'alipay' ? ( - 支付宝收款码 - ) : ( - 微信收款码 - )} -
-
- ); -} - -function MinusIcon(props) { - return ( - - - - ); -} - -function NumericInput({ value, onChange, step = 1, min = 0, placeholder }) { - const decimals = String(step).includes('.') ? String(step).split('.')[1].length : 0; - const fmt = (n) => Number(n).toFixed(decimals); - const inc = () => { - const v = parseFloat(value); - const base = isNaN(v) ? 0 : v; - const next = base + step; - onChange(fmt(next)); - }; - const dec = () => { - const v = parseFloat(value); - const base = isNaN(v) ? 0 : v; - const next = Math.max(min, base - step); - onChange(fmt(next)); - }; - return ( -
- onChange(e.target.value)} - placeholder={placeholder} - style={{ width: '100%', paddingRight: 56 }} - /> -
- - -
-
- ); -} - -function Stat({ label, value, delta }) { - const dir = delta > 0 ? 'up' : delta < 0 ? 'down' : ''; - return ( -
- {label} - {value} -
- ); -} - function FeedbackModal({ onClose, user }) { const [submitting, setSubmitting] = useState(false); const [succeeded, setSucceeded] = useState(false); @@ -574,12 +45,7 @@ function FeedbackModal({ onClose, user }) { formData.append("subject", "基估宝 - 用户反馈"); try { - const response = await fetch("https://api.web3forms.com/submit", { - method: "POST", - body: formData - }); - - const data = await response.json(); + const data = await submitFeedback(formData); if (data.success) { setSucceeded(true); } else { @@ -2426,16 +1892,13 @@ export default function HomePage() { useEffect(() => { const checkUpdate = async () => { try { - const res = await fetch('https://api.github.com/repos/hzm0321/real-time-fund/releases/latest'); - if (!res.ok) return; - const data = await res.json(); - if (data.tag_name) { - const remoteVersion = data.tag_name.replace(/^v/, ''); - if (remoteVersion !== packageJson.version) { - setHasUpdate(true); - setLatestVersion(remoteVersion); - setUpdateContent(data.body || ''); - } + const data = await fetchLatestRelease(); + if (!data?.tagName) return; + const remoteVersion = data.tagName.replace(/^v/, ''); + if (remoteVersion !== packageJson.version) { + setHasUpdate(true); + setLatestVersion(remoteVersion); + setUpdateContent(data.body || ''); } } catch (e) { console.error('Check update failed:', e); @@ -2476,7 +1939,7 @@ export default function HomePage() { }, [swipedFundCode]); // 检查交易日状态 - const checkTradingDay = () => { + const checkTradingDay = async () => { const now = nowInTz(); const isWeekend = now.day() === 0 || now.day() === 6; @@ -2489,39 +1952,26 @@ export default function HomePage() { // 工作日通过上证指数判断是否为节假日 // 接口返回示例: v_sh000001="1~上证指数~...~20260205150000~..." // 第30位是时间字段 - const script = document.createElement('script'); - script.src = `https://qt.gtimg.cn/q=sh000001&_t=${Date.now()}`; - script.onload = () => { - const data = window.v_sh000001; - if (data) { - const parts = data.split('~'); - if (parts.length > 30) { - const dateStr = parts[30].slice(0, 8); // 20260205 - const currentStr = todayStr.replace(/-/g, ''); - - if (dateStr === currentStr) { - setIsTradingDay(true); // 日期匹配,确认为交易日 - } else { - // 日期不匹配 (显示的是旧数据) - // 如果已经过了 09:30 还是旧数据,说明今天休市 - const minutes = now.hour() * 60 + now.minute(); - if (minutes >= 9 * 60 + 30) { - setIsTradingDay(false); - } else { - // 9:30 之前,即使是旧数据,也默认是交易日(盘前) - setIsTradingDay(true); - } - } + try { + const dateStr = await fetchShanghaiIndexDate(); + if (!dateStr) { + setIsTradingDay(!isWeekend); + return; + } + const currentStr = todayStr.replace(/-/g, ''); + if (dateStr === currentStr) { + setIsTradingDay(true); + } else { + const minutes = now.hour() * 60 + now.minute(); + if (minutes >= 9 * 60 + 30) { + setIsTradingDay(false); + } else { + setIsTradingDay(true); } } - document.body.removeChild(script); - }; - script.onerror = () => { - document.body.removeChild(script); - // 接口失败,降级为仅判断周末 + } catch (e) { setIsTradingDay(!isWeekend); - }; - document.body.appendChild(script); + } }; useEffect(() => { @@ -3288,351 +2738,15 @@ export default function HomePage() { }; }, [funds, refreshMs]); - // --- 辅助:JSONP 数据抓取逻辑 --- - const loadScript = (url) => { - return new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = url; - script.async = true; - script.onload = () => { - document.body.removeChild(script); - resolve(); - }; - script.onerror = () => { - document.body.removeChild(script); - reject(new Error('数据加载失败')); - }; - document.body.appendChild(script); - }); - }; - - // 当估值接口无法获取数据时,使用腾讯接口获取基金基本信息和净值(回退方案) - const fetchFundDataFallback = async (c) => { - return new Promise(async (resolve, reject) => { - // 先通过东方财富搜索接口获取基金名称 - const searchCallbackName = `SuggestData_fallback_${Date.now()}`; - const searchUrl = `https://fundsuggest.eastmoney.com/FundSearch/api/FundSearchAPI.ashx?m=1&key=${encodeURIComponent(c)}&callback=${searchCallbackName}&_=${Date.now()}`; - - let fundName = ''; - - try { - await new Promise((resSearch, rejSearch) => { - window[searchCallbackName] = (data) => { - if (data && data.Datas && data.Datas.length > 0) { - const found = data.Datas.find(d => d.CODE === c); - if (found) { - fundName = found.NAME || found.SHORTNAME || ''; - } - } - delete window[searchCallbackName]; - resSearch(); - }; - - const script = document.createElement('script'); - script.src = searchUrl; - script.async = true; - script.onload = () => { - if (document.body.contains(script)) document.body.removeChild(script); - }; - script.onerror = () => { - if (document.body.contains(script)) document.body.removeChild(script); - delete window[searchCallbackName]; - rejSearch(new Error('搜索接口失败')); - }; - document.body.appendChild(script); - - // 超时处理 - setTimeout(() => { - if (window[searchCallbackName]) { - delete window[searchCallbackName]; - resSearch(); - } - }, 3000); - }); - } catch (e) { - // 搜索失败,继续尝试腾讯接口 - } - - // 使用腾讯接口获取净值数据 - const tUrl = `https://qt.gtimg.cn/q=jj${c}`; - const tScript = document.createElement('script'); - tScript.src = tUrl; - tScript.onload = () => { - const v = window[`v_jj${c}`]; - if (v && v.length > 5) { - const p = v.split('~'); - // p[1]: 基金名称, p[5]: 单位净值, p[7]: 涨跌幅, p[8]: 净值日期 - const name = fundName || p[1] || `未知基金(${c})`; - const dwjz = p[5]; - const zzl = parseFloat(p[7]); - const jzrq = p[8] ? p[8].slice(0, 10) : ''; - - if (dwjz) { - // 成功获取净值数据 - resolve({ - code: c, - name: name, - dwjz: dwjz, - gsz: null, // 无估值数据 - gztime: null, - jzrq: jzrq, - gszzl: null, // 无估值涨跌幅 - zzl: !isNaN(zzl) ? zzl : null, - noValuation: true, // 标记为无估值数据 - holdings: [] - }); - } else { - reject(new Error('未能获取到基金数据')); - } - } else { - reject(new Error('未能获取到基金数据')); - } - if (document.body.contains(tScript)) document.body.removeChild(tScript); - }; - tScript.onerror = () => { - if (document.body.contains(tScript)) document.body.removeChild(tScript); - reject(new Error('基金数据加载失败')); - }; - document.body.appendChild(tScript); - }); - }; - - const fetchFundData = async (c) => { - return new Promise(async (resolve, reject) => { - // 腾讯接口识别逻辑优化 - const getTencentPrefix = (code) => { - if (code.startsWith('6') || code.startsWith('9')) return 'sh'; - if (code.startsWith('0') || code.startsWith('3')) return 'sz'; - if (code.startsWith('4') || code.startsWith('8')) return 'bj'; - return 'sz'; - }; - - const gzUrl = `https://fundgz.1234567.com.cn/js/${c}.js?rt=${Date.now()}`; - - // 使用更安全的方式处理全局回调,避免并发覆盖 - const currentCallback = `jsonpgz_${c}_${Math.random().toString(36).slice(2, 7)}`; - - // 动态拦截并处理 jsonpgz 回调 - const scriptGz = document.createElement('script'); - // 东方财富接口固定调用 jsonpgz,我们通过修改全局变量临时捕获它 - scriptGz.src = gzUrl; - - const originalJsonpgz = window.jsonpgz; - window.jsonpgz = (json) => { - window.jsonpgz = originalJsonpgz; // 立即恢复 - if (!json || typeof json !== 'object') { - // 估值数据无法获取时,尝试使用腾讯接口获取基金基本信息和净值 - fetchFundDataFallback(c).then(resolve).catch(reject); - return; - } - const gszzlNum = Number(json.gszzl); - const gzData = { - code: json.fundcode, - name: json.name, - dwjz: json.dwjz, - gsz: json.gsz, - gztime: json.gztime, - jzrq: json.jzrq, - gszzl: Number.isFinite(gszzlNum) ? gszzlNum : json.gszzl - }; - - // 并行获取:1. 腾讯接口获取最新确权净值和涨跌幅;2. 东方财富接口获取持仓 - const tencentPromise = new Promise((resolveT) => { - const tUrl = `https://qt.gtimg.cn/q=jj${c}`; - const tScript = document.createElement('script'); - tScript.src = tUrl; - tScript.onload = () => { - const v = window[`v_jj${c}`]; - if (v) { - const p = v.split('~'); - // p[5]: 单位净值, p[7]: 涨跌幅, p[8]: 净值日期 - resolveT({ - dwjz: p[5], - zzl: parseFloat(p[7]), - jzrq: p[8] ? p[8].slice(0, 10) : '' - }); - } else { - resolveT(null); - } - if (document.body.contains(tScript)) document.body.removeChild(tScript); - }; - tScript.onerror = () => { - if (document.body.contains(tScript)) document.body.removeChild(tScript); - resolveT(null); - }; - document.body.appendChild(tScript); - }); - - const holdingsPromise = new Promise((resolveH) => { - const holdingsUrl = `https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code=${c}&topline=10&year=&month=&_=${Date.now()}`; - loadScript(holdingsUrl).then(async () => { - let holdings = []; - const html = window.apidata?.content || ''; - const headerRow = (html.match(/[\s\S]*?<\/thead>/i) || [])[0] || ''; - const headerCells = (headerRow.match(/([\s\S]*?)<\/th>/gi) || []).map(th => th.replace(/<[^>]*>/g, '').trim()); - let idxCode = -1, idxName = -1, idxWeight = -1; - headerCells.forEach((h, i) => { - const t = h.replace(/\s+/g, ''); - if (idxCode < 0 && (t.includes('股票代码') || t.includes('证券代码'))) idxCode = i; - if (idxName < 0 && (t.includes('股票名称') || t.includes('证券名称'))) idxName = i; - if (idxWeight < 0 && (t.includes('占净值比例') || t.includes('占比'))) idxWeight = i; - }); - const rows = html.match(//i) || []; - const dataRows = rows.length ? rows[0].match(//gi) || [] : html.match(//gi) || []; - for (const r of dataRows) { - const tds = (r.match(/([\s\S]*?)<\/td>/gi) || []).map(td => td.replace(/<[^>]*>/g, '').trim()); - if (!tds.length) continue; - let code = ''; - let name = ''; - let weight = ''; - if (idxCode >= 0 && tds[idxCode]) { - const m = tds[idxCode].match(/(\d{6})/); - code = m ? m[1] : tds[idxCode]; - } else { - const codeIdx = tds.findIndex(txt => /^\d{6}$/.test(txt)); - if (codeIdx >= 0) code = tds[codeIdx]; - } - if (idxName >= 0 && tds[idxName]) { - name = tds[idxName]; - } else if (code) { - const i = tds.findIndex(txt => txt && txt !== code && !/%$/.test(txt)); - name = i >= 0 ? tds[i] : ''; - } - if (idxWeight >= 0 && tds[idxWeight]) { - const wm = tds[idxWeight].match(/([\d.]+)\s*%/); - weight = wm ? `${wm[1]}%` : tds[idxWeight]; - } else { - const wIdx = tds.findIndex(txt => /\d+(?:\.\d+)?\s*%/.test(txt)); - weight = wIdx >= 0 ? tds[wIdx].match(/([\d.]+)\s*%/)?.[1] + '%' : ''; - } - if (code || name || weight) { - holdings.push({ code, name, weight, change: null }); - } - } - holdings = holdings.slice(0, 10); - const needQuotes = holdings.filter(h => /^\d{6}$/.test(h.code) || /^\d{5}$/.test(h.code)); - if (needQuotes.length) { - try { - const tencentCodes = needQuotes.map(h => { - const cd = String(h.code || ''); - if (/^\d{6}$/.test(cd)) { - const pfx = cd.startsWith('6') || cd.startsWith('9') ? 'sh' : ((cd.startsWith('4') || cd.startsWith('8')) ? 'bj' : 'sz'); - return `s_${pfx}${cd}`; - } - if (/^\d{5}$/.test(cd)) { - return `s_hk${cd}`; - } - return null; - }).filter(Boolean).join(','); - if (!tencentCodes) { - resolveH(holdings); - return; - } - const quoteUrl = `https://qt.gtimg.cn/q=${tencentCodes}`; - await new Promise((resQuote) => { - const scriptQuote = document.createElement('script'); - scriptQuote.src = quoteUrl; - scriptQuote.onload = () => { - needQuotes.forEach(h => { - const cd = String(h.code || ''); - let varName = ''; - if (/^\d{6}$/.test(cd)) { - const pfx = cd.startsWith('6') || cd.startsWith('9') ? 'sh' : ((cd.startsWith('4') || cd.startsWith('8')) ? 'bj' : 'sz'); - varName = `v_s_${pfx}${cd}`; - } else if (/^\d{5}$/.test(cd)) { - varName = `v_s_hk${cd}`; - } else { - return; - } - const dataStr = window[varName]; - if (dataStr) { - const parts = dataStr.split('~'); - if (parts.length > 5) { - h.change = parseFloat(parts[5]); - } - } - }); - if (document.body.contains(scriptQuote)) document.body.removeChild(scriptQuote); - resQuote(); - }; - scriptQuote.onerror = () => { - if (document.body.contains(scriptQuote)) document.body.removeChild(scriptQuote); - resQuote(); - }; - document.body.appendChild(scriptQuote); - }); - } catch (e) { } - } - resolveH(holdings); - }).catch(() => resolveH([])); - }); - - Promise.all([tencentPromise, holdingsPromise]).then(([tData, holdings]) => { - if (tData) { - // 如果腾讯数据的日期更新(或相同),优先使用腾讯的净值数据(通常更准且包含涨跌幅) - if (tData.jzrq && (!gzData.jzrq || tData.jzrq >= gzData.jzrq)) { - gzData.dwjz = tData.dwjz; - gzData.jzrq = tData.jzrq; - gzData.zzl = tData.zzl; // 真实涨跌幅 - } - } - resolve({ ...gzData, holdings }); - }); - }; - - scriptGz.onerror = () => { - window.jsonpgz = originalJsonpgz; - if (document.body.contains(scriptGz)) document.body.removeChild(scriptGz); - reject(new Error('基金数据加载失败')); - }; - - document.body.appendChild(scriptGz); - // 加载完立即移除脚本 - setTimeout(() => { - if (document.body.contains(scriptGz)) document.body.removeChild(scriptGz); - }, 5000); - }); - }; - const performSearch = async (val) => { if (!val.trim()) { setSearchResults([]); return; } setIsSearching(true); - // 使用 JSONP 方式获取数据,添加 callback 参数 - const callbackName = `SuggestData_${Date.now()}`; - const url = `https://fundsuggest.eastmoney.com/FundSearch/api/FundSearchAPI.ashx?m=1&key=${encodeURIComponent(val)}&callback=${callbackName}&_=${Date.now()}`; - try { - await new Promise((resolve, reject) => { - window[callbackName] = (data) => { - if (data && data.Datas) { - // 过滤出基金类型的数据 (CATEGORY 为 700 是公募基金) - const fundsOnly = data.Datas.filter(d => - d.CATEGORY === 700 || - d.CATEGORY === "700" || - d.CATEGORYDESC === "基金" - ); - setSearchResults(fundsOnly); - } - delete window[callbackName]; - resolve(); - }; - - const script = document.createElement('script'); - script.src = url; - script.async = true; - script.onload = () => { - if (document.body.contains(script)) document.body.removeChild(script); - }; - script.onerror = () => { - if (document.body.contains(script)) document.body.removeChild(script); - delete window[callbackName]; - reject(new Error('搜索请求失败')); - }; - document.body.appendChild(script); - }); + const fundsOnly = await searchFunds(val); + setSearchResults(fundsOnly); } catch (e) { console.error('搜索失败', e); } finally { diff --git a/next.config.js b/next.config.js index 91ef62f..505c9f3 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + reactCompiler: true, }; module.exports = nextConfig; diff --git a/package-lock.json b/package-lock.json index 4568666..fce2f70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,10 +17,47 @@ "react": "18.3.1", "react-dom": "18.3.1" }, + "devDependencies": { + "babel-plugin-react-compiler": "^1.0.0" + }, "engines": { "node": ">=20.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@dicebear/adventurer": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/@dicebear/adventurer/-/adventurer-9.3.1.tgz", @@ -1178,6 +1215,16 @@ "@types/node": "*" } }, + "node_modules/babel-plugin-react-compiler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", + "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", diff --git a/package.json b/package.json index f4e1680..6040243 100644 --- a/package.json +++ b/package.json @@ -19,5 +19,8 @@ }, "engines": { "node": ">=20.9.0" + }, + "devDependencies": { + "babel-plugin-react-compiler": "^1.0.0" } }