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

View File

@@ -185,7 +185,7 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="glass card modal"
className="glass card modal dca-modal"
onClick={(e) => e.stopPropagation()}
style={{ maxWidth: '420px' }}
>
@@ -220,28 +220,8 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
gap: 6
}}
>
<span
style={{
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 className={`dca-toggle-track ${enabled ? 'enabled' : ''}`}>
<span className="dca-toggle-thumb" style={{ left: enabled ? 16 : 2 }} />
</span>
<span style={{ fontSize: 12, color: enabled ? 'var(--primary)' : 'var(--muted)' }}>
{enabled ? '已启用' : '未启用'}
@@ -284,23 +264,13 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
定投周期 <span style={{ color: 'var(--danger)' }}>*</span>
</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) => (
<button
key={opt.value}
type="button"
className={`dca-option-btn ${cycle === opt.value ? 'active' : ''}`}
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}
</button>
@@ -314,23 +284,13 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
扣款星期 <span style={{ color: 'var(--danger)' }}>*</span>
</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) => (
<button
key={opt.value}
type="button"
className={`dca-option-btn dca-weekday-btn ${weeklyDay === opt.value ? 'active' : ''}`}
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}
</button>
@@ -344,20 +304,7 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
扣款日 <span style={{ color: 'var(--danger)' }}>*</span>
</label>
<div
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'
}}
>
<div className="dca-monthly-day-group scrollbar-y-styled">
{Array.from({ length: 28 }).map((_, idx) => {
const day = idx + 1;
const active = monthlyDay === day;
@@ -366,17 +313,8 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
key={day}
ref={active ? monthlyDayRef : null}
type="button"
className={`dca-option-btn dca-monthly-btn ${active ? 'active' : ''}`}
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}
</button>
@@ -390,15 +328,7 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
<label className="muted" style={{ display: 'block', marginBottom: 4, fontSize: '14px' }}>
首次扣款日期
</label>
<div
style={{
borderRadius: 12,
border: '1px solid var(--border)',
padding: '10px 12px',
fontSize: 14,
background: 'rgba(15,23,42,0.6)'
}}
>
<div className="dca-first-date-display">
{firstDate}
</div>
<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 }}>
<button
type="button"
className="button secondary"
className="button secondary dca-cancel-btn"
onClick={onClose}
style={{ flex: 1, background: 'rgba(255,255,255,0.05)', color: 'var(--text)' }}
style={{ flex: 1 }}
>
取消
</button>

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,10 +40,6 @@ export default function ScanPickModal({ onClose, onPick, onFilesDrop, isScanning
marginBottom: 12,
padding: '20px 16px',
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',
cursor: isScanning ? 'not-allowed' : 'pointer',
pointerEvents: isScanning ? 'none' : 'auto',
@@ -64,7 +60,7 @@ export default function ScanPickModal({ onClose, onPick, onFilesDrop, isScanning
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="glass card modal"
className="glass card modal scan-pick-modal"
onClick={(e) => e.stopPropagation()}
style={{ width: 420, maxWidth: '90vw' }}
>
@@ -75,7 +71,7 @@ export default function ScanPickModal({ onClose, onPick, onFilesDrop, isScanning
从相册选择一张或多张持仓截图系统将自动识别其中的<span style={{ color: 'var(--primary)' }}>基金代码6位数字</span>并支持批量导入
</div>
<div
className="muted"
className={`scan-pick-dropzone muted ${isDragging ? 'dragging' : ''}`}
style={dropZoneStyle}
onDragOver={handleDragOver}
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 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="glass card modal"
className="glass card modal trade-modal"
onClick={(e) => e.stopPropagation()}
style={{ maxWidth: '420px' }}
>
@@ -184,19 +184,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
{!showPendingList && !showConfirm && currentPendingTrades.length > 0 && (
<div
style={{
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'
}}
className="trade-pending-alert"
onClick={() => setShowPendingList(true)}
>
<span> 当前有 {currentPendingTrades.length} 笔待处理交易</span>
@@ -206,7 +194,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
{showPendingList ? (
<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
className="button secondary"
onClick={() => setShowPendingList(false)}
@@ -217,7 +205,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
</div>
<div className="pending-list-items" style={{ paddingTop: 0 }}>
{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 }}>
<span style={{ fontWeight: 600, fontSize: '14px', color: trade.type === 'buy' ? 'var(--danger)' : 'var(--success)' }}>
{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 }}>
<span className="muted">状态</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ color: '#e6a23c' }}>等待净值更新...</span>
<span className="trade-pending-status">等待净值更新...</span>
<button
className="button secondary"
className="button secondary trade-revoke-btn"
onClick={() => setRevokeTrade(trade)}
style={{
padding: '2px 8px',
fontSize: '10px',
height: 'auto',
background: 'rgba(255,255,255,0.1)',
color: 'var(--text)'
}}
style={{ padding: '2px 8px', fontSize: '10px', height: 'auto' }}
>
撤销
</button>
@@ -263,7 +245,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
{showConfirm ? (
isBuy ? (
<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 }}>
<span className="muted">基金名称</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>{date}</span>
</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>{isAfter3pm ? '15:00后' : '15:00前'}</span>
</div>
@@ -301,7 +283,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
<div style={{ marginBottom: 20 }}>
<div className="muted" style={{ marginBottom: 8, fontSize: '12px' }}>持仓变化预览</div>
<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 style={{ fontSize: '12px' }}>
<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>
{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 style={{ fontSize: '12px' }}>
<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 }}>
<button
type="button"
className="button secondary"
className="button secondary trade-back-btn"
onClick={() => setShowConfirm(false)}
style={{ flex: 1, background: 'rgba(255,255,255,0.05)', color: 'var(--text)' }}
style={{ flex: 1 }}
>
返回修改
</button>
@@ -345,7 +327,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
</div>
) : (
<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 }}>
<span className="muted">基金名称</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>{date}</span>
</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 style={{ color: 'var(--danger)', fontWeight: 700 }}>{loadingPrice ? '计算中...' : (price ? `¥${estimatedReturn.toFixed(2)}` : '待计算')}</span>
</div>
@@ -383,7 +365,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
<div style={{ marginBottom: 20 }}>
<div className="muted" style={{ marginBottom: 8, fontSize: '12px' }}>持仓变化预览</div>
<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 style={{ fontSize: '12px' }}>
<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>
{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 style={{ fontSize: '12px' }}>
<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 }}>
<button
type="button"
className="button secondary"
className="button secondary trade-back-btn"
onClick={() => setShowConfirm(false)}
style={{ flex: 1, background: 'rgba(255,255,255,0.05)', color: 'var(--text)' }}
style={{ flex: 1 }}
>
返回修改
</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>
<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
type="button"
className={!isAfter3pm ? 'trade-time-btn active' : 'trade-time-btn'}
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
</button>
<button
type="button"
className={isAfter3pm ? 'trade-time-btn active' : 'trade-time-btn'}
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
</button>
@@ -544,17 +508,8 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
<button
key={opt.label}
type="button"
className="trade-amount-btn"
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}
</button>
@@ -563,7 +518,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
)}
{holding && (
<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>
@@ -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>
<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
type="button"
className={!isAfter3pm ? 'trade-time-btn active' : 'trade-time-btn'}
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
</button>
<button
type="button"
className={isAfter3pm ? 'trade-time-btn active' : 'trade-time-btn'}
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
</button>
@@ -663,7 +600,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
)}
<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
type="submit"
className="button"

View File

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

View File

@@ -8,6 +8,23 @@
--success: #34d399;
--danger: #f87171;
--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 {
max-width: 90%;
width: 1200px;
@@ -67,6 +100,339 @@ body::before {
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 {
display: flex;
align-items: center;
@@ -584,6 +950,40 @@ input[type="number"] {
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 {
padding: 24px;
text-align: center;
@@ -1188,6 +1588,445 @@ input[type="number"] {
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 {
scrollbar-width: none;
}
@@ -1392,6 +2231,17 @@ input[type="number"] {
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 {
font-size: 11px;
padding: 2px 8px;

View File

@@ -11,9 +11,15 @@ export default function RootLayout({ children }) {
const GA_ID = process.env.NEXT_PUBLIC_GA_ID;
return (
<html lang="zh-CN">
<html lang="zh-CN" suppressHydrationWarning>
<head>
<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>
<body>
<AnalyticsGate GA_ID={GA_ID} />

View File

@@ -15,7 +15,7 @@ import Announcement from "./components/Announcement";
import { Stat } from "./components/Common";
import FundTrendChart from "./components/FundTrendChart";
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 AddResultModal from "./components/AddResultModal";
import CloudConfigModal from "./components/CloudConfigModal";
@@ -391,6 +391,30 @@ export default function HomePage() {
const filterBarRef = useRef(null);
const [navbarHeight, setNavbarHeight] = 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(() => {
const updateHeights = () => {
@@ -1874,9 +1898,21 @@ export default function HomePage() {
if (savedViewMode === 'card' || savedViewMode === 'list') {
setViewMode(savedViewMode);
}
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light' || savedTheme === 'dark') {
setTheme(savedTheme);
}
} catch { }
}, []);
// 主题同步到 document 并持久化
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
try {
localStorage.setItem('theme', theme);
} catch { }
}, [theme]);
// 初始化认证状态监听
useEffect(() => {
if (!isSupabaseConfigured) {
@@ -3251,6 +3287,24 @@ export default function HomePage() {
return (
<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 />
<div className="navbar glass" ref={navbarRef}>
{refreshing && <div className="loading-bar"></div>}
@@ -3434,7 +3488,9 @@ export default function HomePage() {
<UpdateIcon width="14" height="14" />
</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 && (
<button
className="icon-button mobile-search-btn"
@@ -3464,6 +3520,14 @@ export default function HomePage() {
{/*>*/}
{/* <SettingsIcon width="18" height="18" />*/}
{/*</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}>
<button
@@ -4313,8 +4377,10 @@ export default function HomePage() {
return (
<FundIntradayChart
key={`${f.code}-intraday-${theme}`}
series={valuationSeries[f.code]}
referenceNav={f.dwjz != null ? Number(f.dwjz) : undefined}
theme={theme}
/>
);
})()}
@@ -4371,10 +4437,12 @@ export default function HomePage() {
)}
</AnimatePresence>
<FundTrendChart
key={`${f.code}-${theme}`}
code={f.code}
isExpanded={!collapsedTrends.has(f.code)}
onToggleExpand={() => toggleTrendCollapse(f.code)}
transactions={transactions[f.code] || []}
theme={theme}
/>
</>
)}