Skip to content

Commit 4d89463

Browse files
committed
[refactor] split Spark Line component into an independent module
[optimize] simplify CSS modules & Finance page [migrate] upgrade to Next.js 16, Koa Router 15, Marked 17, ECharts-JSX 0.6 & other latest Upstream packages
1 parent 3a1f964 commit 4d89463

File tree

15 files changed

+1931
-1880
lines changed

15 files changed

+1931
-1880
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
## 技术栈
1212

1313
- Language: [TypeScript v5][2]
14-
- Component engine: [Nextjs v15][3]
14+
- Component engine: [Nextjs v16][3]
1515
- Component suite: [Bootstrap v5][4]
1616
- CI / CD: GitHub [Actions][10] + [Vercel][11]
1717

components/Finance/Finance.module.less

Lines changed: 12 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,149 +1,27 @@
1-
.financePage {
2-
padding-bottom: 4rem;
3-
}
4-
5-
.hero {
6-
border: 1px solid rgba(0, 0, 0, 0.05);
7-
border-radius: 32px;
8-
background: linear-gradient(135deg, rgba(13, 110, 253, 0.08), rgba(25, 135, 84, 0.08));
9-
padding-right: clamp(1.5rem, 4vw, 3rem);
10-
padding-left: clamp(1.5rem, 4vw, 3rem);
11-
}
12-
13-
.heroBadge {
14-
letter-spacing: 0.2em;
15-
text-transform: uppercase;
16-
}
17-
18-
.heroChip {
19-
border-radius: 999px;
20-
background: rgba(13, 110, 253, 0.08);
21-
padding: 0.35rem 0.9rem;
22-
font-weight: 600;
23-
font-size: 0.9rem;
24-
}
25-
26-
.heroStats {
27-
display: grid;
28-
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
29-
gap: 1rem;
30-
}
31-
32-
.heroStatCard {
33-
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.1);
34-
border: 1px solid rgba(15, 23, 42, 0.05);
35-
border-radius: 20px;
36-
background: #fff;
37-
padding: 1.5rem;
38-
}
39-
40-
.filterPanel {
41-
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.08);
42-
border: 1px solid rgba(15, 23, 42, 0.05);
43-
border-radius: 24px;
44-
background: #fff;
45-
padding: 2rem;
46-
}
47-
48-
.segmentedControl {
49-
display: grid;
50-
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
51-
gap: 0.75rem;
52-
}
53-
54-
.segmentedButton {
55-
transition: all 0.2s ease;
56-
border: 1px solid rgba(15, 23, 42, 0.15);
57-
border-radius: 16px;
58-
background: #f5f7fb;
59-
padding: 0.75rem 1rem;
60-
font-weight: 600;
61-
text-align: left;
62-
}
63-
64-
.segmentedButtonActive {
65-
box-shadow: 0 10px 30px rgba(13, 110, 253, 0.15);
66-
border-color: rgba(13, 110, 253, 0.5);
67-
background: linear-gradient(135deg, rgba(13, 110, 253, 0.12), rgba(25, 135, 84, 0.12));
68-
color: #0d6efd;
69-
}
70-
71-
.listSection {
72-
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.08);
73-
border: 1px solid rgba(15, 23, 42, 0.05);
74-
border-radius: 24px;
75-
background: #fff;
76-
padding: 2rem;
77-
}
78-
79-
.emptyState {
80-
border: 1px dashed rgba(15, 23, 42, 0.2);
81-
border-radius: 20px;
82-
background: rgba(248, 249, 250, 0.7);
83-
padding: 2.5rem;
84-
text-align: center;
85-
}
86-
87-
.educationSection {
88-
border-radius: 28px;
89-
background: rgba(13, 110, 253, 0.05);
90-
padding: 2rem;
91-
}
92-
93-
.educationCard {
94-
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
95-
border: 1px solid rgba(15, 23, 42, 0.05);
96-
border-radius: 20px;
97-
background: #fff;
98-
padding: 1.75rem;
99-
height: 100%;
100-
}
101-
1021
.fundCard {
1032
transition:
1043
transform 0.2s ease,
1054
box-shadow 0.2s ease;
1065
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08);
1076
border: 1px solid rgba(15, 23, 42, 0.08);
1087
border-radius: 20px;
109-
}
8+
&:hover {
9+
transform: translateY(-4px);
10+
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.12);
11+
}
11012

111-
.fundCard:hover {
112-
transform: translateY(-4px);
113-
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.12);
13+
.header small {
14+
line-height: 1.3;
15+
}
11416
}
11517

116-
.fundCardHeader small {
117-
line-height: 1.3;
118-
}
119-
120-
.metricRow {
18+
.metric {
12119
gap: 1.5rem;
20+
.value {
21+
font-weight: 700;
22+
font-size: 2rem;
23+
}
12224
}
123-
124-
.metricValue {
125-
font-weight: 700;
126-
font-size: 2rem;
127-
}
128-
129-
.sparkline {
130-
width: 100%;
131-
height: 80px;
132-
}
133-
134-
.sparkline svg {
135-
width: 100%;
136-
height: 100%;
137-
}
138-
139-
.sparklinePlaceholder {
140-
border: 1px dashed rgba(15, 23, 42, 0.2);
141-
border-radius: 12px;
142-
padding: 1rem;
143-
color: rgba(15, 23, 42, 0.6);
144-
text-align: center;
145-
}
146-
14725
.tagBadge {
14826
border: 1px solid rgba(15, 23, 42, 0.15);
14927
border-radius: 999px;

components/Finance/FundCard.tsx

Lines changed: 9 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,76 +3,19 @@ import { Badge, Card } from 'react-bootstrap';
33

44
import { INDEX_CATEGORY_LABEL_KEYS, INDEX_RISK_LABEL_KEYS } from '../../constants/finance';
55
import { I18nContext } from '../../models/Translation';
6-
import { IndexFundSnapshot, IndexHistoryPoint } from '../../types/finance';
6+
import { IndexFundSnapshot } from '../../types/finance';
77
import styles from './Finance.module.less';
8+
import { SparkLine } from './SparkLine';
89

9-
export interface FundCardProps {
10-
data: IndexFundSnapshot;
11-
}
12-
13-
const formatNumber = (value?: number | null) =>
14-
value != null ? value.toLocaleString('zh-CN', { maximumFractionDigits: 2 }) : '--';
10+
const formatNumber = (value?: number | null, locale = 'zh-CN') =>
11+
value != null ? value.toLocaleString(locale, { maximumFractionDigits: 2 }) : '--';
1512

1613
const formatPercent = (value?: number | null) =>
1714
value != null ? `${(value * 100).toFixed(2)}%` : '--';
1815

1916
const valueTone = (value?: number | null) =>
2017
value != null ? (value >= 0 ? styles.positiveText : styles.negativeText) : styles.mutedText;
2118

22-
const Sparkline: FC<{ points: IndexHistoryPoint[]; chartId: string }> = ({ points, chartId }) => {
23-
const { t } = useContext(I18nContext);
24-
25-
const { polyline, gradientStops } = useMemo(() => {
26-
if (!points.length) return { polyline: '', gradientStops: [] as number[] };
27-
28-
const values = points.map(({ value }) => value);
29-
const min = Math.min(...values);
30-
const max = Math.max(...values);
31-
const delta = max - min || 1;
32-
33-
const polylinePoints = points
34-
.map(({ value }, index, { length }) => {
35-
const x = (index / Math.max(length - 1, 1)) * 100;
36-
const y = ((max - value) / delta) * 40;
37-
38-
return `${x.toFixed(2)},${y.toFixed(2)}`;
39-
})
40-
.join(' ');
41-
42-
const gradientOffsets = [0, 50, 100];
43-
44-
return { polyline: polylinePoints, gradientStops: gradientOffsets };
45-
}, [points]);
46-
const gradientId = `${chartId}-gradient`;
47-
48-
return (
49-
<div className={styles.sparkline}>
50-
<svg viewBox="0 0 100 40" role="img" aria-label={t('index_sparkline_60d_label')}>
51-
<defs>
52-
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
53-
{gradientStops.map(offset => (
54-
<stop
55-
key={offset}
56-
offset={`${offset}%`}
57-
stopColor="var(--bs-primary)"
58-
stopOpacity="0.5"
59-
/>
60-
))}
61-
</linearGradient>
62-
</defs>
63-
<polyline
64-
fill="none"
65-
stroke={`url(#${gradientId})`}
66-
strokeWidth="2"
67-
strokeLinejoin="round"
68-
strokeLinecap="round"
69-
points={polyline}
70-
/>
71-
</svg>
72-
</div>
73-
);
74-
};
75-
7619
export const FundCard: FC<IndexFundSnapshot> = ({
7720
symbol,
7821
displayName,
@@ -89,16 +32,14 @@ export const FundCard: FC<IndexFundSnapshot> = ({
8932
description,
9033
source,
9134
}) => {
92-
const { t } = useContext(I18nContext);
35+
const { currentLanguage, t } = useContext(I18nContext);
9336
const sparklineId = useMemo(() => `fund-${symbol}`, [symbol]);
9437
const updatedAtISO = updatedAt ? new Date(updatedAt).toJSON() : undefined;
9538

9639
return (
9740
<Card className={`${styles.fundCard} h-100`}>
9841
<Card.Body className="d-flex flex-column gap-3">
99-
<div
100-
className={`${styles.fundCardHeader} d-flex justify-content-between align-items-start`}
101-
>
42+
<div className={`${styles.header} d-flex justify-content-between align-items-start`}>
10243
<div>
10344
<h3 className="h5 mb-1">{displayName}</h3>
10445
<div className="d-flex flex-wrap gap-2 align-items-center">
@@ -131,9 +72,9 @@ export const FundCard: FC<IndexFundSnapshot> = ({
13172

13273
<p className="text-muted mb-0">{description}</p>
13374

134-
<dl className={`${styles.metricRow} d-flex flex-wrap gap-4 align-items-center`}>
75+
<dl className={`${styles.metric} d-flex flex-wrap gap-4 align-items-center`}>
13576
<dt className="text-muted mb-1">{t('index_metric_latest_value')}</dt>
136-
<dd className={styles.metricValue}>{formatNumber(latestValue)}</dd>
77+
<dd className={styles.value}>{formatNumber(latestValue, currentLanguage)}</dd>
13778

13879
<dt className="text-muted mb-1">{t('index_metric_daily_change')}</dt>
13980
<dd className={valueTone(dailyChangePct)}>{formatPercent(dailyChangePct)}</dd>
@@ -145,11 +86,7 @@ export const FundCard: FC<IndexFundSnapshot> = ({
14586
<dd className={valueTone(maxDrawdownPct)}>{formatPercent(maxDrawdownPct)}</dd>
14687
</dl>
14788

148-
{sparkline.length ? (
149-
<Sparkline points={sparkline} chartId={sparklineId} />
150-
) : (
151-
<div className={styles.sparklinePlaceholder}>{t('data_preparing')}</div>
152-
)}
89+
<SparkLine points={sparkline} chartId={sparklineId} />
15390

15491
<ul className="list-unstyled m-0 d-flex flex-wrap gap-2">
15592
{tags?.map(tag => (
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.sparkline {
2+
width: 100%;
3+
height: 80px;
4+
5+
svg {
6+
width: 100%;
7+
height: 100%;
8+
}
9+
10+
.placeholder {
11+
border: 1px dashed rgba(15, 23, 42, 0.2);
12+
border-radius: 12px;
13+
padding: 1rem;
14+
color: rgba(15, 23, 42, 0.6);
15+
text-align: center;
16+
}
17+
}

0 commit comments

Comments
 (0)