Files
qlib/frontend/web/components/icons.tsx
T
Simon 24f98e90c6 feat(web): 按 ui-ux-pro-max 设计系统重构前端(侧边导航 + 组件库 + 图标 + 可视化)
- 设计令牌与组件体系:语义色/间距/圆角/状态 + Card/Metric/Pill/Btn/Field/Empty/Skeleton/Progress 等
- 桌面左侧导航 + 窄屏抽屉(usePathname 高亮、ESC/遮罩关闭);修复抽屉/遮罩占据网格把 main 挤到第二行的问题
- 自绘内联 SVG 图标集替代 emoji;图标/表格/按钮等交互态齐全,空态/加载/错误/禁用态补齐
- 因子研究分层柱状、因子组合与回测净值/回撤双图 + 月度收益热力表、实验页任务状态徽标
- LineChart 主题化并按需引入 echarts(回测/组合首载 JS 448k→281k);next.config 放行局域网 dev 来源
2026-09-08 21:48:20 +08:00

241 lines
6.8 KiB
TypeScript

/**
* 内联 SVG 图标集(Phosphor 风格线性图标,1.6px 描边,stroke=currentColor)。
* 不引入第三方图标库(离线可用),禁止用 emoji 作结构图标。
*
* 用法:<Icon name="search" size={16} /> 或 <Icon.Home .../>
*/
import type { ReactNode, SVGProps } from "react";
export type IconName =
| "logo"
| "grid"
| "candles"
| "flask"
| "layers"
| "gauge"
| "archive"
| "search"
| "x"
| "chevDown"
| "chevRight"
| "arrowUpRight"
| "arrowUp"
| "arrowDown"
| "play"
| "refresh"
| "spinner"
| "alert"
| "check"
| "info"
| "database"
| "calendar"
| "target"
| "chartLine"
| "scale"
| "book"
| "link"
| "spark"
| "clock"
| "filter";
const P: Record<
IconName,
(p: { filled?: boolean }) => ReactNode
> = {
logo: () => (
<>
<path d="M4 20V4h4v6h4V4h4v10h4V4h4v16H4z" fill="currentColor" stroke="none" />
<path d="M3 20h18" strokeWidth="2.4" strokeLinecap="round" />
</>
),
grid: () => (
<>
<rect x="3.5" y="3.5" width="7" height="7" rx="1.6" />
<rect x="13.5" y="3.5" width="7" height="7" rx="1.6" />
<rect x="3.5" y="13.5" width="7" height="7" rx="1.6" />
<rect x="13.5" y="13.5" width="7" height="7" rx="1.6" />
</>
),
candles: () => (
<>
<path d="M7 3v3.2M7 17.8V21M7 6.2a1.8 1.8 0 0 1 1.8 1.8v6a1.8 1.8 0 0 1-1.8 1.8v0a1.8 1.8 0 0 1-1.8-1.8V8a1.8 1.8 0 0 1 1.8-1.8z" />
<path d="M17 3v2M17 19v2M17 5a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2v0a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z" />
</>
),
flask: () => (
<>
<path d="M9.5 3h5M10.5 3v6.2L5.2 18.6A2 2 0 0 0 6.9 21.5h10.2a2 2 0 0 0 1.7-2.9L13.5 9.2V3" />
<path d="M7.2 15.5h9.6" />
</>
),
layers: () => (
<>
<path d="M12 3.5 21 8.5l-9 5-9-5 9-5z" />
<path d="m4.5 12.5 7.5 4.2 7.5-4.2M4.5 16.5l7.5 4.2 7.5-4.2" />
</>
),
gauge: () => (
<>
<path d="M12 21a9 9 0 1 1 9-9" />
<path d="M12 12.5 16.6 6.8" />
<circle cx="12" cy="12.5" r="1.5" fill="currentColor" stroke="none" />
</>
),
archive: () => (
<>
<rect x="3.5" y="4" width="17" height="5" rx="1.6" />
<path d="M5.5 9v9.5a1.5 1.5 0 0 0 1.5 1.5h10a1.5 1.5 0 0 0 1.5-1.5V9" />
<path d="M10 13.5h4" />
</>
),
search: () => (
<>
<circle cx="11" cy="11" r="6.5" />
<path d="m20 20-4.4-4.4" strokeLinecap="round" />
</>
),
x: () => (
<>
<path d="m6 6 12 12M18 6 6 18" strokeLinecap="round" />
</>
),
chevDown: () => <path d="m6 9.5 6 6 6-6" strokeLinecap="round" strokeLinejoin="round" />,
chevRight: () => <path d="m9.5 6 6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />,
arrowUpRight: () => (
<>
<path d="M7 17 17 7M9 7h8v8" strokeLinecap="round" strokeLinejoin="round" />
</>
),
arrowUp: () => <path d="M12 19V5M5.5 11.5 12 5l6.5 6.5" strokeLinecap="round" strokeLinejoin="round" />,
arrowDown: () => <path d="M12 5v14M18.5 12.5 12 19l-6.5-6.5" strokeLinecap="round" strokeLinejoin="round" />,
play: ({ filled }) =>
filled ? (
<path d="M8 5.5v13a1 1 0 0 0 1.5.9l11-6.5a1 1 0 0 0 0-1.8l-11-6.5A1 1 0 0 0 8 5.5z" fill="currentColor" stroke="none" />
) : (
<path d="M8.5 5.8v12.4a.6.6 0 0 0 .9.5l10-6.2a.6.6 0 0 0 0-1L9.4 5.3a.6.6 0 0 0-.9.5z" fill="currentColor" stroke="none" />
),
refresh: () => (
<>
<path d="M20 12a8 8 0 1 1-2.3-5.6" strokeLinecap="round" />
<path d="M20 3.5V6.6a.4.4 0 0 1-.4.4H16.5" strokeLinecap="round" strokeLinejoin="round" />
</>
),
spinner: () => (
<>
<path d="M12 3a9 9 0 1 0 9 9" strokeLinecap="round" />
</>
),
alert: ({ filled }) =>
filled ? (
<path
d="M12 2.6 23 20.8a1 1 0 0 1-.9 1.5H1.9a1 1 0 0 1-.9-1.5L12 2.6zM12 10v4.6m0 3.1v.1"
fill="currentColor"
stroke="none"
opacity="0.25"
/>
) : (
<>
<path d="M12 3.2 21.7 20a1 1 0 0 1-.85 1.5H3.15A1 1 0 0 1 2.3 20L12 3.2z" />
<path d="M12 9.5v4.4M12 17.4v.1" strokeLinecap="round" />
</>
),
check: () => <path d="m5 12.5 4.5 4.5L19 7.5" strokeLinecap="round" strokeLinejoin="round" />,
info: () => (
<>
<circle cx="12" cy="12" r="9" />
<path d="M12 11v5.5M12 7.6v.1" strokeLinecap="round" />
</>
),
database: () => (
<>
<ellipse cx="12" cy="5.5" rx="7.5" ry="3" />
<path d="M4.5 5.5v6c0 1.7 3.4 3 7.5 3s7.5-1.3 7.5-3v-6" />
<path d="M4.5 11.5v6c0 1.7 3.4 3 7.5 3s7.5-1.3 7.5-3v-6" />
</>
),
calendar: () => (
<>
<rect x="3.5" y="5" width="17" height="15.5" rx="2" />
<path d="M8 3v4M16 3v4M3.5 10h17" />
</>
),
target: () => (
<>
<circle cx="12" cy="12" r="8.5" />
<circle cx="12" cy="12" r="4.5" />
<circle cx="12" cy="12" r="0.8" fill="currentColor" stroke="none" />
</>
),
chartLine: () => (
<>
<path d="M3.5 3.5v17h17" />
<path d="m6.5 15 4-5 3.5 3 4.5-6.5" strokeLinecap="round" strokeLinejoin="round" />
</>
),
scale: () => (
<>
<path d="M12 4v16M8 20h8" />
<path d="m5 8 7-3 7 3-2.2 3H7.2L5 8z" />
<path d="M3.5 12.2 2 11l3-3M20.5 12.2 22 11l-3-3" strokeLinecap="round" strokeLinejoin="round" />
</>
),
book: () => (
<>
<path d="M4.5 5A2.5 2.5 0 0 1 7 2.5h12V18H7a2.5 2.5 0 0 0-2.5 2.5V5z" />
<path d="M4.5 20.5A2.5 2.5 0 0 1 7 18h12" />
<path d="M9.5 7h6" strokeLinecap="round" />
</>
),
link: () => (
<>
<path d="M10 14.5 14.5 10" strokeLinecap="round" />
<path d="M8.2 15.8 5.8 18.2a2.8 2.8 0 1 1-3.9-3.9l4-4a2.8 2.8 0 0 1 3.9 0" strokeLinecap="round" strokeLinejoin="round" />
<path d="M15.8 8.2l2.4-2.4a2.8 2.8 0 1 1 3.9 3.9l-4 4a2.8 2.8 0 0 1-3.9 0" strokeLinecap="round" strokeLinejoin="round" />
</>
),
spark: () => (
<>
<path d="M12 3.5 13.8 9l5.7 1.4-5.7 1.4L12 17.5l-1.8-5.7L4.5 10.4 10.2 9 12 3.5z" fill="currentColor" stroke="none" opacity="0.35" />
<path d="M19 15.5l.9 2.6 2.6.9-2.6.9-.9 2.6-.9-2.6-2.6-.9 2.6-.9.9-2.6z" fill="currentColor" stroke="none" />
</>
),
clock: () => (
<>
<circle cx="12" cy="12" r="8.5" />
<path d="M12 7.5V12l3 2" strokeLinecap="round" strokeLinejoin="round" />
</>
),
filter: () => (
<>
<path d="M4 6h16M7 12h10M10 18h4" strokeLinecap="round" />
</>
),
};
interface IconProps extends Omit<SVGProps<SVGSVGElement>, "name"> {
name: IconName;
size?: number;
/** 填充态(部分图标支持实心强调) */
filled?: boolean;
}
export function Icon({ name, size = 18, filled = false, ...rest }: IconProps) {
return (
<svg
className="ic"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.6}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...rest}
>
{P[name]({ filled })}
</svg>
);
}