feat:亮色主题
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
850
app/globals.css
850
app/globals.css
@@ -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;
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
70
app/page.jsx
70
app/page.jsx
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
<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")} />
|
<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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user