feat:亮色主题

This commit is contained in:
hzm
2026-02-28 19:45:54 +08:00
parent 37243c5fc0
commit 1256b807a9
13 changed files with 1108 additions and 309 deletions

View File

@@ -73,20 +73,8 @@ export function DatePicker({ value, onChange }) {
return ( return (
<div className="date-picker" style={{ position: 'relative' }} onClick={(e) => e.stopPropagation()}> <div className="date-picker" style={{ position: 'relative' }} onClick={(e) => e.stopPropagation()}>
<div <div
className="input-trigger" className="date-picker-trigger"
onClick={() => setIsOpen(!isOpen)} onClick={() => 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'
}}
> >
<span>{value || '选择日期'}</span> <span>{value || '选择日期'}</span>
<CalendarIcon width="16" height="16" className="muted" /> <CalendarIcon width="16" height="16" className="muted" />
@@ -98,7 +86,7 @@ export function DatePicker({ value, onChange }) {
initial={{ opacity: 0, y: 10, scale: 0.95 }} initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }} animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }} exit={{ opacity: 0, y: 10, scale: 0.95 }}
className="glass card" className="date-picker-dropdown glass card"
style={{ style={{
position: 'absolute', position: 'absolute',
top: '100%', top: '100%',
@@ -106,10 +94,7 @@ export function DatePicker({ value, onChange }) {
width: '100%', width: '100%',
marginTop: 8, marginTop: 8,
padding: 12, padding: 12,
zIndex: 10, zIndex: 10
background: 'rgba(30, 41, 59, 0.95)',
backdropFilter: 'blur(12px)',
border: '1px solid rgba(255,255,255,0.1)'
}} }}
> >
<div className="calendar-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}> <div className="calendar-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
@@ -141,26 +126,8 @@ export function DatePicker({ value, onChange }) {
return ( return (
<div <div
key={i} key={i}
className={`date-picker-cell ${isSelected ? 'selected' : ''} ${isToday ? 'today' : ''} ${isFuture ? 'future' : ''}`}
onClick={(e) => !isFuture && handleSelect(e, d)} onClick={(e) => !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} {d}
</div> </div>

View File

@@ -185,7 +185,7 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
initial={{ opacity: 0, scale: 0.95, y: 20 }} initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }} animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }} exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="glass card modal" className="glass card modal dca-modal"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
style={{ maxWidth: '420px' }} style={{ maxWidth: '420px' }}
> >
@@ -220,28 +220,8 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
gap: 6 gap: 6
}} }}
> >
<span <span className={`dca-toggle-track ${enabled ? 'enabled' : ''}`}>
style={{ <span className="dca-toggle-thumb" style={{ left: enabled ? 16 : 2 }} />
width: 32,
height: 18,
borderRadius: 999,
background: enabled ? 'var(--primary)' : 'rgba(148,163,184,0.6)',
position: 'relative',
transition: 'background 0.2s'
}}
>
<span
style={{
position: 'absolute',
top: 2,
left: enabled ? 16 : 2,
width: 14,
height: 14,
borderRadius: '50%',
background: '#0f172a',
transition: 'left 0.2s'
}}
/>
</span> </span>
<span style={{ fontSize: 12, color: enabled ? 'var(--primary)' : 'var(--muted)' }}> <span style={{ fontSize: 12, color: enabled ? 'var(--primary)' : 'var(--muted)' }}>
{enabled ? '已启用' : '未启用'} {enabled ? '已启用' : '未启用'}
@@ -284,23 +264,13 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}> <label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
定投周期 <span style={{ color: 'var(--danger)' }}>*</span> 定投周期 <span style={{ color: 'var(--danger)' }}>*</span>
</label> </label>
<div className="row" style={{ gap: 4, background: 'rgba(0,0,0,0.2)', borderRadius: 8, padding: 4 }}> <div className="dca-option-group row" style={{ gap: 4 }}>
{CYCLES.map((opt) => ( {CYCLES.map((opt) => (
<button <button
key={opt.value} key={opt.value}
type="button" type="button"
className={`dca-option-btn ${cycle === opt.value ? 'active' : ''}`}
onClick={() => setCycle(opt.value)} onClick={() => setCycle(opt.value)}
style={{
flex: 1,
border: 'none',
background: cycle === opt.value ? 'var(--primary)' : 'transparent',
color: cycle === opt.value ? '#05263b' : 'var(--muted)',
borderRadius: 6,
fontSize: 11,
cursor: 'pointer',
padding: '4px 6px',
whiteSpace: 'nowrap'
}}
> >
{opt.label} {opt.label}
</button> </button>
@@ -314,23 +284,13 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}> <label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
扣款星期 <span style={{ color: 'var(--danger)' }}>*</span> 扣款星期 <span style={{ color: 'var(--danger)' }}>*</span>
</label> </label>
<div className="row" style={{ gap: 4, background: 'rgba(0,0,0,0.2)', borderRadius: 8, padding: 4 }}> <div className="dca-option-group row" style={{ gap: 4 }}>
{WEEKDAY_OPTIONS.map((opt) => ( {WEEKDAY_OPTIONS.map((opt) => (
<button <button
key={opt.value} key={opt.value}
type="button" type="button"
className={`dca-option-btn dca-weekday-btn ${weeklyDay === opt.value ? 'active' : ''}`}
onClick={() => setWeeklyDay(opt.value)} onClick={() => setWeeklyDay(opt.value)}
style={{
flex: 1,
border: 'none',
background: weeklyDay === opt.value ? 'var(--primary)' : 'transparent',
color: weeklyDay === opt.value ? '#05263b' : 'var(--muted)',
borderRadius: 6,
fontSize: 12,
cursor: 'pointer',
padding: '6px 4px',
whiteSpace: 'nowrap'
}}
> >
{opt.label} {opt.label}
</button> </button>
@@ -344,20 +304,7 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}> <label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
扣款日 <span style={{ color: 'var(--danger)' }}>*</span> 扣款日 <span style={{ color: 'var(--danger)' }}>*</span>
</label> </label>
<div <div className="dca-monthly-day-group scrollbar-y-styled">
className="scrollbar-y-styled"
style={{
display: 'flex',
flexWrap: 'wrap',
gap: 4,
background: 'rgba(0,0,0,0.2)',
borderRadius: 8,
padding: 4,
maxHeight: 140,
overflowY: 'auto',
scrollBehavior: 'smooth'
}}
>
{Array.from({ length: 28 }).map((_, idx) => { {Array.from({ length: 28 }).map((_, idx) => {
const day = idx + 1; const day = idx + 1;
const active = monthlyDay === day; const active = monthlyDay === day;
@@ -366,17 +313,8 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
key={day} key={day}
ref={active ? monthlyDayRef : null} ref={active ? monthlyDayRef : null}
type="button" type="button"
className={`dca-option-btn dca-monthly-btn ${active ? 'active' : ''}`}
onClick={() => setMonthlyDay(day)} onClick={() => setMonthlyDay(day)}
style={{
flex: '0 0 calc(25% - 4px)',
border: 'none',
background: active ? 'var(--primary)' : 'transparent',
color: active ? '#05263b' : 'var(--muted)',
borderRadius: 6,
fontSize: 11,
cursor: 'pointer',
padding: '4px 0'
}}
> >
{day} {day}
</button> </button>
@@ -390,15 +328,7 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
<label className="muted" style={{ display: 'block', marginBottom: 4, fontSize: '14px' }}> <label className="muted" style={{ display: 'block', marginBottom: 4, fontSize: '14px' }}>
首次扣款日期 首次扣款日期
</label> </label>
<div <div className="dca-first-date-display">
style={{
borderRadius: 12,
border: '1px solid var(--border)',
padding: '10px 12px',
fontSize: 14,
background: 'rgba(15,23,42,0.6)'
}}
>
{firstDate} {firstDate}
</div> </div>
<div className="muted" style={{ marginTop: 4, fontSize: 12 }}> <div className="muted" style={{ marginTop: 4, fontSize: 12 }}>
@@ -409,9 +339,9 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
<div className="row" style={{ gap: 12, marginTop: 12 }}> <div className="row" style={{ gap: 12, marginTop: 12 }}>
<button <button
type="button" type="button"
className="button secondary" className="button secondary dca-cancel-btn"
onClick={onClose} onClick={onClose}
style={{ flex: 1, background: 'rgba(255,255,255,0.05)', color: 'var(--text)' }} style={{ flex: 1 }}
> >
取消 取消
</button> </button>

View File

@@ -22,14 +22,41 @@ ChartJS.register(
Filler Filler
); );
const CHART_COLORS = {
dark: {
danger: '#f87171',
success: '#34d399',
primary: '#22d3ee',
muted: '#9ca3af',
border: '#1f2937',
text: '#e5e7eb',
crosshairText: '#0f172a',
},
light: {
danger: '#dc2626',
success: '#059669',
primary: '#0891b2',
muted: '#475569',
border: '#e2e8f0',
text: '#0f172a',
crosshairText: '#ffffff',
}
};
function getChartThemeColors(theme) {
return CHART_COLORS[theme] || CHART_COLORS.dark;
}
/** /**
* 分时图:展示当日(或最近一次记录日)的估值序列,纵轴为相对参考净值的涨跌幅百分比。 * 分时图:展示当日(或最近一次记录日)的估值序列,纵轴为相对参考净值的涨跌幅百分比。
* series: Array<{ time: string, value: number, date?: string }> * series: Array<{ time: string, value: number, date?: string }>
* referenceNav: 参考净值(最新单位净值),用于计算涨跌幅;未传则用当日第一个估值作为参考。 * referenceNav: 参考净值(最新单位净值),用于计算涨跌幅;未传则用当日第一个估值作为参考。
* theme: 'light' | 'dark',用于亮色主题下坐标轴与 crosshair 样式
*/ */
export default function FundIntradayChart({ series = [], referenceNav }) { export default function FundIntradayChart({ series = [], referenceNav, theme = 'dark' }) {
const chartRef = useRef(null); const chartRef = useRef(null);
const hoverTimeoutRef = useRef(null); const hoverTimeoutRef = useRef(null);
const chartColors = useMemo(() => getChartThemeColors(theme), [theme]);
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!series.length) return { labels: [], datasets: [] }; if (!series.length) return { labels: [], datasets: [] };
@@ -40,9 +67,8 @@ export default function FundIntradayChart({ series = [], referenceNav }) {
: values[0]; : values[0];
const percentages = values.map((v) => (ref ? ((v - ref) / ref) * 100 : 0)); const percentages = values.map((v) => (ref ? ((v - ref) / ref) * 100 : 0));
const lastPct = percentages[percentages.length - 1]; const lastPct = percentages[percentages.length - 1];
const riseColor = '#f87171'; // 涨用红色 const riseColor = chartColors.danger;
const fallColor = '#34d399'; // 跌用绿色 const fallColor = chartColors.success;
// 以最新点相对参考净值的涨跌定色:涨(>=0)红,跌(<0)绿
const lineColor = lastPct != null && lastPct >= 0 ? riseColor : fallColor; const lineColor = lastPct != null && lastPct >= 0 ? riseColor : fallColor;
return { return {
@@ -68,9 +94,11 @@ export default function FundIntradayChart({ series = [], referenceNav }) {
} }
] ]
}; };
}, [series, referenceNav]); }, [series, referenceNav, chartColors.danger, chartColors.success]);
const options = useMemo(() => ({ const options = useMemo(() => {
const colors = getChartThemeColors();
return {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false }, interaction: { mode: 'index', intersect: false },
@@ -88,7 +116,7 @@ export default function FundIntradayChart({ series = [], referenceNav }) {
display: true, display: true,
grid: { display: false }, grid: { display: false },
ticks: { ticks: {
color: '#9ca3af', color: colors.muted,
font: { size: 10 }, font: { size: 10 },
maxTicksLimit: 6 maxTicksLimit: 6
} }
@@ -96,9 +124,9 @@ export default function FundIntradayChart({ series = [], referenceNav }) {
y: { y: {
display: true, display: true,
position: 'left', position: 'left',
grid: { color: '#1f2937', drawBorder: false }, grid: { color: colors.border, drawBorder: false },
ticks: { ticks: {
color: '#9ca3af', color: colors.muted,
font: { size: 10 }, font: { size: 10 },
callback: (v) => (isNumber(v) ? `${v >= 0 ? '+' : ''}${v.toFixed(2)}%` : v) callback: (v) => (isNumber(v) ? `${v >= 0 ? '+' : ''}${v.toFixed(2)}%` : v)
} }
@@ -142,7 +170,8 @@ export default function FundIntradayChart({ series = [], referenceNav }) {
}, 2000); }, 2000);
} }
} }
}), []); };
}, [theme]);
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -152,7 +181,9 @@ export default function FundIntradayChart({ series = [], referenceNav }) {
}; };
}, []); }, []);
const plugins = useMemo(() => [{ const plugins = useMemo(() => {
const colors = getChartThemeColors(theme);
return [{
id: 'crosshair', id: 'crosshair',
afterDraw: (chart) => { afterDraw: (chart) => {
const ctx = chart.ctx; const ctx = chart.ctx;
@@ -175,17 +206,15 @@ export default function FundIntradayChart({ series = [], referenceNav }) {
ctx.save(); ctx.save();
ctx.setLineDash([3, 3]); ctx.setLineDash([3, 3]);
ctx.lineWidth = 1; ctx.lineWidth = 1;
ctx.strokeStyle = '#9ca3af'; ctx.strokeStyle = colors.muted;
ctx.moveTo(x, topY); ctx.moveTo(x, topY);
ctx.lineTo(x, bottomY); ctx.lineTo(x, bottomY);
ctx.moveTo(leftX, y); ctx.moveTo(leftX, y);
ctx.lineTo(rightX, y); ctx.lineTo(rightX, y);
ctx.stroke(); ctx.stroke();
const prim = typeof document !== 'undefined' const prim = colors.primary;
? (getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#22d3ee') const textCol = colors.crosshairText;
: '#22d3ee';
const bgText = '#0f172a';
ctx.font = '10px sans-serif'; ctx.font = '10px sans-serif';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
@@ -202,7 +231,7 @@ export default function FundIntradayChart({ series = [], referenceNav }) {
const labelCenterX = labelLeft + tw / 2; const labelCenterX = labelLeft + tw / 2;
ctx.fillStyle = prim; ctx.fillStyle = prim;
ctx.fillRect(labelLeft, bottomY, tw, 16); ctx.fillRect(labelLeft, bottomY, tw, 16);
ctx.fillStyle = bgText; ctx.fillStyle = textCol;
ctx.fillText(timeStr, labelCenterX, bottomY + 8); ctx.fillText(timeStr, labelCenterX, bottomY + 8);
} }
if (data && index in data) { if (data && index in data) {
@@ -211,12 +240,13 @@ export default function FundIntradayChart({ series = [], referenceNav }) {
const vw = ctx.measureText(valueStr).width + 8; const vw = ctx.measureText(valueStr).width + 8;
ctx.fillStyle = prim; ctx.fillStyle = prim;
ctx.fillRect(leftX, y - 8, vw, 16); ctx.fillRect(leftX, y - 8, vw, 16);
ctx.fillStyle = bgText; ctx.fillStyle = textCol;
ctx.fillText(valueStr, leftX + vw / 2, y); ctx.fillText(valueStr, leftX + vw / 2, y);
} }
ctx.restore(); ctx.restore();
} }
}], []); }];
}, [theme]);
if (series.length < 2) return null; if (series.length < 2) return null;

View File

@@ -29,7 +29,32 @@ ChartJS.register(
Filler Filler
); );
export default function FundTrendChart({ code, isExpanded, onToggleExpand, transactions = [] }) { const CHART_COLORS = {
dark: {
danger: '#f87171',
success: '#34d399',
primary: '#22d3ee',
muted: '#9ca3af',
border: '#1f2937',
text: '#e5e7eb',
crosshairText: '#0f172a',
},
light: {
danger: '#dc2626',
success: '#059669',
primary: '#0891b2',
muted: '#475569',
border: '#e2e8f0',
text: '#0f172a',
crosshairText: '#ffffff',
}
};
function getChartThemeColors(theme) {
return CHART_COLORS[theme] || CHART_COLORS.dark;
}
export default function FundTrendChart({ code, isExpanded, onToggleExpand, transactions = [], theme = 'dark' }) {
const [range, setRange] = useState('1m'); const [range, setRange] = useState('1m');
const [data, setData] = useState([]); const [data, setData] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -37,6 +62,8 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
const chartRef = useRef(null); const chartRef = useRef(null);
const hoverTimeoutRef = useRef(null); const hoverTimeoutRef = useRef(null);
const chartColors = useMemo(() => getChartThemeColors(theme), [theme]);
useEffect(() => { useEffect(() => {
// If collapsed, don't fetch data unless we have no data yet // If collapsed, don't fetch data unless we have no data yet
if (!isExpanded && data.length > 0) return; if (!isExpanded && data.length > 0) return;
@@ -84,12 +111,11 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
return ((last - first) / first) * 100; return ((last - first) / first) * 100;
}, [data]); }, [data]);
// Red for up, Green for down (CN market style) // Red for up, Green for down (CN market style),随主题使用 CSS 变量
// Hardcoded hex values from globals.css for Chart.js const upColor = chartColors.danger;
const upColor = '#f87171'; // --danger与折线图红色一致 const downColor = chartColors.success;
const downColor = '#34d399'; // --success
const lineColor = change >= 0 ? upColor : downColor; const lineColor = change >= 0 ? upColor : downColor;
const primaryColor = typeof document !== 'undefined' ? (getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#22d3ee') : '#22d3ee'; const primaryColor = chartColors.primary;
const chartData = useMemo(() => { const chartData = useMemo(() => {
// Calculate percentage change based on the first data point // Calculate percentage change based on the first data point
@@ -165,9 +191,10 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
} }
] ]
}; };
}, [data, lineColor, transactions, primaryColor]); }, [data, transactions, lineColor, primaryColor, upColor]);
const options = useMemo(() => { const options = useMemo(() => {
const colors = getChartThemeColors(theme);
return { return {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
@@ -190,7 +217,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
drawBorder: false drawBorder: false
}, },
ticks: { ticks: {
color: '#9ca3af', color: colors.muted,
font: { size: 10 }, font: { size: 10 },
maxTicksLimit: 4, maxTicksLimit: 4,
maxRotation: 0 maxRotation: 0
@@ -201,12 +228,12 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
display: true, display: true,
position: 'left', position: 'left',
grid: { grid: {
color: '#1f2937', color: colors.border,
drawBorder: false, drawBorder: false,
tickLength: 0 tickLength: 0
}, },
ticks: { ticks: {
color: '#9ca3af', color: colors.muted,
font: { size: 10 }, font: { size: 10 },
count: 5, count: 5,
callback: (value) => `${value.toFixed(2)}%` callback: (value) => `${value.toFixed(2)}%`
@@ -240,7 +267,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
}, },
onClick: () => {} onClick: () => {}
}; };
}, []); }, [theme]);
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -250,7 +277,9 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
}; };
}, []); }, []);
const plugins = useMemo(() => [{ const plugins = useMemo(() => {
const colors = getChartThemeColors(theme);
return [{
id: 'crosshair', id: 'crosshair',
afterEvent: (chart, args) => { afterEvent: (chart, args) => {
const { event, replay } = args || {}; const { event, replay } = args || {};
@@ -276,7 +305,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
afterDraw: (chart) => { afterDraw: (chart) => {
const ctx = chart.ctx; const ctx = chart.ctx;
const datasets = chart.data.datasets; const datasets = chart.data.datasets;
const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#22d3ee'; const primaryColor = colors.primary;
// 绘制圆角矩形(兼容无 roundRect 的环境) // 绘制圆角矩形(兼容无 roundRect 的环境)
const drawRoundRect = (left, top, w, h, r) => { const drawRoundRect = (left, top, w, h, r) => {
@@ -377,7 +406,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
ctx.beginPath(); ctx.beginPath();
ctx.setLineDash([3, 3]); ctx.setLineDash([3, 3]);
ctx.lineWidth = 1; ctx.lineWidth = 1;
ctx.strokeStyle = '#9ca3af'; ctx.strokeStyle = colors.muted;
// Draw vertical line // Draw vertical line
ctx.moveTo(x, topY); ctx.moveTo(x, topY);
@@ -415,7 +444,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
const labelCenterX = labelLeft + textWidth / 2; const labelCenterX = labelLeft + textWidth / 2;
ctx.fillStyle = primaryColor; ctx.fillStyle = primaryColor;
ctx.fillRect(labelLeft, bottomY, textWidth, 16); ctx.fillRect(labelLeft, bottomY, textWidth, 16);
ctx.fillStyle = '#0f172a'; // --background ctx.fillStyle = colors.crosshairText;
ctx.fillText(dateStr, labelCenterX, bottomY + 8); ctx.fillText(dateStr, labelCenterX, bottomY + 8);
// Y axis label (value) // Y axis label (value)
@@ -423,7 +452,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
const valWidth = ctx.measureText(valueStr).width + 8; const valWidth = ctx.measureText(valueStr).width + 8;
ctx.fillStyle = primaryColor; ctx.fillStyle = primaryColor;
ctx.fillRect(leftX, y - 8, valWidth, 16); ctx.fillRect(leftX, y - 8, valWidth, 16);
ctx.fillStyle = '#0f172a'; // --background ctx.fillStyle = colors.crosshairText;
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.fillText(valueStr, leftX + valWidth / 2, y); ctx.fillText(valueStr, leftX + valWidth / 2, y);
} }
@@ -442,7 +471,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
const label = datasets[dsIndex].label; const label = datasets[dsIndex].label;
// Determine background color based on dataset index // Determine background color based on dataset index
// 1 = Buy (主题色), 2 = Sell (与折线图红色一致) // 1 = Buy (主题色), 2 = Sell (与折线图红色一致)
const bgColor = dsIndex === 1 ? primaryColor : '#f87171'; const bgColor = dsIndex === 1 ? primaryColor : colors.danger;
// If collision, offset Buy label upwards // If collision, offset Buy label upwards
let yOffset = 0; let yOffset = 0;
@@ -457,7 +486,8 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
ctx.restore(); ctx.restore();
} }
} }
}], []); // 移除 data 依赖,因为我们直接从 chart 实例读取数据 }];
}, [theme]); // theme 变化时重算以应用亮色/暗色坐标轴与 crosshair
return ( return (
<div style={{ marginTop: 16 }} onClick={(e) => e.stopPropagation()}> <div style={{ marginTop: 16 }} onClick={(e) => e.stopPropagation()}>
@@ -501,19 +531,13 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
> >
<div style={{ position: 'relative', height: 180, width: '100%' }}> <div style={{ position: 'relative', height: 180, width: '100%' }}>
{loading && ( {loading && (
<div style={{ <div className="chart-overlay" style={{ backdropFilter: 'blur(2px)' }}>
position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'rgba(255,255,255,0.02)', zIndex: 10, backdropFilter: 'blur(2px)'
}}>
<span className="muted" style={{ fontSize: '12px' }}>加载中...</span> <span className="muted" style={{ fontSize: '12px' }}>加载中...</span>
</div> </div>
)} )}
{!loading && data.length === 0 && ( {!loading && data.length === 0 && (
<div style={{ <div className="chart-overlay">
position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'rgba(255,255,255,0.02)', zIndex: 10
}}>
<span className="muted" style={{ fontSize: '12px' }}>暂无数据</span> <span className="muted" style={{ fontSize: '12px' }}>暂无数据</span>
</div> </div>
)} )}
@@ -523,23 +547,13 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
)} )}
</div> </div>
<div style={{ display: 'flex', gap: 4, marginTop: 12, justifyContent: 'space-between', background: 'rgba(0,0,0,0.2)', padding: 4, borderRadius: 8 }}> <div className="trend-range-bar">
{ranges.map(r => ( {ranges.map(r => (
<button <button
key={r.value} key={r.value}
type="button"
className={`trend-range-btn ${range === r.value ? 'active' : ''}`}
onClick={(e) => { e.stopPropagation(); setRange(r.value); }} onClick={(e) => { e.stopPropagation(); setRange(r.value); }}
style={{
flex: 1,
padding: '6px 0',
fontSize: '11px',
borderRadius: '6px',
border: 'none',
background: range === r.value ? 'rgba(255,255,255,0.1)' : 'transparent',
color: range === r.value ? 'var(--primary)' : 'var(--muted)',
cursor: 'pointer',
transition: 'all 0.2s',
fontWeight: range === r.value ? 600 : 400
}}
> >
{r.label} {r.label}
</button> </button>

View File

@@ -75,9 +75,9 @@ export default function HoldingActionModal({ fund, onClose, onAction, hasHistory
减仓 减仓
</button> </button>
<button <button
className="button col-4" className="button col-4 dca-btn"
onClick={() => onAction('dca')} onClick={() => onAction('dca')}
style={{ background: 'rgba(34, 211, 238, 0.12)', border: '1px solid #ffffff', color: '#ffffff', fontSize: 14 }} style={{ fontSize: 14 }}
> >
定投 定投
</button> </button>

View File

@@ -243,3 +243,20 @@ export function CameraIcon(props) {
</svg> </svg>
); );
} }
export function SunIcon(props) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
</svg>
);
}
export function MoonIcon(props) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
);
}

View File

@@ -564,7 +564,7 @@ export default function PcFundTable({
left: isLeft ? `${column.getStart('left')}px` : undefined, left: isLeft ? `${column.getStart('left')}px` : undefined,
right: isRight ? `${column.getAfter('right')}px` : undefined, right: isRight ? `${column.getAfter('right')}px` : undefined,
zIndex: isHeader ? 11 : 10, zIndex: isHeader ? 11 : 10,
backgroundColor: isHeader ? '#2a394b' : 'var(--row-bg)', backgroundColor: isHeader ? 'var(--table-pinned-header-bg)' : 'var(--row-bg)',
boxShadow: 'none', boxShadow: 'none',
textAlign: isNameColumn ? 'left' : 'center', textAlign: isNameColumn ? 'left' : 'center',
justifyContent: isNameColumn ? 'flex-start' : 'center', justifyContent: isNameColumn ? 'flex-start' : 'center',
@@ -572,14 +572,14 @@ export default function PcFundTable({
}; };
return ( return (
<> <div className="pc-fund-table">
<style>{` <style>{`
.table-row-scroll { .table-row-scroll {
--row-bg: var(--bg); --row-bg: var(--bg);
background-color: var(--row-bg); background-color: var(--row-bg);
} }
.table-row-scroll:hover { .table-row-scroll:hover {
--row-bg: #2a394b; --row-bg: var(--table-row-hover-bg);
} }
/* 覆盖 grid 布局为 flex 以支持动态列宽 */ /* 覆盖 grid 布局为 flex 以支持动态列宽 */
@@ -752,6 +752,6 @@ export default function PcFundTable({
confirmText="重置" confirmText="重置"
/> />
)} )}
</> </div>
); );
} }

View File

@@ -40,10 +40,6 @@ export default function ScanPickModal({ onClose, onPick, onFilesDrop, isScanning
marginBottom: 12, marginBottom: 12,
padding: '20px 16px', padding: '20px 16px',
borderRadius: 12, borderRadius: 12,
border: `2px dashed ${isDragging ? 'var(--primary)' : 'var(--border)'}`,
background: isDragging
? 'rgba(34, 211, 238, 0.08)'
: 'rgba(255, 255, 255, 0.02)',
transition: 'border-color 0.2s ease, background 0.2s ease', transition: 'border-color 0.2s ease, background 0.2s ease',
cursor: isScanning ? 'not-allowed' : 'pointer', cursor: isScanning ? 'not-allowed' : 'pointer',
pointerEvents: isScanning ? 'none' : 'auto', pointerEvents: isScanning ? 'none' : 'auto',
@@ -64,7 +60,7 @@ export default function ScanPickModal({ onClose, onPick, onFilesDrop, isScanning
initial={{ opacity: 0, scale: 0.95, y: 20 }} initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }} animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }} exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="glass card modal" className="glass card modal scan-pick-modal"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
style={{ width: 420, maxWidth: '90vw' }} style={{ width: 420, maxWidth: '90vw' }}
> >
@@ -75,7 +71,7 @@ export default function ScanPickModal({ onClose, onPick, onFilesDrop, isScanning
从相册选择一张或多张持仓截图系统将自动识别其中的<span style={{ color: 'var(--primary)' }}>基金代码6位数字</span>并支持批量导入 从相册选择一张或多张持仓截图系统将自动识别其中的<span style={{ color: 'var(--primary)' }}>基金代码6位数字</span>并支持批量导入
</div> </div>
<div <div
className="muted" className={`scan-pick-dropzone muted ${isDragging ? 'dragging' : ''}`}
style={dropZoneStyle} style={dropZoneStyle}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}

View File

@@ -168,7 +168,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
initial={{ opacity: 0, scale: 0.95, y: 20 }} initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }} animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }} exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="glass card modal" className="glass card modal trade-modal"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
style={{ maxWidth: '420px' }} style={{ maxWidth: '420px' }}
> >
@@ -184,19 +184,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
{!showPendingList && !showConfirm && currentPendingTrades.length > 0 && ( {!showPendingList && !showConfirm && currentPendingTrades.length > 0 && (
<div <div
style={{ className="trade-pending-alert"
marginBottom: 16,
background: 'rgba(230, 162, 60, 0.1)',
border: '1px solid rgba(230, 162, 60, 0.2)',
borderRadius: 8,
padding: '8px 12px',
fontSize: '12px',
color: '#e6a23c',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer'
}}
onClick={() => setShowPendingList(true)} onClick={() => setShowPendingList(true)}
> >
<span> 当前有 {currentPendingTrades.length} 笔待处理交易</span> <span> 当前有 {currentPendingTrades.length} 笔待处理交易</span>
@@ -206,7 +194,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
{showPendingList ? ( {showPendingList ? (
<div className="pending-list" style={{ maxHeight: '300px', overflowY: 'auto' }}> <div className="pending-list" style={{ maxHeight: '300px', overflowY: 'auto' }}>
<div className="pending-list-header" style={{ position: 'sticky', top: 0, zIndex: 1, background: 'rgba(15,23,42,0.95)', backdropFilter: 'blur(6px)', paddingBottom: 8, marginBottom: 8, borderBottom: '1px solid var(--border)' }}> <div className="pending-list-header trade-pending-header">
<button <button
className="button secondary" className="button secondary"
onClick={() => setShowPendingList(false)} onClick={() => setShowPendingList(false)}
@@ -217,7 +205,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
</div> </div>
<div className="pending-list-items" style={{ paddingTop: 0 }}> <div className="pending-list-items" style={{ paddingTop: 0 }}>
{currentPendingTrades.map((trade, idx) => ( {currentPendingTrades.map((trade, idx) => (
<div key={trade.id || idx} style={{ background: 'rgba(255,255,255,0.05)', padding: 12, borderRadius: 8, marginBottom: 8 }}> <div key={trade.id || idx} className="trade-pending-item">
<div className="row" style={{ justifyContent: 'space-between', marginBottom: 4 }}> <div className="row" style={{ justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontWeight: 600, fontSize: '14px', color: trade.type === 'buy' ? 'var(--danger)' : 'var(--success)' }}> <span style={{ fontWeight: 600, fontSize: '14px', color: trade.type === 'buy' ? 'var(--danger)' : 'var(--success)' }}>
{trade.type === 'buy' ? '买入' : '卖出'} {trade.type === 'buy' ? '买入' : '卖出'}
@@ -231,17 +219,11 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
<div className="row" style={{ justifyContent: 'space-between', fontSize: '12px', marginTop: 4 }}> <div className="row" style={{ justifyContent: 'space-between', fontSize: '12px', marginTop: 4 }}>
<span className="muted">状态</span> <span className="muted">状态</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ color: '#e6a23c' }}>等待净值更新...</span> <span className="trade-pending-status">等待净值更新...</span>
<button <button
className="button secondary" className="button secondary trade-revoke-btn"
onClick={() => setRevokeTrade(trade)} onClick={() => setRevokeTrade(trade)}
style={{ style={{ padding: '2px 8px', fontSize: '10px', height: 'auto' }}
padding: '2px 8px',
fontSize: '10px',
height: 'auto',
background: 'rgba(255,255,255,0.1)',
color: 'var(--text)'
}}
> >
撤销 撤销
</button> </button>
@@ -263,7 +245,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
{showConfirm ? ( {showConfirm ? (
isBuy ? ( isBuy ? (
<div style={{ fontSize: '14px' }}> <div style={{ fontSize: '14px' }}>
<div style={{ background: 'rgba(255,255,255,0.05)', borderRadius: 12, padding: 16, marginBottom: 20 }}> <div className="trade-confirm-card">
<div className="row" style={{ justifyContent: 'space-between', marginBottom: 8 }}> <div className="row" style={{ justifyContent: 'space-between', marginBottom: 8 }}>
<span className="muted">基金名称</span> <span className="muted">基金名称</span>
<span style={{ fontWeight: 600 }}>{fund?.name}</span> <span style={{ fontWeight: 600 }}>{fund?.name}</span>
@@ -288,7 +270,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
<span className="muted">买入日期</span> <span className="muted">买入日期</span>
<span>{date}</span> <span>{date}</span>
</div> </div>
<div className="row" style={{ justifyContent: 'space-between', marginBottom: 8, borderTop: '1px solid rgba(255,255,255,0.1)', paddingTop: 8 }}> <div className="row trade-confirm-divider" style={{ justifyContent: 'space-between', marginBottom: 8, paddingTop: 8 }}>
<span className="muted">交易时段</span> <span className="muted">交易时段</span>
<span>{isAfter3pm ? '15:00后' : '15:00前'}</span> <span>{isAfter3pm ? '15:00后' : '15:00前'}</span>
</div> </div>
@@ -301,7 +283,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
<div style={{ marginBottom: 20 }}> <div style={{ marginBottom: 20 }}>
<div className="muted" style={{ marginBottom: 8, fontSize: '12px' }}>持仓变化预览</div> <div className="muted" style={{ marginBottom: 8, fontSize: '12px' }}>持仓变化预览</div>
<div className="row" style={{ gap: 12 }}> <div className="row" style={{ gap: 12 }}>
<div style={{ flex: 1, background: 'rgba(0,0,0,0.2)', padding: 12, borderRadius: 8 }}> <div className="trade-preview-card" style={{ flex: 1 }}>
<div className="muted" style={{ fontSize: '12px', marginBottom: 4 }}>持有份额</div> <div className="muted" style={{ fontSize: '12px', marginBottom: 4 }}>持有份额</div>
<div style={{ fontSize: '12px' }}> <div style={{ fontSize: '12px' }}>
<span style={{ opacity: 0.7 }}>{holding.share.toFixed(2)}</span> <span style={{ opacity: 0.7 }}>{holding.share.toFixed(2)}</span>
@@ -310,7 +292,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
</div> </div>
</div> </div>
{price ? ( {price ? (
<div style={{ flex: 1, background: 'rgba(0,0,0,0.2)', padding: 12, borderRadius: 8 }}> <div className="trade-preview-card" style={{ flex: 1 }}>
<div className="muted" style={{ fontSize: '12px', marginBottom: 4 }}>持有市值 ()</div> <div className="muted" style={{ fontSize: '12px', marginBottom: 4 }}>持有市值 ()</div>
<div style={{ fontSize: '12px' }}> <div style={{ fontSize: '12px' }}>
<span style={{ opacity: 0.7 }}>¥{(holding.share * Number(price)).toFixed(2)}</span> <span style={{ opacity: 0.7 }}>¥{(holding.share * Number(price)).toFixed(2)}</span>
@@ -326,9 +308,9 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
<div className="row" style={{ gap: 12 }}> <div className="row" style={{ gap: 12 }}>
<button <button
type="button" type="button"
className="button secondary" className="button secondary trade-back-btn"
onClick={() => setShowConfirm(false)} onClick={() => setShowConfirm(false)}
style={{ flex: 1, background: 'rgba(255,255,255,0.05)', color: 'var(--text)' }} style={{ flex: 1 }}
> >
返回修改 返回修改
</button> </button>
@@ -345,7 +327,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
</div> </div>
) : ( ) : (
<div style={{ fontSize: '14px' }}> <div style={{ fontSize: '14px' }}>
<div style={{ background: 'rgba(255,255,255,0.05)', borderRadius: 12, padding: 16, marginBottom: 20 }}> <div className="trade-confirm-card">
<div className="row" style={{ justifyContent: 'space-between', marginBottom: 8 }}> <div className="row" style={{ justifyContent: 'space-between', marginBottom: 8 }}>
<span className="muted">基金名称</span> <span className="muted">基金名称</span>
<span style={{ fontWeight: 600 }}>{fund?.name}</span> <span style={{ fontWeight: 600 }}>{fund?.name}</span>
@@ -370,7 +352,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
<span className="muted">卖出日期</span> <span className="muted">卖出日期</span>
<span>{date}</span> <span>{date}</span>
</div> </div>
<div className="row" style={{ justifyContent: 'space-between', marginBottom: 8, borderTop: '1px solid rgba(255,255,255,0.1)', paddingTop: 8 }}> <div className="row trade-confirm-divider" style={{ justifyContent: 'space-between', marginBottom: 8, paddingTop: 8 }}>
<span className="muted">预计回款</span> <span className="muted">预计回款</span>
<span style={{ color: 'var(--danger)', fontWeight: 700 }}>{loadingPrice ? '计算中...' : (price ? `¥${estimatedReturn.toFixed(2)}` : '待计算')}</span> <span style={{ color: 'var(--danger)', fontWeight: 700 }}>{loadingPrice ? '计算中...' : (price ? `¥${estimatedReturn.toFixed(2)}` : '待计算')}</span>
</div> </div>
@@ -383,7 +365,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
<div style={{ marginBottom: 20 }}> <div style={{ marginBottom: 20 }}>
<div className="muted" style={{ marginBottom: 8, fontSize: '12px' }}>持仓变化预览</div> <div className="muted" style={{ marginBottom: 8, fontSize: '12px' }}>持仓变化预览</div>
<div className="row" style={{ gap: 12 }}> <div className="row" style={{ gap: 12 }}>
<div style={{ flex: 1, background: 'rgba(0,0,0,0.2)', padding: 12, borderRadius: 8 }}> <div className="trade-preview-card" style={{ flex: 1 }}>
<div className="muted" style={{ fontSize: '12px', marginBottom: 4 }}>持有份额</div> <div className="muted" style={{ fontSize: '12px', marginBottom: 4 }}>持有份额</div>
<div style={{ fontSize: '12px' }}> <div style={{ fontSize: '12px' }}>
<span style={{ opacity: 0.7 }}>{holding.share.toFixed(2)}</span> <span style={{ opacity: 0.7 }}>{holding.share.toFixed(2)}</span>
@@ -392,7 +374,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
</div> </div>
</div> </div>
{price ? ( {price ? (
<div style={{ flex: 1, background: 'rgba(0,0,0,0.2)', padding: 12, borderRadius: 8 }}> <div className="trade-preview-card" style={{ flex: 1 }}>
<div className="muted" style={{ fontSize: '12px', marginBottom: 4 }}>持有市值 ()</div> <div className="muted" style={{ fontSize: '12px', marginBottom: 4 }}>持有市值 ()</div>
<div style={{ fontSize: '12px' }}> <div style={{ fontSize: '12px' }}>
<span style={{ opacity: 0.7 }}>¥{(holding.share * sellPrice).toFixed(2)}</span> <span style={{ opacity: 0.7 }}>¥{(holding.share * sellPrice).toFixed(2)}</span>
@@ -408,9 +390,9 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
<div className="row" style={{ gap: 12 }}> <div className="row" style={{ gap: 12 }}>
<button <button
type="button" type="button"
className="button secondary" className="button secondary trade-back-btn"
onClick={() => setShowConfirm(false)} onClick={() => setShowConfirm(false)}
style={{ flex: 1, background: 'rgba(255,255,255,0.05)', color: 'var(--text)' }} style={{ flex: 1 }}
> >
返回修改 返回修改
</button> </button>
@@ -472,36 +454,18 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}> <label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
交易时段 交易时段
</label> </label>
<div className="row" style={{ gap: 8, background: 'rgba(0,0,0,0.2)', borderRadius: '8px', padding: '4px' }}> <div className="trade-time-slot row" style={{ gap: 8 }}>
<button <button
type="button" type="button"
className={!isAfter3pm ? 'trade-time-btn active' : 'trade-time-btn'}
onClick={() => setIsAfter3pm(false)} onClick={() => setIsAfter3pm(false)}
style={{
flex: 1,
border: 'none',
background: !isAfter3pm ? 'var(--primary)' : 'transparent',
color: !isAfter3pm ? '#05263b' : 'var(--muted)',
borderRadius: '6px',
fontSize: '12px',
cursor: 'pointer',
padding: '6px 8px'
}}
> >
15:00 15:00
</button> </button>
<button <button
type="button" type="button"
className={isAfter3pm ? 'trade-time-btn active' : 'trade-time-btn'}
onClick={() => setIsAfter3pm(true)} onClick={() => setIsAfter3pm(true)}
style={{
flex: 1,
border: 'none',
background: isAfter3pm ? 'var(--primary)' : 'transparent',
color: isAfter3pm ? '#05263b' : 'var(--muted)',
borderRadius: '6px',
fontSize: '12px',
cursor: 'pointer',
padding: '6px 8px'
}}
> >
15:00 15:00
</button> </button>
@@ -544,17 +508,8 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
<button <button
key={opt.label} key={opt.label}
type="button" type="button"
className="trade-amount-btn"
onClick={() => handleSetShareFraction(opt.value)} onClick={() => handleSetShareFraction(opt.value)}
style={{
flex: 1,
padding: '4px 8px',
fontSize: '12px',
background: 'rgba(255,255,255,0.1)',
border: 'none',
borderRadius: '4px',
color: 'var(--text)',
cursor: 'pointer'
}}
> >
{opt.label} {opt.label}
</button> </button>
@@ -563,7 +518,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
)} )}
{holding && ( {holding && (
<div className="muted" style={{ fontSize: '12px', marginTop: 6 }}> <div className="muted" style={{ fontSize: '12px', marginTop: 6 }}>
当前持仓: {holding.share.toFixed(2)} {pendingSellShare > 0 && <span style={{ color: '#e6a23c', marginLeft: 8 }}>冻结: {pendingSellShare.toFixed(2)} </span>} 当前持仓: {holding.share.toFixed(2)} {pendingSellShare > 0 && <span className="trade-pending-status" style={{ marginLeft: 8 }}>冻结: {pendingSellShare.toFixed(2)} </span>}
</div> </div>
)} )}
</div> </div>
@@ -614,36 +569,18 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}> <label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
交易时段 交易时段
</label> </label>
<div className="row" style={{ gap: 8, background: 'rgba(0,0,0,0.2)', borderRadius: '8px', padding: '4px' }}> <div className="trade-time-slot row" style={{ gap: 8 }}>
<button <button
type="button" type="button"
className={!isAfter3pm ? 'trade-time-btn active' : 'trade-time-btn'}
onClick={() => setIsAfter3pm(false)} onClick={() => setIsAfter3pm(false)}
style={{
flex: 1,
border: 'none',
background: !isAfter3pm ? 'var(--primary)' : 'transparent',
color: !isAfter3pm ? '#05263b' : 'var(--muted)',
borderRadius: '6px',
fontSize: '12px',
cursor: 'pointer',
padding: '6px 8px'
}}
> >
15:00 15:00
</button> </button>
<button <button
type="button" type="button"
className={isAfter3pm ? 'trade-time-btn active' : 'trade-time-btn'}
onClick={() => setIsAfter3pm(true)} onClick={() => setIsAfter3pm(true)}
style={{
flex: 1,
border: 'none',
background: isAfter3pm ? 'var(--primary)' : 'transparent',
color: isAfter3pm ? '#05263b' : 'var(--muted)',
borderRadius: '6px',
fontSize: '12px',
cursor: 'pointer',
padding: '6px 8px'
}}
> >
15:00 15:00
</button> </button>
@@ -663,7 +600,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
)} )}
<div className="row" style={{ gap: 12, marginTop: 12 }}> <div className="row" style={{ gap: 12, marginTop: 12 }}>
<button type="button" className="button secondary" onClick={onClose} style={{ flex: 1, background: 'rgba(255,255,255,0.05)', color: 'var(--text)' }}>取消</button> <button type="button" className="button secondary trade-cancel-btn" onClick={onClose} style={{ flex: 1 }}>取消</button>
<button <button
type="submit" type="submit"
className="button" className="button"

View File

@@ -54,7 +54,7 @@ export default function TransactionHistoryModal({
initial={{ opacity: 0, scale: 0.95, y: 20 }} initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }} animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }} exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="glass card modal" className="glass card modal tx-history-modal"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
style={{ maxWidth: '480px', maxHeight: '80vh', display: 'flex', flexDirection: 'column' }} style={{ maxWidth: '480px', maxHeight: '80vh', display: 'flex', flexDirection: 'column' }}
> >
@@ -88,22 +88,14 @@ export default function TransactionHistoryModal({
<div style={{ marginBottom: 20 }}> <div style={{ marginBottom: 20 }}>
<div className="muted" style={{ fontSize: '12px', marginBottom: 8, paddingLeft: 4 }}>待处理队列</div> <div className="muted" style={{ fontSize: '12px', marginBottom: 8, paddingLeft: 4 }}>待处理队列</div>
{pendingTransactions.map((item) => ( {pendingTransactions.map((item) => (
<div key={item.id} style={{ background: 'rgba(230, 162, 60, 0.1)', border: '1px solid rgba(230, 162, 60, 0.2)', borderRadius: 8, padding: 12, marginBottom: 8 }}> <div key={item.id} className="tx-history-pending-item">
<div className="row" style={{ justifyContent: 'space-between', marginBottom: 4 }}> <div className="row" style={{ justifyContent: 'space-between', marginBottom: 4 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontWeight: 600, fontSize: '14px', color: item.type === 'buy' ? 'var(--primary)' : 'var(--danger)' }}> <span style={{ fontWeight: 600, fontSize: '14px', color: item.type === 'buy' ? 'var(--primary)' : 'var(--danger)' }}>
{item.type === 'buy' ? '买入' : '卖出'} {item.type === 'buy' ? '买入' : '卖出'}
</span> </span>
{item.type === 'buy' && item.isDca && ( {item.type === 'buy' && item.isDca && (
<span <span className="tx-history-dca-badge">
style={{
fontSize: 10,
padding: '2px 6px',
borderRadius: 999,
background: 'rgba(34,197,94,0.15)',
color: '#4ade80'
}}
>
定投 定投
</span> </span>
)} )}
@@ -115,11 +107,11 @@ export default function TransactionHistoryModal({
<span>{item.share ? `${Number(item.share).toFixed(2)}` : `¥${Number(item.amount).toFixed(2)}`}</span> <span>{item.share ? `${Number(item.share).toFixed(2)}` : `¥${Number(item.amount).toFixed(2)}`}</span>
</div> </div>
<div className="row" style={{ justifyContent: 'space-between', fontSize: '12px', marginTop: 8 }}> <div className="row" style={{ justifyContent: 'space-between', fontSize: '12px', marginTop: 8 }}>
<span style={{ color: '#e6a23c' }}>等待净值更新...</span> <span className="tx-history-pending-status">等待净值更新...</span>
<button <button
className="button secondary" className="button secondary tx-history-action-btn"
onClick={() => handleDeleteClick(item, 'pending')} onClick={() => handleDeleteClick(item, 'pending')}
style={{ padding: '2px 8px', fontSize: '10px', height: 'auto', background: 'rgba(255,255,255,0.1)' }} style={{ padding: '2px 8px', fontSize: '10px', height: 'auto' }}
> >
撤销 撤销
</button> </button>
@@ -136,22 +128,14 @@ export default function TransactionHistoryModal({
<div className="muted" style={{ textAlign: 'center', padding: '20px 0', fontSize: '12px' }}>暂无历史交易记录</div> <div className="muted" style={{ textAlign: 'center', padding: '20px 0', fontSize: '12px' }}>暂无历史交易记录</div>
) : ( ) : (
sortedTransactions.map((item) => ( sortedTransactions.map((item) => (
<div key={item.id} style={{ background: 'rgba(255, 255, 255, 0.05)', borderRadius: 8, padding: 12, marginBottom: 8 }}> <div key={item.id} className="tx-history-record-item">
<div className="row" style={{ justifyContent: 'space-between', marginBottom: 4 }}> <div className="row" style={{ justifyContent: 'space-between', marginBottom: 4 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontWeight: 600, fontSize: '14px', color: item.type === 'buy' ? 'var(--primary)' : 'var(--danger)' }}> <span style={{ fontWeight: 600, fontSize: '14px', color: item.type === 'buy' ? 'var(--primary)' : 'var(--danger)' }}>
{item.type === 'buy' ? '买入' : '卖出'} {item.type === 'buy' ? '买入' : '卖出'}
</span> </span>
{item.type === 'buy' && item.isDca && ( {item.type === 'buy' && item.isDca && (
<span <span className="tx-history-dca-badge">
style={{
fontSize: 10,
padding: '2px 6px',
borderRadius: 999,
background: 'rgba(34,197,94,0.15)',
color: '#4ade80'
}}
>
定投 定投
</span> </span>
)} )}
@@ -175,9 +159,9 @@ export default function TransactionHistoryModal({
<div className="row" style={{ justifyContent: 'space-between', fontSize: '12px', marginTop: 8 }}> <div className="row" style={{ justifyContent: 'space-between', fontSize: '12px', marginTop: 8 }}>
<span className="muted"></span> <span className="muted"></span>
<button <button
className="button secondary" className="button secondary tx-history-action-btn"
onClick={() => handleDeleteClick(item, 'history')} onClick={() => handleDeleteClick(item, 'history')}
style={{ padding: '2px 8px', fontSize: '10px', height: 'auto', background: 'rgba(255,255,255,0.1)', color: 'var(--muted)' }} style={{ padding: '2px 8px', fontSize: '10px', height: 'auto' }}
> >
删除记录 删除记录
</button> </button>

View File

@@ -8,6 +8,23 @@
--success: #34d399; --success: #34d399;
--danger: #f87171; --danger: #f87171;
--border: #1f2937; --border: #1f2937;
--table-pinned-header-bg: #2a394b;
--table-row-hover-bg: #2a394b;
}
/* 亮色主题ui-ux-pro-max 规范 - 正文 #0F172A、弱化 #475569、玻璃 bg-white/80+、边框可见 */
[data-theme="light"] {
--bg: #f1f5f9;
--card: #ffffff;
--text: #0f172a;
--muted: #475569;
--primary: #0891b2;
--accent: #2563eb;
--success: #059669;
--danger: #dc2626;
--border: #e2e8f0;
--table-pinned-header-bg: #e2e8f0;
--table-row-hover-bg: #e2e8f0;
} }
* { * {
@@ -52,6 +69,22 @@ body::before {
); );
} }
[data-theme="light"] body::before {
background:
radial-gradient(
ellipse 120% 120% at 10% -10%,
rgba(59, 130, 246, 0.06) 0%,
rgba(59, 130, 246, 0.02) 40%,
transparent 70%
),
radial-gradient(
ellipse 120% 120% at 90% 0%,
rgba(8, 145, 178, 0.05) 0%,
rgba(8, 145, 178, 0.015) 45%,
transparent 70%
);
}
.container { .container {
max-width: 90%; max-width: 90%;
width: 1200px; width: 1200px;
@@ -67,6 +100,339 @@ body::before {
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
} }
[data-theme="light"] .glass {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0.85));
border: 1px solid var(--border);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
backdrop-filter: blur(12px);
}
/* 亮色主题:按钮样式适配 */
[data-theme="light"] .button {
border-color: rgba(8, 145, 178, 0.4);
background: linear-gradient(180deg, #0ea5e9, #0891b2);
color: #fff;
box-shadow: 0 2px 8px rgba(8, 145, 178, 0.2);
}
[data-theme="light"] .button:hover {
box-shadow: 0 6px 20px rgba(8, 145, 178, 0.35);
}
[data-theme="light"] .button.secondary {
background: #fff;
border-color: var(--border);
color: var(--text);
}
[data-theme="light"] .button.secondary:hover {
background: #f1f5f9;
border-color: var(--muted);
}
[data-theme="light"] .icon-button {
background: #fff;
border-color: var(--border);
color: var(--muted);
}
[data-theme="light"] .icon-button:hover {
border-color: var(--accent);
color: var(--text);
background: #f8fafc;
}
[data-theme="light"] .icon-button.danger {
background: linear-gradient(180deg, #ef4444, #dc2626);
color: #fff;
border-color: rgba(220, 38, 38, 0.4);
}
[data-theme="light"] .icon-button.danger:hover {
box-shadow: 0 6px 16px rgba(220, 38, 38, 0.25);
}
/* 亮色主题GitHub 图标加深,提升在浅色背景上的可见度 */
[data-theme="light"] .github-icon-wrap img {
filter: brightness(0.4);
}
[data-theme="light"] .icon-button[aria-busy="true"] {
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
}
[data-theme="light"] .badge {
background: #fff;
border-color: var(--border);
}
[data-theme="light"] .badge-v {
background: #fff;
border-color: var(--border);
}
[data-theme="light"] .sticky-toggle-btn:hover {
background: rgba(0, 0, 0, 0.06);
}
[data-theme="light"] .user-menu-item:hover {
background: rgba(0, 0, 0, 0.05);
}
[data-theme="light"] .user-menu-item.danger:hover {
background: rgba(220, 38, 38, 0.08);
}
[data-theme="light"] .fund-chip {
background: rgba(8, 145, 178, 0.12);
border-color: rgba(8, 145, 178, 0.35);
}
[data-theme="light"] .remove-chip:hover {
background: rgba(8, 145, 178, 0.18);
}
/* 亮色主题:搜索输入框与下拉 */
[data-theme="light"] .input {
background: #fff;
border-color: var(--border);
color: var(--text);
}
[data-theme="light"] .input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}
[data-theme="light"] .navbar-add-fund .input {
background: rgba(255, 255, 255, 0.95);
border: none;
}
[data-theme="light"] .navbar-input-field {
color: var(--text);
}
[data-theme="light"] .search-dropdown {
background: rgba(255, 255, 255, 0.98) !important;
border: 1px solid var(--border);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
[data-theme="light"] .search-item:hover:not(.added) {
background: rgba(0, 0, 0, 0.05);
}
[data-theme="light"] .search-item.selected {
background: rgba(8, 145, 178, 0.12);
border: 1px solid rgba(8, 145, 178, 0.3);
}
[data-theme="light"] .search-spinner {
border-color: rgba(0, 0, 0, 0.1);
border-top-color: var(--primary);
}
/* 亮色主题filter-bar 区域 */
[data-theme="light"] .filter-bar {
background: rgba(255, 255, 255, 0.9);
border: 1px solid var(--border);
backdrop-filter: blur(12px);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
[data-theme="light"] .tab {
color: var(--muted);
}
[data-theme="light"] .tab:hover {
color: var(--primary);
background: rgba(8, 145, 178, 0.1);
}
[data-theme="light"] .tab.active {
background: rgba(8, 145, 178, 0.15);
color: var(--primary);
}
[data-theme="light"] .tabs-nav-btn {
background: rgba(255, 255, 255, 0.9);
border-color: var(--border);
color: var(--muted);
}
[data-theme="light"] .tabs-nav-btn:hover {
color: var(--primary);
border-color: var(--primary);
background: #fff;
}
[data-theme="light"] .group-summary-sticky {
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(16px);
border-bottom-color: var(--border);
}
[data-theme="light"] .group-selector-popup {
background: rgba(255, 255, 255, 0.98) !important;
border: 1px solid var(--border);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.08), 0 8px 10px -6px rgba(0, 0, 0, 0.06);
}
@media (max-width: 640px) {
[data-theme="light"] .filter-bar {
background: rgba(255, 255, 255, 0.95);
border-bottom-color: var(--border);
}
[data-theme="light"] .group-summary-sticky {
background: rgba(255, 255, 255, 0.95);
border-bottom-color: var(--border);
}
}
/* 亮色主题sort-group 区域(视图切换 + 排序芯片) */
[data-theme="light"] .view-toggle {
background: rgba(0, 0, 0, 0.06) !important;
}
[data-theme="light"] .view-toggle .icon-button.active {
color: #fff !important;
}
[data-theme="light"] .chip {
background: #fff;
border-color: var(--border);
color: var(--text);
}
[data-theme="light"] .chip:hover {
border-color: var(--accent);
}
[data-theme="light"] .chip.active {
background: linear-gradient(180deg, #0ea5e9, #0891b2);
color: #fff;
border-color: transparent;
}
/* 亮色主题:表格表头与行悬浮高亮 */
[data-theme="light"] .table-header-row {
background: #e2e8f0;
border-bottom-color: var(--border);
}
[data-theme="light"] .table-header-row-scroll {
background: rgba(0, 0, 0, 0.04);
border-bottom-color: var(--border);
}
[data-theme="light"] .table-header-cell-fixed {
background: rgba(0, 0, 0, 0.04);
border-bottom-color: var(--border);
}
[data-theme="light"] .table-row:hover {
background: #e2e8f0;
}
[data-theme="light"] .table-row-scroll:hover,
[data-theme="light"] .table-row-scroll.row-hovered {
background: #e2e8f0;
}
[data-theme="light"] .table-fixed-row.row-hovered {
background: #e2e8f0;
}
/* 亮色主题PcFundTable 专用 */
[data-theme="light"] .pc-fund-table .table-header-row-scroll {
background: #cbd5e1;
}
[data-theme="light"] .pc-fund-table .table-header-row-scroll .table-header-cell {
background: #cbd5e1 !important;
color: #0f172a;
}
[data-theme="light"] .pc-fund-table .resizer:hover::after {
box-shadow: 0 0 0 2px rgba(8, 145, 178, 0.25);
}
[data-theme="light"] .pc-fund-table .table-row.empty-row {
background: rgba(0, 0, 0, 0.02);
border: 1px solid var(--border);
}
/* 亮色主题前10重仓股票展开展示 */
[data-theme="light"] .list .item {
background: #fff;
border-color: var(--border);
}
[data-theme="light"] .list .item .name {
color: var(--text);
}
[data-theme="light"] .list .item .weight {
color: var(--accent);
}
/* 亮色主题:业绩走势下方筛选区域 */
[data-theme="light"] .trend-range-bar {
background: rgba(0, 0, 0, 0.06);
border: 1px solid var(--border);
}
[data-theme="light"] .trend-range-btn {
color: var(--muted);
}
[data-theme="light"] .trend-range-btn:hover {
color: var(--primary);
background: rgba(8, 145, 178, 0.08);
}
[data-theme="light"] .trend-range-btn.active {
background: rgba(8, 145, 178, 0.15);
color: var(--primary);
}
/* 折线图区域:加载/空状态遮罩(暗色默认,亮色主题覆盖) */
.chart-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.02);
z-index: 10;
}
[data-theme="light"] .chart-overlay {
background: rgba(0, 0, 0, 0.03);
}
/* 主题切换时屏幕中心动画 */
.theme-transition-overlay {
position: fixed;
inset: 0;
z-index: 99999;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.theme-transition-circle {
width: 120px;
height: 120px;
border-radius: 50%;
background: var(--primary);
opacity: 0.5;
transform-origin: center;
}
.card .title { .card .title {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -584,6 +950,40 @@ input[type="number"] {
color: var(--accent); color: var(--accent);
} }
/* 业绩走势下方筛选区域近1月/3月/6月/1年/3年 */
.trend-range-bar {
display: flex;
gap: 4px;
margin-top: 12px;
justify-content: space-between;
background: rgba(0, 0, 0, 0.2);
padding: 4px;
border-radius: 8px;
}
.trend-range-btn {
flex: 1;
padding: 6px 0;
font-size: 11px;
border-radius: 6px;
border: none;
background: transparent;
color: var(--muted);
cursor: pointer;
transition: all 0.2s ease;
font-weight: 400;
}
.trend-range-btn:hover {
color: var(--primary);
}
.trend-range-btn.active {
background: rgba(255, 255, 255, 0.1);
color: var(--primary);
font-weight: 600;
}
.empty { .empty {
padding: 24px; padding: 24px;
text-align: center; text-align: center;
@@ -1188,6 +1588,445 @@ input[type="number"] {
padding: 16px; padding: 16px;
} }
/* 定投按钮:暗色主题 */
.dca-btn {
background: rgba(34, 211, 238, 0.12);
border: 1px solid rgba(255, 255, 255, 0.85);
color: #fff;
}
.dca-btn:hover {
background: rgba(34, 211, 238, 0.2);
border-color: rgba(255, 255, 255, 0.95);
}
/* 亮色主题:定投按钮(使用 accent 蓝区分加仓的 primary 青) */
[data-theme="light"] .dca-btn {
background: rgba(37, 99, 235, 0.1);
border: 1px solid var(--accent);
color: var(--accent);
}
[data-theme="light"] .dca-btn:hover {
background: rgba(37, 99, 235, 0.16);
border-color: var(--accent);
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.2);
}
/* TradeModal 基础样式 */
.trade-pending-alert {
margin-bottom: 16px;
background: rgba(230, 162, 60, 0.1);
border: 1px solid rgba(230, 162, 60, 0.2);
border-radius: 8px;
padding: 8px 12px;
font-size: 12px;
color: #e6a23c;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.trade-pending-header {
position: sticky;
top: 0;
z-index: 1;
background: rgba(15, 23, 42, 0.95);
backdrop-filter: blur(6px);
padding-bottom: 8px;
margin-bottom: 8px;
border-bottom: 1px solid var(--border);
}
.trade-pending-item {
background: rgba(255, 255, 255, 0.05);
padding: 12px;
border-radius: 8px;
margin-bottom: 8px;
}
.trade-confirm-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
}
.trade-confirm-divider {
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.trade-preview-card {
padding: 12px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.2);
}
.trade-back-btn,
.trade-cancel-btn,
.trade-revoke-btn {
background: rgba(255, 255, 255, 0.05) !important;
color: var(--text) !important;
}
.trade-time-slot {
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 4px;
}
.trade-time-btn {
flex: 1;
border: none;
background: transparent;
color: var(--muted);
border-radius: 6px;
font-size: 12px;
cursor: pointer;
padding: 6px 8px;
}
.trade-time-btn.active {
background: var(--primary);
color: #05263b;
}
.trade-amount-btn {
flex: 1;
padding: 4px 8px;
font-size: 12px;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 4px;
color: var(--text);
cursor: pointer;
}
/* 亮色主题TradeModal */
[data-theme="light"] .trade-modal .trade-pending-alert {
background: rgba(217, 119, 6, 0.12);
border-color: rgba(217, 119, 6, 0.35);
color: #b45309;
}
[data-theme="light"] .trade-modal .trade-pending-header {
background: rgba(255, 255, 255, 0.98);
border-bottom-color: var(--border);
}
[data-theme="light"] .trade-modal .trade-pending-item {
background: rgba(0, 0, 0, 0.04);
}
[data-theme="light"] .trade-modal .trade-confirm-card {
background: rgba(0, 0, 0, 0.04);
}
[data-theme="light"] .trade-modal .trade-confirm-divider {
border-top-color: var(--border);
}
[data-theme="light"] .trade-modal .trade-preview-card {
background: rgba(0, 0, 0, 0.05);
}
[data-theme="light"] .trade-modal .trade-back-btn,
[data-theme="light"] .trade-modal .trade-cancel-btn,
[data-theme="light"] .trade-modal .trade-revoke-btn {
background: rgba(0, 0, 0, 0.06) !important;
}
[data-theme="light"] .trade-modal .trade-time-slot {
background: rgba(0, 0, 0, 0.06);
}
[data-theme="light"] .trade-modal .trade-time-btn.active {
color: #fff;
}
[data-theme="light"] .trade-modal .trade-amount-btn {
background: rgba(0, 0, 0, 0.06);
}
.trade-pending-status {
color: #e6a23c;
}
[data-theme="light"] .trade-modal .trade-pending-status {
color: #b45309;
}
/* DatePicker 基础样式 */
.date-picker-trigger {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
height: 40px;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s;
}
.date-picker-dropdown {
background: rgba(30, 41, 59, 0.95);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.date-picker-cell {
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
border-radius: 6px;
cursor: pointer;
background: transparent;
color: var(--text);
font-weight: 400;
}
.date-picker-cell.selected {
background: var(--primary);
color: #000;
font-weight: 600;
}
.date-picker-cell.today {
background: rgba(255, 255, 255, 0.1);
font-weight: 600;
}
.date-picker-cell.future {
cursor: not-allowed;
color: var(--muted);
opacity: 0.3;
}
.date-picker-cell:not(.selected):not(.future):hover {
background: rgba(255, 255, 255, 0.1);
}
.date-picker-cell.today:not(.selected):not(.future):hover {
background: rgba(255, 255, 255, 0.1);
}
/* 亮色主题DatePicker */
[data-theme="light"] .date-picker-trigger {
background: rgba(0, 0, 0, 0.06);
border-color: var(--border);
}
[data-theme="light"] .date-picker-dropdown {
background: rgba(255, 255, 255, 0.98);
border-color: var(--border);
}
[data-theme="light"] .date-picker-cell.selected {
color: #fff;
}
[data-theme="light"] .date-picker-cell.today {
background: rgba(8, 145, 178, 0.15);
}
[data-theme="light"] .date-picker-cell:not(.selected):not(.future):hover {
background: rgba(0, 0, 0, 0.06);
}
[data-theme="light"] .date-picker-cell.today:not(.selected):not(.future):hover {
background: rgba(8, 145, 178, 0.2);
}
/* DcaModal 基础样式 */
.dca-toggle-track {
width: 32px;
height: 18px;
border-radius: 999px;
background: rgba(148, 163, 184, 0.6);
position: relative;
transition: background 0.2s;
}
.dca-toggle-track.enabled {
background: var(--primary);
}
.dca-toggle-thumb {
position: absolute;
top: 2px;
width: 14px;
height: 14px;
border-radius: 50%;
background: #0f172a;
transition: left 0.2s;
}
.dca-option-group {
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 4px;
}
.dca-option-btn {
flex: 1;
border: none;
background: transparent;
color: var(--muted);
border-radius: 6px;
font-size: 11px;
cursor: pointer;
padding: 4px 6px;
white-space: nowrap;
}
.dca-option-btn.active {
background: var(--primary);
color: #05263b;
}
.dca-weekday-btn {
font-size: 12px;
padding: 6px 4px;
}
.dca-monthly-day-group {
display: flex;
flex-wrap: wrap;
gap: 4px;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 4px;
max-height: 140px;
overflow-y: auto;
scroll-behavior: smooth;
}
.dca-monthly-btn {
flex: 0 0 calc(25% - 4px);
padding: 4px 0;
}
.dca-first-date-display {
border-radius: 12px;
border: 1px solid var(--border);
padding: 10px 12px;
font-size: 14px;
background: rgba(15, 23, 42, 0.6);
}
.dca-cancel-btn {
background: rgba(255, 255, 255, 0.05) !important;
color: var(--text) !important;
}
/* 亮色主题DcaModal */
[data-theme="light"] .dca-modal .dca-toggle-thumb {
background: #fff;
}
[data-theme="light"] .dca-modal .dca-option-group {
background: rgba(0, 0, 0, 0.06);
}
[data-theme="light"] .dca-modal .dca-option-btn.active {
color: #fff;
}
[data-theme="light"] .dca-modal .dca-monthly-day-group {
background: rgba(0, 0, 0, 0.06);
}
[data-theme="light"] .dca-modal .dca-first-date-display {
background: rgba(0, 0, 0, 0.04);
}
[data-theme="light"] .dca-modal .dca-cancel-btn {
background: rgba(0, 0, 0, 0.06) !important;
}
/* ScanPickModal 拖拽区域 */
.scan-pick-dropzone {
border: 2px dashed var(--border);
background: rgba(255, 255, 255, 0.02);
}
.scan-pick-dropzone.dragging {
border-color: var(--primary);
background: rgba(34, 211, 238, 0.08);
}
/* 亮色主题ScanPickModal 拖拽区域边框更明显 */
[data-theme="light"] .scan-pick-modal .scan-pick-dropzone {
border-color: rgba(0, 0, 0, 0.25);
background: rgba(0, 0, 0, 0.03);
}
[data-theme="light"] .scan-pick-modal .scan-pick-dropzone.dragging {
border-color: var(--primary);
background: rgba(8, 145, 178, 0.12);
}
/* TransactionHistoryModal 基础样式 */
.tx-history-pending-item {
background: rgba(230, 162, 60, 0.1);
border: 1px solid rgba(230, 162, 60, 0.2);
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
}
.tx-history-dca-badge {
font-size: 10px;
padding: 2px 6px;
border-radius: 999px;
background: rgba(34, 197, 94, 0.15);
color: #4ade80;
}
.tx-history-pending-status {
color: #e6a23c;
}
.tx-history-action-btn {
background: rgba(255, 255, 255, 0.1) !important;
color: var(--text) !important;
}
.tx-history-record-item {
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
}
/* 亮色主题TransactionHistoryModal */
[data-theme="light"] .tx-history-modal .tx-history-pending-item {
background: rgba(217, 119, 6, 0.12);
border-color: rgba(217, 119, 6, 0.35);
}
[data-theme="light"] .tx-history-modal .tx-history-dca-badge {
background: rgba(5, 150, 105, 0.15);
color: #059669;
}
[data-theme="light"] .tx-history-modal .tx-history-pending-status {
color: #b45309;
}
[data-theme="light"] .tx-history-modal .tx-history-action-btn {
background: rgba(0, 0, 0, 0.06) !important;
}
[data-theme="light"] .tx-history-modal .tx-history-record-item {
background: rgba(0, 0, 0, 0.04);
}
.pending-list { .pending-list {
scrollbar-width: none; scrollbar-width: none;
} }
@@ -1392,6 +2231,17 @@ input[type="number"] {
transform: rotate(-45deg) translateY(-1px); transform: rotate(-45deg) translateY(-1px);
} }
/* 亮色主题checkbox 边框更明显,选中时勾为白色 */
[data-theme="light"] .checkbox {
border-color: rgba(0, 0, 0, 0.28);
}
[data-theme="light"] .search-item.selected .checkbox .checked-mark,
[data-theme="light"] .group-manage-item.selected .checkbox .checked-mark {
border-left-color: #fff;
border-bottom-color: #fff;
}
.added-label { .added-label {
font-size: 11px; font-size: 11px;
padding: 2px 8px; padding: 2px 8px;

View File

@@ -11,9 +11,15 @@ export default function RootLayout({ children }) {
const GA_ID = process.env.NEXT_PUBLIC_GA_ID; const GA_ID = process.env.NEXT_PUBLIC_GA_ID;
return ( return (
<html lang="zh-CN"> <html lang="zh-CN" suppressHydrationWarning>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
{/* 尽早设置 data-theme减少首屏主题闪烁与 suppressHydrationWarning 配合避免服务端/客户端 html 属性不一致报错 */}
<script
dangerouslySetInnerHTML={{
__html: `(function(){try{var t=localStorage.getItem("theme");if(t==="light"||t==="dark")document.documentElement.setAttribute("data-theme",t);}catch(e){}})();`,
}}
/>
</head> </head>
<body> <body>
<AnalyticsGate GA_ID={GA_ID} /> <AnalyticsGate GA_ID={GA_ID} />

View File

@@ -15,7 +15,7 @@ import Announcement from "./components/Announcement";
import { Stat } from "./components/Common"; import { Stat } from "./components/Common";
import FundTrendChart from "./components/FundTrendChart"; import FundTrendChart from "./components/FundTrendChart";
import FundIntradayChart from "./components/FundIntradayChart"; import FundIntradayChart from "./components/FundIntradayChart";
import { ChevronIcon, CloseIcon, ExitIcon, EyeIcon, EyeOffIcon, GridIcon, ListIcon, LoginIcon, LogoutIcon, PinIcon, PinOffIcon, PlusIcon, RefreshIcon, SettingsIcon, SortIcon, StarIcon, TrashIcon, UpdateIcon, UserIcon, CameraIcon } from "./components/Icons"; import { ChevronIcon, CloseIcon, ExitIcon, EyeIcon, EyeOffIcon, GridIcon, ListIcon, LoginIcon, LogoutIcon, MoonIcon, PinIcon, PinOffIcon, PlusIcon, RefreshIcon, SettingsIcon, SortIcon, StarIcon, SunIcon, TrashIcon, UpdateIcon, UserIcon, CameraIcon } from "./components/Icons";
import AddFundToGroupModal from "./components/AddFundToGroupModal"; import AddFundToGroupModal from "./components/AddFundToGroupModal";
import AddResultModal from "./components/AddResultModal"; import AddResultModal from "./components/AddResultModal";
import CloudConfigModal from "./components/CloudConfigModal"; import CloudConfigModal from "./components/CloudConfigModal";
@@ -391,6 +391,30 @@ export default function HomePage() {
const filterBarRef = useRef(null); const filterBarRef = useRef(null);
const [navbarHeight, setNavbarHeight] = useState(0); const [navbarHeight, setNavbarHeight] = useState(0);
const [filterBarHeight, setFilterBarHeight] = useState(0); const [filterBarHeight, setFilterBarHeight] = useState(0);
// 主题初始固定为 dark避免 SSR 与客户端首屏不一致导致 hydration 报错;真实偏好由 useLayoutEffect 在首帧前恢复
const [theme, setTheme] = useState('dark');
const [showThemeTransition, setShowThemeTransition] = useState(false);
const handleThemeToggle = useCallback(() => {
setTheme((t) => (t === 'dark' ? 'light' : 'dark'));
setShowThemeTransition(true);
}, []);
// 首帧前同步主题(与 layout 中脚本设置的 data-theme 一致),减少图标闪烁
useLayoutEffect(() => {
try {
const fromDom = document.documentElement.getAttribute('data-theme');
if (fromDom === 'light' || fromDom === 'dark') {
setTheme(fromDom);
return;
}
const fromStorage = localStorage.getItem('theme');
if (fromStorage === 'light' || fromStorage === 'dark') {
setTheme(fromStorage);
document.documentElement.setAttribute('data-theme', fromStorage);
}
} catch { }
}, []);
useEffect(() => { useEffect(() => {
const updateHeights = () => { const updateHeights = () => {
@@ -1874,9 +1898,21 @@ export default function HomePage() {
if (savedViewMode === 'card' || savedViewMode === 'list') { if (savedViewMode === 'card' || savedViewMode === 'list') {
setViewMode(savedViewMode); setViewMode(savedViewMode);
} }
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light' || savedTheme === 'dark') {
setTheme(savedTheme);
}
} catch { } } catch { }
}, []); }, []);
// 主题同步到 document 并持久化
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
try {
localStorage.setItem('theme', theme);
} catch { }
}, [theme]);
// 初始化认证状态监听 // 初始化认证状态监听
useEffect(() => { useEffect(() => {
if (!isSupabaseConfigured) { if (!isSupabaseConfigured) {
@@ -3251,6 +3287,24 @@ export default function HomePage() {
return ( return (
<div className="container content"> <div className="container content">
<AnimatePresence>
{showThemeTransition && (
<motion.div
className="theme-transition-overlay"
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<motion.div
className="theme-transition-circle"
initial={{ scale: 0, opacity: 0.5 }}
animate={{ scale: 2.5, opacity: 0 }}
transition={{ duration: 1, ease: [0.22, 1, 0.36, 1] }}
onAnimationComplete={() => setShowThemeTransition(false)}
/>
</motion.div>
)}
</AnimatePresence>
<Announcement /> <Announcement />
<div className="navbar glass" ref={navbarRef}> <div className="navbar glass" ref={navbarRef}>
{refreshing && <div className="loading-bar"></div>} {refreshing && <div className="loading-bar"></div>}
@@ -3434,7 +3488,9 @@ export default function HomePage() {
<UpdateIcon width="14" height="14" /> <UpdateIcon width="14" height="14" />
</div> </div>
)} )}
<Image unoptimized alt="项目Github地址" src={githubImg} style={{ width: '30px', height: '30px', cursor: 'pointer' }} onClick={() => window.open("https://github.com/hzm0321/real-time-fund")} /> <span className="github-icon-wrap">
<Image unoptimized alt="项目Github地址" src={githubImg} style={{ width: '30px', height: '30px', cursor: 'pointer' }} onClick={() => window.open("https://github.com/hzm0321/real-time-fund")} />
</span>
{isMobile && ( {isMobile && (
<button <button
className="icon-button mobile-search-btn" className="icon-button mobile-search-btn"
@@ -3464,6 +3520,14 @@ export default function HomePage() {
{/*>*/} {/*>*/}
{/* <SettingsIcon width="18" height="18" />*/} {/* <SettingsIcon width="18" height="18" />*/}
{/*</button>*/} {/*</button>*/}
<button
className="icon-button"
aria-label={theme === 'dark' ? '切换到亮色主题' : '切换到暗色主题'}
onClick={handleThemeToggle}
title={theme === 'dark' ? '亮色' : '暗色'}
>
{theme === 'dark' ? <SunIcon width="18" height="18" /> : <MoonIcon width="18" height="18" />}
</button>
{/* 用户菜单 */} {/* 用户菜单 */}
<div className="user-menu-container" ref={userMenuRef}> <div className="user-menu-container" ref={userMenuRef}>
<button <button
@@ -4313,8 +4377,10 @@ export default function HomePage() {
return ( return (
<FundIntradayChart <FundIntradayChart
key={`${f.code}-intraday-${theme}`}
series={valuationSeries[f.code]} series={valuationSeries[f.code]}
referenceNav={f.dwjz != null ? Number(f.dwjz) : undefined} referenceNav={f.dwjz != null ? Number(f.dwjz) : undefined}
theme={theme}
/> />
); );
})()} })()}
@@ -4371,10 +4437,12 @@ export default function HomePage() {
)} )}
</AnimatePresence> </AnimatePresence>
<FundTrendChart <FundTrendChart
key={`${f.code}-${theme}`}
code={f.code} code={f.code}
isExpanded={!collapsedTrends.has(f.code)} isExpanded={!collapsedTrends.has(f.code)}
onToggleExpand={() => toggleTrendCollapse(f.code)} onToggleExpand={() => toggleTrendCollapse(f.code)}
transactions={transactions[f.code] || []} transactions={transactions[f.code] || []}
theme={theme}
/> />
</> </>
)} )}