6 Commits
v0.2.9 ... main

Author SHA1 Message Date
hzm
57c575b000 docs: 补充对于 github 登录配置文档 2026-03-24 13:55:44 +08:00
hzm
4a3431cc1e Merge remote-tracking branch 'origin/main' 2026-03-23 21:44:33 +08:00
hzm
12667521a6 feat: 增加当日收益率切换 2026-03-23 21:31:23 +08:00
hzm
84d021506b Merge remote-tracking branch 'origin/main' 2026-03-23 10:47:03 +08:00
hzm
e5b00515d3 fix: 修复数字动画引起的金额显示不正确问题 2026-03-19 20:04:35 +08:00
hzm
6fad4ee487 feat: 去掉加仓弹框最小金额限制 2026-03-19 19:47:03 +08:00
5 changed files with 141 additions and 26 deletions

View File

@@ -84,6 +84,12 @@
NEXT_PUBLIC_Supabase_URLSupabase控制台 → Project Settings → General → Project ID NEXT_PUBLIC_Supabase_URLSupabase控制台 → Project Settings → General → Project ID
NEXT_PUBLIC_Supabase_ANON_KEY Supabase控制台 → Project Settings → API Keys → Publishable key NEXT_PUBLIC_Supabase_ANON_KEY Supabase控制台 → Project Settings → API Keys → Publishable key
示例:
```
NEXT_PUBLIC_SUPABASE_URL=https://abcdefghijklmnop.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=xxxxxx
```
2. 邮件数量修改 2. 邮件数量修改
Supabase 免费项目自带每小时2条邮件服务。如果觉得额度不够可以改成自己的邮箱SMTP。修改路径在 Supabase控制台 → Authentication → Email → SMTP Settings。 Supabase 免费项目自带每小时2条邮件服务。如果觉得额度不够可以改成自己的邮箱SMTP。修改路径在 Supabase控制台 → Authentication → Email → SMTP Settings。
@@ -102,7 +108,47 @@
在 Supabase控制台 → Authentication → Sign In / Providers → Auth Providers → email 中,关闭 **Confirm email** 选项。这样用户注册后就不需要再去邮箱点击确认链接了,直接使用验证码登录即可。 在 Supabase控制台 → Authentication → Sign In / Providers → Auth Providers → email 中,关闭 **Confirm email** 选项。这样用户注册后就不需要再去邮箱点击确认链接了,直接使用验证码登录即可。
6. 目前项目用到的 sql 语句,查看项目 /doc/supabase.sql 文件。 6. 配置 GitHub 登录(可选)
如需支持 GitHub OAuth 登录,需完成以下配置:
**第一步:在 GitHub 创建 OAuth App**
- 访问 GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
- 填写信息:
- Application name自定义应用名称
- Homepage URL你的应用地址如 `https://hzm0321.github.io/real-time-fund/`
- Authorization callback URL`https://<your-supabase-project-id>.supabase.co/auth/v1/callback`
- 创建后获取 **Client ID** 和 **Client Secret**
**第二步:在 Supabase 启用 GitHub Provider**
- Supabase控制台 → Authentication → Sign In / Providers → Auth Providers → GitHub
- 开启 **GitHub** 开关
- 填入 GitHub OAuth App 的 **Client ID** 和 **Client Secret**
- 点击 **Save** 保存
**第三步:配置站点 URL重要**
- Supabase控制台 → Authentication → URL Configuration
- **Site URL**:设置为你的应用主域名(如 `https://hzm0321.github.io/`
- **Redirect URLs**:添加你的应用完整路径(如 `https://hzm0321.github.io/real-time-fund/`
配置完成后,用户即可通过 GitHub 账号一键登录。
7. 执行数据库初始化 SQL
项目需要创建 `user_configs` 表及相关策略才能使用云端同步功能。SQL 语句位于项目 `/doc/supabase.sql` 文件。
**执行步骤:**
- Supabase控制台 → SQL Editor → New query
- 复制 `/doc/supabase.sql` 文件中的全部内容,粘贴到编辑器
- 点击 **Run** 执行
SQL 脚本将完成以下操作:
- 创建 `user_configs` 表(存储用户配置数据)
- 启用行级安全RLS确保用户只能访问自己的数据
- 创建 SELECT / INSERT / UPDATE 策略
- 创建 `update_user_config_partial` 函数(用于增量更新配置)
执行成功后,可在 Table Editor 中看到 `user_configs` 表。
更多 Supabase 相关内容查阅官方文档。 更多 Supabase 相关内容查阅官方文档。

View File

@@ -53,6 +53,7 @@ export default function FundCard({
dcaPlans, dcaPlans,
holdings, holdings,
percentModes, percentModes,
todayPercentModes,
valuationSeries, valuationSeries,
collapsedCodes, collapsedCodes,
collapsedTrends, collapsedTrends,
@@ -67,6 +68,7 @@ export default function FundCard({
onHoldingClick, onHoldingClick,
onActionClick, onActionClick,
onPercentModeToggle, onPercentModeToggle,
onTodayPercentModeToggle,
onToggleCollapse, onToggleCollapse,
onToggleTrendCollapse, onToggleTrendCollapse,
layoutMode = 'card', // 'card' | 'drawer'drawer 时前10重仓与业绩走势以 Tabs 展示 layoutMode = 'card', // 'card' | 'drawer'drawer 时前10重仓与业绩走势以 Tabs 展示
@@ -281,8 +283,28 @@ export default function FundCard({
</div> </div>
); );
})()} })()}
<div className="stat" style={{ flexDirection: 'column', gap: 4 }}> <div
<span className="label">当日收益</span> className="stat"
onClick={(e) => {
e.stopPropagation();
if (profit.profitToday != null) {
onTodayPercentModeToggle?.(f.code);
}
}}
style={{
cursor: profit.profitToday != null ? 'pointer' : 'default',
flexDirection: 'column',
gap: 4,
}}
title={profit.profitToday != null ? '点击切换金额/百分比' : ''}
>
<span
className="label"
style={{ display: 'flex', alignItems: 'center', gap: 1 }}
>
当日收益{todayPercentModes?.[f.code] ? '(%)' : ''}
{profit.profitToday != null && <SwitchIcon />}
</span>
<span <span
className={`value ${ className={`value ${
profit.profitToday != null profit.profitToday != null
@@ -297,7 +319,16 @@ export default function FundCard({
{profit.profitToday != null {profit.profitToday != null
? masked ? masked
? '******' ? '******'
: `${profit.profitToday > 0 ? '+' : profit.profitToday < 0 ? '-' : ''}¥${Math.abs(profit.profitToday).toFixed(2)}` : <>
{profit.profitToday > 0 ? '+' : profit.profitToday < 0 ? '-' : ''}
{todayPercentModes?.[f.code]
? `${Math.abs(
holding?.cost * holding?.share
? (profit.profitToday / (holding.cost * holding.share)) * 100
: 0,
).toFixed(2)}%`
: `¥${Math.abs(profit.profitToday).toFixed(2)}`}
</>
: '--'} : '--'}
</span> </span>
</div> </div>

View File

@@ -8,6 +8,8 @@ function CountUp({ value, prefix = '', suffix = '', decimals = 2, className = ''
const [displayValue, setDisplayValue] = useState(value); const [displayValue, setDisplayValue] = useState(value);
const previousValue = useRef(value); const previousValue = useRef(value);
const isFirstChange = useRef(true); const isFirstChange = useRef(true);
const rafIdRef = useRef(null);
const displayValueRef = useRef(value);
useEffect(() => { useEffect(() => {
if (previousValue.current === value) return; if (previousValue.current === value) return;
@@ -15,13 +17,14 @@ function CountUp({ value, prefix = '', suffix = '', decimals = 2, className = ''
if (isFirstChange.current) { if (isFirstChange.current) {
isFirstChange.current = false; isFirstChange.current = false;
previousValue.current = value; previousValue.current = value;
displayValueRef.current = value;
setDisplayValue(value); setDisplayValue(value);
return; return;
} }
const start = previousValue.current; const start = displayValueRef.current;
const end = value; const end = value;
const duration = 400; const duration = 300;
const startTime = performance.now(); const startTime = performance.now();
const animate = (currentTime) => { const animate = (currentTime) => {
@@ -29,16 +32,25 @@ function CountUp({ value, prefix = '', suffix = '', decimals = 2, className = ''
const progress = Math.min(elapsed / duration, 1); const progress = Math.min(elapsed / duration, 1);
const ease = 1 - Math.pow(1 - progress, 4); const ease = 1 - Math.pow(1 - progress, 4);
const current = start + (end - start) * ease; const current = start + (end - start) * ease;
displayValueRef.current = current;
setDisplayValue(current); setDisplayValue(current);
if (progress < 1) { if (progress < 1) {
requestAnimationFrame(animate); rafIdRef.current = requestAnimationFrame(animate);
} else { } else {
previousValue.current = value; previousValue.current = value;
rafIdRef.current = null;
} }
}; };
requestAnimationFrame(animate); rafIdRef.current = requestAnimationFrame(animate);
return () => {
if (rafIdRef.current != null) {
cancelAnimationFrame(rafIdRef.current);
rafIdRef.current = null;
}
};
}, [value]); }, [value]);
return ( return (
@@ -64,6 +76,7 @@ export default function GroupSummary({
navbarHeight navbarHeight
}) { }) {
const [showPercent, setShowPercent] = useState(true); const [showPercent, setShowPercent] = useState(true);
const [showTodayPercent, setShowTodayPercent] = useState(false);
const [isMasked, setIsMasked] = useState(masked ?? false); const [isMasked, setIsMasked] = useState(masked ?? false);
const rowRef = useRef(null); const rowRef = useRef(null);
const [assetSize, setAssetSize] = useState(24); const [assetSize, setAssetSize] = useState(24);
@@ -137,6 +150,7 @@ export default function GroupSummary({
const roundedTotalProfitToday = Math.round(totalProfitToday * 100) / 100; const roundedTotalProfitToday = Math.round(totalProfitToday * 100) / 100;
const returnRate = totalCost > 0 ? (totalHoldingReturn / totalCost) * 100 : 0; const returnRate = totalCost > 0 ? (totalHoldingReturn / totalCost) * 100 : 0;
const todayReturnRate = totalCost > 0 ? (roundedTotalProfitToday / totalCost) * 100 : 0;
return { return {
totalAsset, totalAsset,
@@ -144,6 +158,7 @@ export default function GroupSummary({
totalHoldingReturn, totalHoldingReturn,
hasHolding, hasHolding,
returnRate, returnRate,
todayReturnRate,
hasAnyTodayData, hasAnyTodayData,
}; };
}, [funds, holdings, getProfit]); }, [funds, holdings, getProfit]);
@@ -277,9 +292,17 @@ export default function GroupSummary({
<div style={{ textAlign: 'right' }}> <div style={{ textAlign: 'right' }}>
<div <div
className="muted" className="muted"
style={{ fontSize: '12px', marginBottom: 4 }} style={{
fontSize: '12px',
marginBottom: 4,
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
gap: 2,
}}
> >
当日收益 当日收益{showTodayPercent ? '(%)' : ''}{' '}
<SwitchIcon style={{ opacity: 0.4 }} />
</div> </div>
<div <div
className={ className={
@@ -295,7 +318,10 @@ export default function GroupSummary({
fontSize: '18px', fontSize: '18px',
fontWeight: 700, fontWeight: 700,
fontFamily: 'var(--font-mono)', fontFamily: 'var(--font-mono)',
cursor: summary.hasAnyTodayData ? 'pointer' : 'default',
}} }}
onClick={() => summary.hasAnyTodayData && setShowTodayPercent(!showTodayPercent)}
title="点击切换金额/百分比"
> >
{isMasked ? ( {isMasked ? (
<span className="mask-text" style={{ fontSize: metricSize }}> <span className="mask-text" style={{ fontSize: metricSize }}>
@@ -310,10 +336,18 @@ export default function GroupSummary({
? '-' ? '-'
: ''} : ''}
</span> </span>
{showTodayPercent ? (
<CountUp
value={Math.abs(summary.todayReturnRate)}
suffix="%"
style={{ fontSize: metricSize }}
/>
) : (
<CountUp <CountUp
value={Math.abs(summary.totalProfitToday)} value={Math.abs(summary.totalProfitToday)}
style={{ fontSize: metricSize }} style={{ fontSize: metricSize }}
/> />
)}
</> </>
) : ( ) : (
<span style={{ fontSize: metricSize }}>--</span> <span style={{ fontSize: metricSize }}>--</span>

View File

@@ -202,7 +202,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
}; };
const isValid = isBuy const isValid = isBuy
? (!!amount && !!feeRate && !!date && calcShare !== null && !loadingBuyMeta && (parseFloat(amount) || 0) >= (Number(minBuyAmount) || 0)) ? (!!amount && !!feeRate && !!date && calcShare !== null && !loadingBuyMeta)
: (!!share && !!date); : (!!share && !!date);
const handleSetShareFraction = (fraction) => { const handleSetShareFraction = (fraction) => {
@@ -434,9 +434,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
</label> </label>
<div <div
style={{ style={{
border: (!amount || (Number(minBuyAmount) > 0 && (parseFloat(amount) || 0) < Number(minBuyAmount))) border: !amount ? '1px solid var(--danger)' : '1px solid var(--border)',
? '1px solid var(--danger)'
: '1px solid var(--border)',
borderRadius: 12 borderRadius: 12
}} }}
> >
@@ -444,15 +442,10 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
value={amount} value={amount}
onChange={setAmount} onChange={setAmount}
step={100} step={100}
min={Number(minBuyAmount) || 0} min={0}
placeholder={(Number(minBuyAmount) || 0) > 0 ? `最少 ¥${Number(minBuyAmount)},请输入加仓金额` : '请输入加仓金额'} placeholder="请输入加仓金额"
/> />
</div> </div>
{(Number(minBuyAmount) || 0) > 0 && (
<div className="muted" style={{ fontSize: '12px', marginTop: 6 }}>
最小加仓金额¥{Number(minBuyAmount)}
</div>
)}
</div> </div>
<div className="row" style={{ gap: 12, marginBottom: 16 }}> <div className="row" style={{ gap: 12, marginBottom: 16 }}>

View File

@@ -442,6 +442,7 @@ export default function HomePage() {
const [historyModal, setHistoryModal] = useState({ open: false, fund: null }); const [historyModal, setHistoryModal] = useState({ open: false, fund: null });
const [addHistoryModal, setAddHistoryModal] = useState({ open: false, fund: null }); const [addHistoryModal, setAddHistoryModal] = useState({ open: false, fund: null });
const [percentModes, setPercentModes] = useState({}); // { [code]: boolean } const [percentModes, setPercentModes] = useState({}); // { [code]: boolean }
const [todayPercentModes, setTodayPercentModes] = useState({}); // { [code]: boolean }
const holdingsRef = useRef(holdings); const holdingsRef = useRef(holdings);
const pendingTradesRef = useRef(pendingTrades); const pendingTradesRef = useRef(pendingTrades);
@@ -4335,6 +4336,8 @@ export default function HomePage() {
dcaPlans, dcaPlans,
holdings, holdings,
percentModes, percentModes,
todayPercentModes,
todayPercentModes,
valuationSeries, valuationSeries,
collapsedCodes, collapsedCodes,
collapsedTrends, collapsedTrends,
@@ -4350,6 +4353,8 @@ export default function HomePage() {
onActionClick: (f) => setActionModal({ open: true, fund: f }), onActionClick: (f) => setActionModal({ open: true, fund: f }),
onPercentModeToggle: (code) => onPercentModeToggle: (code) =>
setPercentModes((prev) => ({ ...prev, [code]: !prev[code] })), setPercentModes((prev) => ({ ...prev, [code]: !prev[code] })),
onTodayPercentModeToggle: (code) =>
setTodayPercentModes((prev) => ({ ...prev, [code]: !prev[code] })),
onToggleCollapse: toggleCollapse, onToggleCollapse: toggleCollapse,
onToggleTrendCollapse: toggleTrendCollapse, onToggleTrendCollapse: toggleTrendCollapse,
masked: maskAmounts, masked: maskAmounts,
@@ -4426,6 +4431,8 @@ export default function HomePage() {
onActionClick: (f) => setActionModal({ open: true, fund: f }), onActionClick: (f) => setActionModal({ open: true, fund: f }),
onPercentModeToggle: (code) => onPercentModeToggle: (code) =>
setPercentModes((prev) => ({ ...prev, [code]: !prev[code] })), setPercentModes((prev) => ({ ...prev, [code]: !prev[code] })),
onTodayPercentModeToggle: (code) =>
setTodayPercentModes((prev) => ({ ...prev, [code]: !prev[code] })),
onToggleCollapse: toggleCollapse, onToggleCollapse: toggleCollapse,
onToggleTrendCollapse: toggleTrendCollapse, onToggleTrendCollapse: toggleTrendCollapse,
masked: maskAmounts, masked: maskAmounts,
@@ -4455,6 +4462,7 @@ export default function HomePage() {
dcaPlans={dcaPlans} dcaPlans={dcaPlans}
holdings={holdings} holdings={holdings}
percentModes={percentModes} percentModes={percentModes}
todayPercentModes={todayPercentModes}
valuationSeries={valuationSeries} valuationSeries={valuationSeries}
collapsedCodes={collapsedCodes} collapsedCodes={collapsedCodes}
collapsedTrends={collapsedTrends} collapsedTrends={collapsedTrends}
@@ -4471,6 +4479,9 @@ export default function HomePage() {
onPercentModeToggle={(code) => onPercentModeToggle={(code) =>
setPercentModes((prev) => ({ ...prev, [code]: !prev[code] })) setPercentModes((prev) => ({ ...prev, [code]: !prev[code] }))
} }
onTodayPercentModeToggle={(code) =>
setTodayPercentModes((prev) => ({ ...prev, [code]: !prev[code] }))
}
onToggleCollapse={toggleCollapse} onToggleCollapse={toggleCollapse}
onToggleTrendCollapse={toggleTrendCollapse} onToggleTrendCollapse={toggleTrendCollapse}
masked={maskAmounts} masked={maskAmounts}