diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..5cac8dc --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "next/core-web-vitals" + ] +} \ No newline at end of file diff --git a/app/components/AddFundToGroupModal.jsx b/app/components/AddFundToGroupModal.jsx new file mode 100644 index 0000000..87dfb8f --- /dev/null +++ b/app/components/AddFundToGroupModal.jsx @@ -0,0 +1,90 @@ +'use client'; + +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import { CloseIcon, PlusIcon } from './Icons'; + +export default function AddFundToGroupModal({ allFunds, currentGroupCodes, onClose, onAdd }) { + const [selected, setSelected] = useState(new Set()); + + const availableFunds = (allFunds || []).filter(f => !(currentGroupCodes || []).includes(f.code)); + + const toggleSelect = (code) => { + setSelected(prev => { + const next = new Set(prev); + if (next.has(code)) next.delete(code); + else next.add(code); + return next; + }); + }; + + return ( + + e.stopPropagation()} + > +
+
+ + 添加基金到分组 +
+ +
+ +
+ {availableFunds.length === 0 ? ( +
+

所有基金已在该分组中

+
+ ) : ( +
+ {availableFunds.map((fund) => ( +
toggleSelect(fund.code)} + style={{ cursor: 'pointer' }} + > +
+ {selected.has(fund.code) &&
} +
+
+
{fund.name}
+
#{fund.code}
+
+
+ ))} +
+ )} +
+ +
+ + +
+ + + ); +} diff --git a/app/components/AddResultModal.jsx b/app/components/AddResultModal.jsx new file mode 100644 index 0000000..491baa2 --- /dev/null +++ b/app/components/AddResultModal.jsx @@ -0,0 +1,53 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { CloseIcon, SettingsIcon } from './Icons'; + +export default function AddResultModal({ failures, onClose }) { + return ( + + e.stopPropagation()} + > +
+
+ + 部分基金添加失败 +
+ +
+
+ 未获取到估值数据的基金如下: +
+
+ {failures.map((it, idx) => ( +
+ {it.name || '未知名称'} +
+ #{it.code} +
+
+ ))} +
+
+ +
+
+
+ ); +} diff --git a/app/components/CloudConfigModal.jsx b/app/components/CloudConfigModal.jsx new file mode 100644 index 0000000..d62781a --- /dev/null +++ b/app/components/CloudConfigModal.jsx @@ -0,0 +1,54 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { CloseIcon, CloudIcon } from './Icons'; + +export default function CloudConfigModal({ onConfirm, onCancel, type = 'empty' }) { + const isConflict = type === 'conflict'; + return ( + + e.stopPropagation()} + > +
+
+ + {isConflict ? '发现配置冲突' : '云端暂无配置'} +
+ {!isConflict && ( + + )} +
+

+ {isConflict + ? '检测到本地配置与云端不一致,请选择操作:' + : '是否将本地配置同步到云端?'} +

+
+ + +
+
+
+ ); +} diff --git a/app/components/ConfirmModal.jsx b/app/components/ConfirmModal.jsx new file mode 100644 index 0000000..ea32584 --- /dev/null +++ b/app/components/ConfirmModal.jsx @@ -0,0 +1,43 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { TrashIcon } from './Icons'; + +export default function ConfirmModal({ title, message, onConfirm, onCancel, confirmText = "确定删除" }) { + return ( + { + e.stopPropagation(); + onCancel(); + }} + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + exit={{ opacity: 0 }} + style={{ zIndex: 10002 }} + > + e.stopPropagation()} + > +
+ + {title} +
+

+ {message} +

+
+ + +
+
+
+ ); +} diff --git a/app/components/DonateModal.jsx b/app/components/DonateModal.jsx new file mode 100644 index 0000000..1a41e40 --- /dev/null +++ b/app/components/DonateModal.jsx @@ -0,0 +1,37 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { CloseIcon } from './Icons'; +import { DonateTabs } from './Common'; + +export default function DonateModal({ onClose }) { + return ( +
+ e.stopPropagation()} + > +
+
+ ☕ 请作者喝杯咖啡 +
+ +
+ +
+ +
+ +
+ 感谢您的支持!您的鼓励是我持续维护和更新的动力。 +
+
+
+ ); +} diff --git a/app/components/FeedbackModal.jsx b/app/components/FeedbackModal.jsx new file mode 100644 index 0000000..2290d20 --- /dev/null +++ b/app/components/FeedbackModal.jsx @@ -0,0 +1,148 @@ +'use client'; + +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import { CloseIcon, SettingsIcon } from './Icons'; +import { submitFeedback } from '../api/fund'; + +export default function FeedbackModal({ onClose, user, onOpenWeChat }) { + const [submitting, setSubmitting] = useState(false); + const [succeeded, setSucceeded] = useState(false); + const [error, setError] = useState(""); + + const onSubmit = async (e) => { + e.preventDefault(); + setSubmitting(true); + setError(""); + + const formData = new FormData(e.target); + const nickname = formData.get("nickname")?.trim(); + if (!nickname) { + formData.set("nickname", "匿名"); + } + + // Web3Forms Access Key + formData.append("access_key", process.env.NEXT_PUBLIC_WEB3FORMS_ACCESS_KEY || ''); + formData.append("subject", "基估宝 - 用户反馈"); + + try { + const data = await submitFeedback(formData); + if (data.success) { + setSucceeded(true); + } else { + setError(data.message || "提交失败,请稍后再试"); + } + } catch (err) { + setError("网络错误,请检查您的连接"); + } finally { + setSubmitting(false); + } + }; + + return ( + + e.stopPropagation()} + > +
+
+ + 意见反馈 +
+ +
+ + {succeeded ? ( +
+
🎉
+

感谢您的反馈!

+

我们已收到您的建议,会尽快查看。

+ +
+ ) : ( +
+
+ + +
+ +
+ +