feat(web): 整体替换 UI 框架为 Ant Design 6 并接入自定义主题

以 antd ConfigProvider + antd-style 承载 zinc 风格自定义主题(明暗双套 token),
button/input/select/notification 用 createStyles 定制;三态主题经
darkAlgorithm 无刷新切换;use-toast 改基于 antd notification;
移除 shadcn/ui 组件目录与 radix 依赖;样式桥接层保证排版与多端布局不变。
This commit is contained in:
2026-09-09 10:35:24 +08:00
parent 5a78de6aa6
commit 87d9bab5dc
23 changed files with 2787 additions and 1608 deletions
+414
View File
@@ -0,0 +1,414 @@
import { useMemo } from 'react'
import { theme } from 'antd'
import type { ConfigProviderProps } from 'antd'
import { createStyles } from 'antd-style'
import { clsx } from 'clsx'
/**
* ConfigProvider 主题:把应用的黑白灰 zinc 体系映射到 antd token。
* 浅色 token 与设计稿 theme-vars 一致;深色走 darkAlgorithm 并覆盖
* index.css `.dark` 中定义的语义色。组件样式跟随 antd,本文件只做
* 品牌色/圆角/阴影的对齐,不再逐像素复刻旧手搓样式。
*/
const useStyles = createStyles(({ css }) => {
return {
buttonPrimaryLight: css({
backgroundColor: '#18181b',
color: '#ffffff',
border: '1px solid #18181b',
fontWeight: 500,
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
}),
buttonPrimaryDark: css({
backgroundColor: '#fafafa',
color: '#18181b',
border: '1px solid #fafafa',
fontWeight: 500,
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
}),
buttonDefaultLight: css({
backgroundColor: '#ffffff',
color: '#18181b',
border: '1px solid #e4e4e7',
fontWeight: 500,
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
}),
buttonDefaultDark: css({
backgroundColor: '#27272a',
color: '#fafafa',
border: '1px solid #3f3f46',
fontWeight: 500,
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
}),
buttonDangerLight: css({
backgroundColor: '#dc2626',
color: '#ffffff',
border: '1px solid #dc2626',
fontWeight: 500,
}),
buttonDangerDark: css({
backgroundColor: '#ef4444',
color: '#fafafa',
border: '1px solid #ef4444',
fontWeight: 500,
}),
inputRootLight: css({
borderColor: '#e4e4e7',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
}),
inputRootDark: css({
borderColor: '#3f3f46',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
}),
inputElementLight: css({
color: '#18181b',
}),
inputElementDark: css({
color: '#fafafa',
}),
inputErrorLight: css({
borderColor: '#dc2626',
}),
inputErrorDark: css({
borderColor: '#ef4444',
}),
selectRoot: css({
borderColor: '#e4e4e7',
}),
notificationRootLight: css({
'&.ant-notification-notice, & .ant-notification-notice': {
border: '1px solid #e4e4e7',
borderRadius: 8,
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
},
}),
notificationRootDark: css({
'&.ant-notification-notice, & .ant-notification-notice': {
border: '1px solid #3f3f46',
borderRadius: 8,
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3)',
},
}),
notificationTitle: css({
fontWeight: 600,
}),
notificationDescriptionLight: css({
color: '#525252',
}),
notificationDescriptionDark: css({
color: '#d4d4d8',
}),
}
})
/* eslint-disable react-hooks/exhaustive-deps */
const useShadcnTheme = (dark: boolean) => {
const { styles } = useStyles()
return useMemo<ConfigProviderProps>(
() => ({
theme: {
algorithm: dark ? theme.darkAlgorithm : theme.defaultAlgorithm,
token: dark
? {
// 深色:与 index.css `.dark` 语义变量对齐(zinc 深色 ramp)
fontFamily: 'var(--font-sans)',
// 控件高度对齐设计稿:普通按钮 34 / 紧凑 24 / 登录大按钮 44
controlHeight: 34,
controlHeightSM: 24,
controlHeightLG: 44,
colorPrimary: '#fafafa',
colorSuccess: '#22c55e',
colorWarning: '#f97316',
colorError: '#ef4444',
colorInfo: '#e4e4e7',
colorTextBase: '#fafafa',
colorBgBase: '#18181b',
colorPrimaryBg: '#27272a',
colorPrimaryBgHover: '#3f3f46',
colorPrimaryBorder: '#3f3f46',
colorPrimaryBorderHover: '#52525b',
colorPrimaryHover: '#ffffff',
colorPrimaryActive: '#d4d4d8',
colorPrimaryText: '#fafafa',
colorPrimaryTextHover: '#ffffff',
colorPrimaryTextActive: '#e4e4e7',
colorSuccessBg: '#0f2417',
colorSuccessBgHover: '#163321',
colorSuccessBorder: '#166534',
colorSuccessBorderHover: '#15803d',
colorSuccessHover: '#16a34a',
colorSuccessActive: '#15803d',
colorSuccessText: '#4ade80',
colorSuccessTextHover: '#4ade80',
colorSuccessTextActive: '#22c55e',
colorWarningBg: '#271a0c',
colorWarningBgHover: '#33220f',
colorWarningBorder: '#9a3412',
colorWarningBorderHover: '#c2410c',
colorWarningHover: '#ea580c',
colorWarningActive: '#c2410c',
colorWarningText: '#fb923c',
colorWarningTextHover: '#fb923c',
colorWarningTextActive: '#f97316',
colorErrorBg: '#2a1416',
colorErrorBgHover: '#38191c',
colorErrorBorder: '#7f1d1d',
colorErrorBorderHover: '#991b1b',
colorErrorHover: '#dc2626',
colorErrorActive: '#b91c1c',
colorErrorText: '#fca5a5',
colorErrorTextHover: '#fca5a5',
colorErrorTextActive: '#f87171',
colorInfoBg: '#27272a',
colorInfoBgHover: '#3f3f46',
colorInfoBorder: '#3f3f46',
colorInfoBorderHover: '#52525b',
colorInfoHover: '#f4f4f5',
colorInfoActive: '#e4e4e7',
colorInfoText: '#fafafa',
colorInfoTextHover: '#f4f4f5',
colorInfoTextActive: '#e4e4e7',
colorText: '#fafafa',
colorTextSecondary: '#d4d4d8',
colorTextTertiary: '#a1a1aa',
colorTextQuaternary: '#71717a',
colorTextDisabled: '#71717a',
colorBgContainer: '#27272a',
colorBgElevated: '#27272a',
colorBgLayout: '#18181b',
colorBgSpotlight: 'rgba(250, 250, 250, 0.85)',
colorBgMask: 'rgba(0, 0, 0, 0.6)',
colorBorder: '#3f3f46',
colorBorderSecondary: '#27272a',
borderRadius: 10,
borderRadiusXS: 2,
borderRadiusSM: 6,
borderRadiusLG: 14,
padding: 16,
paddingSM: 12,
paddingLG: 24,
margin: 16,
marginSM: 12,
marginLG: 24,
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px -1px rgba(0, 0, 0, 0.25)',
boxShadowSecondary:
'0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3)',
}
: {
fontFamily: 'var(--font-sans)',
// 控件高度对齐设计稿:普通按钮 34 / 紧凑 24 / 登录大按钮 44
controlHeight: 34,
controlHeightSM: 24,
controlHeightLG: 44,
colorPrimary: '#262626',
colorSuccess: '#22c55e',
colorWarning: '#f97316',
colorError: '#ef4444',
colorInfo: '#262626',
colorTextBase: '#262626',
colorBgBase: '#ffffff',
colorPrimaryBg: '#f5f5f5',
colorPrimaryBgHover: '#e5e5e5',
colorPrimaryBorder: '#d4d4d4',
colorPrimaryBorderHover: '#a3a3a3',
colorPrimaryHover: '#404040',
colorPrimaryActive: '#171717',
colorPrimaryText: '#262626',
colorPrimaryTextHover: '#404040',
colorPrimaryTextActive: '#171717',
colorSuccessBg: '#f0fdf4',
colorSuccessBgHover: '#dcfce7',
colorSuccessBorder: '#bbf7d0',
colorSuccessBorderHover: '#86efac',
colorSuccessHover: '#16a34a',
colorSuccessActive: '#15803d',
colorSuccessText: '#16a34a',
colorSuccessTextHover: '#16a34a',
colorSuccessTextActive: '#15803d',
colorWarningBg: '#fff7ed',
colorWarningBgHover: '#fed7aa',
colorWarningBorder: '#fdba74',
colorWarningBorderHover: '#fb923c',
colorWarningHover: '#ea580c',
colorWarningActive: '#c2410c',
colorWarningText: '#ea580c',
colorWarningTextHover: '#ea580c',
colorWarningTextActive: '#c2410c',
colorErrorBg: '#fef2f2',
colorErrorBgHover: '#fecaca',
colorErrorBorder: '#fca5a5',
colorErrorBorderHover: '#f87171',
colorErrorHover: '#dc2626',
colorErrorActive: '#b91c1c',
colorErrorText: '#dc2626',
colorErrorTextHover: '#dc2626',
colorErrorTextActive: '#b91c1c',
colorInfoBg: '#f5f5f5',
colorInfoBgHover: '#e5e5e5',
colorInfoBorder: '#d4d4d4',
colorInfoBorderHover: '#a3a3a3',
colorInfoHover: '#404040',
colorInfoActive: '#171717',
colorInfoText: '#262626',
colorInfoTextHover: '#404040',
colorInfoTextActive: '#171717',
colorText: '#262626',
colorTextSecondary: '#525252',
colorTextTertiary: '#737373',
colorTextQuaternary: '#a3a3a3',
colorTextDisabled: '#a3a3a3',
colorBgContainer: '#ffffff',
colorBgElevated: '#ffffff',
colorBgLayout: '#fafafa',
colorBgSpotlight: 'rgba(38, 38, 38, 0.85)',
colorBgMask: 'rgba(38, 38, 38, 0.45)',
colorBorder: '#e5e5e5',
colorBorderSecondary: '#f5f5f5',
borderRadius: 10,
borderRadiusXS: 2,
borderRadiusSM: 6,
borderRadiusLG: 14,
padding: 16,
paddingSM: 12,
paddingLG: 24,
margin: 16,
marginSM: 12,
marginLG: 24,
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
boxShadowSecondary:
'0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
},
components: {
Button: {
primaryShadow: 'none',
defaultShadow: 'none',
dangerShadow: 'none',
defaultBorderColor: dark ? '#3f3f46' : '#e4e4e7',
defaultColor: dark ? '#fafafa' : '#18181b',
defaultBg: dark ? '#27272a' : '#ffffff',
defaultHoverBg: dark ? '#3f3f46' : '#f4f4f5',
defaultHoverBorderColor: dark ? '#52525b' : '#d4d4d8',
defaultHoverColor: dark ? '#fafafa' : '#18181b',
defaultActiveBg: dark ? '#52525b' : '#e4e4e7',
defaultActiveBorderColor: dark ? '#52525b' : '#d4d4d8',
borderRadius: 6,
},
Input: {
activeShadow: 'none',
hoverBorderColor: dark ? '#a1a1aa' : '#a1a1aa',
activeBorderColor: dark ? '#d4d4d8' : '#18181b',
borderRadius: 6,
},
Select: {
optionSelectedBg: dark ? '#3f3f46' : '#f4f4f5',
optionActiveBg: dark ? '#27272a' : '#fafafa',
optionSelectedFontWeight: 500,
borderRadius: 6,
},
Alert: {
borderRadiusLG: 8,
},
Modal: {
borderRadiusLG: 12,
},
Switch: {
trackHeight: 22,
trackMinWidth: 44,
innerMinMargin: 4,
innerMaxMargin: 24,
},
Checkbox: {
borderRadiusSM: 4,
},
Notification: {
colorSuccessBg: dark ? '#0f2417' : '#f0fdf4',
colorErrorBg: dark ? '#2a1416' : '#fef2f2',
colorInfoBg: dark ? '#27272a' : '#f5f5f5',
colorWarningBg: dark ? '#271a0c' : '#fff7ed',
},
Card: {},
Tooltip: {},
Radio: {},
},
},
button: {
classNames: ({ props }) => ({
root: clsx(
(props as { type?: string }).type === 'primary' &&
(dark ? styles.buttonPrimaryDark : styles.buttonPrimaryLight),
(props as { type?: string }).type === 'default' &&
(dark ? styles.buttonDefaultDark : styles.buttonDefaultLight),
(props as { danger?: boolean }).danger &&
(dark ? styles.buttonDangerDark : styles.buttonDangerLight),
),
}),
},
input: {
classNames: ({ props }) => ({
root: clsx(
dark ? styles.inputRootDark : styles.inputRootLight,
(props as { status?: string }).status === 'error' &&
(dark ? styles.inputErrorDark : styles.inputErrorLight),
),
input: dark ? styles.inputElementDark : styles.inputElementLight,
}),
},
select: {
classNames: {
root: styles.selectRoot,
},
},
notification: {
classNames: {
root: dark ? styles.notificationRootDark : styles.notificationRootLight,
title: styles.notificationTitle,
description: dark
? styles.notificationDescriptionDark
: styles.notificationDescriptionLight,
},
},
wave: {},
app: {},
card: {},
modal: {},
alert: {},
colorPicker: {},
checkbox: {},
dropdown: {},
datePicker: {},
inputNumber: {},
popover: {},
tooltip: {},
switch: {},
radio: {},
segmented: {},
progress: {},
}),
[
dark,
styles.buttonDangerDark,
styles.buttonDangerLight,
styles.buttonDefaultDark,
styles.buttonDefaultLight,
styles.buttonPrimaryDark,
styles.buttonPrimaryLight,
styles.inputElementDark,
styles.inputElementLight,
styles.inputErrorDark,
styles.inputErrorLight,
styles.inputRootDark,
styles.inputRootLight,
styles.notificationDescriptionDark,
styles.notificationDescriptionLight,
styles.notificationRootDark,
styles.notificationRootLight,
styles.notificationTitle,
styles.selectRoot,
],
)
}
/* eslint-enable react-hooks/exhaustive-deps */
export default useShadcnTheme