更新Company页面的UI为FUI风格

This commit is contained in:
2025-12-17 19:05:02 +08:00
parent 9b8d7d1d96
commit 4eb8310038
10 changed files with 1405 additions and 113 deletions

View File

@@ -56,16 +56,16 @@ export interface SubTabTheme {
}
/**
* 预设主题
* 预设主题 - FUI 风格优化
*/
const THEME_PRESETS: Record<string, SubTabTheme> = {
blackGold: {
bg: 'gray.900',
borderColor: 'rgba(212, 175, 55, 0.3)',
tabSelectedBg: '#D4AF37',
tabSelectedColor: 'gray.900',
tabUnselectedColor: '#D4AF37',
tabHoverBg: 'gray.600',
bg: 'transparent',
borderColor: 'rgba(212, 175, 55, 0.2)',
tabSelectedBg: 'linear-gradient(135deg, #D4AF37 0%, #B8960C 100%)',
tabSelectedColor: '#0A0A14',
tabUnselectedColor: 'rgba(212, 175, 55, 0.8)',
tabHoverBg: 'rgba(212, 175, 55, 0.1)',
},
default: {
bg: 'white',
@@ -179,23 +179,28 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
key={tab.key}
color={theme.tabUnselectedColor}
borderRadius="full"
px={2.5}
px={3}
py={1.5}
fontSize="xs"
whiteSpace="nowrap"
flexShrink={0}
border="1px solid transparent"
transition="all 0.2s cubic-bezier(0.4, 0, 0.2, 1)"
_selected={{
bg: theme.tabSelectedBg,
color: theme.tabSelectedColor,
fontWeight: 'bold',
boxShadow: '0 0 12px rgba(212, 175, 55, 0.4)',
border: '1px solid rgba(212, 175, 55, 0.5)',
}}
_hover={{
bg: theme.tabHoverBg,
border: '1px solid rgba(212, 175, 55, 0.3)',
}}
>
<HStack spacing={1}>
{tab.icon && <Icon as={tab.icon} boxSize={3} />}
<Text>{tab.name}</Text>
<HStack spacing={1.5}>
{tab.icon && <Icon as={tab.icon} boxSize={3.5} />}
<Text letterSpacing="wide">{tab.name}</Text>
</HStack>
</Tab>
))}

View File

@@ -1,8 +1,11 @@
/**
* Company 页面顶部搜索栏组件
* - 显示股票代码、名称、价格、涨跌幅
* - 股票搜索功能
* - 自选股操作
* Company 页面顶部搜索栏组件 - FUI 科幻风格
*
* 设计特点:
* - Glassmorphism 毛玻璃背景
* - 发光效果和微动画
* - Ash Thorp 风格的数据展示
* - James Turrell 柔和光影
*/
import React, { memo, useMemo, useCallback, useState } from 'react';
@@ -14,17 +17,24 @@ import {
Text,
Button,
Icon,
Badge,
Skeleton,
keyframes,
} from '@chakra-ui/react';
import { AutoComplete, Spin } from 'antd';
import { Search, Star } from 'lucide-react';
import { Search, Star, TrendingUp, TrendingDown } from 'lucide-react';
import { useStockSearch } from '@hooks/useStockSearch';
import { THEME, getSearchBoxStyles } from '../../config';
import { FUI_COLORS, FUI_GLOW, FUI_ANIMATION, FUI_GLASS } from '../../theme/fui';
import type { CompanyHeaderProps, StockSearchResult } from '../../types';
// 发光脉冲动画
const glowPulse = keyframes`
0%, 100% { box-shadow: 0 0 20px rgba(212, 175, 55, 0.2); }
50% { box-shadow: 0 0 30px rgba(212, 175, 55, 0.4); }
`;
/**
* 股票信息展示组件
* 股票信息展示组件 - FUI 风格
*/
const StockInfoDisplay = memo<{
stockCode: string;
@@ -35,47 +45,95 @@ const StockInfoDisplay = memo<{
}>(({ stockCode, stockName, price, change, loading }) => {
if (loading) {
return (
<VStack align="start" spacing={1}>
<Skeleton height="32px" width="200px" />
<Skeleton height="24px" width="150px" />
<VStack align="start" spacing={2}>
<Skeleton height="36px" width="220px" startColor={FUI_COLORS.bg.surface} endColor={FUI_COLORS.bg.elevated} />
<Skeleton height="28px" width="160px" startColor={FUI_COLORS.bg.surface} endColor={FUI_COLORS.bg.elevated} />
</VStack>
);
}
const isPositive = change !== null && change !== undefined && change >= 0;
const TrendIcon = isPositive ? TrendingUp : TrendingDown;
return (
<VStack align="start" spacing={0}>
<HStack spacing={2}>
<VStack align="start" spacing={1}>
{/* 股票代码 & 名称 */}
<HStack spacing={3} align="baseline">
<Text
fontSize="2xl"
fontWeight="bold"
color={THEME.gold}
color={FUI_COLORS.gold[400]}
letterSpacing="wider"
textShadow={FUI_GLOW.text.gold}
>
{stockCode}
</Text>
{stockName && (
<Text fontSize="xl" fontWeight="medium" color={THEME.textPrimary}>
<Text
fontSize="xl"
fontWeight="medium"
color={FUI_COLORS.text.primary}
letterSpacing="wide"
>
{stockName}
</Text>
)}
</HStack>
{/* 价格 & 涨跌幅 */}
{price !== null && price !== undefined && (
<HStack spacing={3} mt={1}>
<Text fontSize="lg" fontWeight="bold" color={THEME.textPrimary}>
<HStack spacing={4} mt={1}>
{/* 价格 */}
<HStack spacing={1}>
<Text
fontSize="xs"
color={FUI_COLORS.text.muted}
textTransform="uppercase"
letterSpacing="wider"
>
Price
</Text>
<Text
fontSize="xl"
fontWeight="bold"
color={FUI_COLORS.text.primary}
fontFamily="mono"
>
¥{price.toFixed(2)}
</Text>
</HStack>
{/* 涨跌幅 Badge */}
{change !== null && change !== undefined && (
<Badge
px={2}
py={0.5}
borderRadius="md"
bg={change >= 0 ? THEME.positiveBg : THEME.negativeBg}
color={change >= 0 ? THEME.positive : THEME.negative}
<Box
display="inline-flex"
alignItems="center"
gap={1}
px={3}
py={1}
borderRadius="full"
bg={isPositive ? 'rgba(239, 68, 68, 0.15)' : 'rgba(34, 197, 94, 0.15)'}
border="1px solid"
borderColor={isPositive ? 'rgba(239, 68, 68, 0.3)' : 'rgba(34, 197, 94, 0.3)'}
boxShadow={isPositive
? '0 0 12px rgba(239, 68, 68, 0.2)'
: '0 0 12px rgba(34, 197, 94, 0.2)'
}
>
<Icon
as={TrendIcon}
boxSize={3.5}
color={isPositive ? FUI_COLORS.status.positive : FUI_COLORS.status.negative}
/>
<Text
fontSize="sm"
fontWeight="bold"
fontFamily="mono"
color={isPositive ? FUI_COLORS.status.positive : FUI_COLORS.status.negative}
>
{change >= 0 ? '+' : ''}{change.toFixed(2)}%
</Badge>
{isPositive ? '+' : ''}{change.toFixed(2)}%
</Text>
</Box>
)}
</HStack>
)}
@@ -154,8 +212,36 @@ const SearchActions = memo<{
return (
<HStack spacing={3}>
{/* 搜索框 */}
<Box sx={getSearchBoxStyles(THEME)}>
{/* 搜索框 - FUI 风格 */}
<Box
sx={{
...getSearchBoxStyles(THEME),
'.ant-select-selector': {
backgroundColor: `${FUI_COLORS.bg.primary} !important`,
borderColor: `${FUI_COLORS.line.default} !important`,
borderRadius: '10px !important',
height: '42px !important',
backdropFilter: FUI_GLASS.blur.sm,
transition: `all ${FUI_ANIMATION.duration.fast} ${FUI_ANIMATION.easing.default}`,
'&:hover': {
borderColor: `${FUI_COLORS.line.emphasis} !important`,
boxShadow: FUI_GLOW.gold.sm,
},
'&:focus-within': {
borderColor: `${FUI_COLORS.gold[400]} !important`,
boxShadow: FUI_GLOW.gold.md,
},
},
'.ant-select-selection-search-input': {
color: `${FUI_COLORS.text.primary} !important`,
height: '40px !important',
fontFamily: 'inherit',
},
'.ant-select-selection-placeholder': {
color: `${FUI_COLORS.text.muted} !important`,
},
}}
>
<AutoComplete
value={inputCode}
options={stockOptions}
@@ -163,36 +249,57 @@ const SearchActions = memo<{
onSelect={handleSelect}
onChange={onInputChange}
placeholder="输入代码、名称或拼音"
style={{ width: 220 }}
style={{ width: 240 }}
notFoundContent={isSearching ? <Spin size="small" /> : null}
onKeyDown={handleKeyDown}
/>
</Box>
{/* 搜索按钮 */}
{/* 搜索按钮 - 发光效果 */}
<Button
bg={THEME.gold}
color={THEME.bg}
_hover={{ bg: THEME.goldLight }}
_active={{ bg: THEME.goldDark }}
bg={`linear-gradient(135deg, ${FUI_COLORS.gold[500]} 0%, ${FUI_COLORS.gold[400]} 100%)`}
color={FUI_COLORS.bg.deep}
_hover={{
bg: `linear-gradient(135deg, ${FUI_COLORS.gold[400]} 0%, ${FUI_COLORS.gold[300]} 100%)`,
boxShadow: FUI_GLOW.gold.md,
transform: 'translateY(-1px)',
}}
_active={{
bg: FUI_COLORS.gold[600],
transform: 'translateY(0)',
}}
size="md"
h="42px"
px={5}
onClick={onSearch}
leftIcon={<Icon as={Search} boxSize={4} />}
fontWeight="bold"
borderRadius="10px"
transition={`all ${FUI_ANIMATION.duration.fast} ${FUI_ANIMATION.easing.default}`}
>
</Button>
{/* 自选按钮 */}
{/* 自选按钮 - FUI 风格 */}
<Button
variant={isInWatchlist ? 'solid' : 'outline'}
bg={isInWatchlist ? THEME.gold : 'transparent'}
color={isInWatchlist ? THEME.bg : THEME.gold}
borderColor={THEME.gold}
variant="outline"
bg={isInWatchlist ? 'rgba(212, 175, 55, 0.2)' : 'transparent'}
color={FUI_COLORS.gold[400]}
borderColor={isInWatchlist ? FUI_COLORS.gold[400] : FUI_COLORS.line.emphasis}
borderWidth="1px"
_hover={{
bg: isInWatchlist ? THEME.goldLight : 'rgba(212, 175, 55, 0.1)',
bg: 'rgba(212, 175, 55, 0.15)',
borderColor: FUI_COLORS.gold[400],
boxShadow: FUI_GLOW.gold.sm,
transform: 'translateY(-1px)',
}}
_active={{
bg: 'rgba(212, 175, 55, 0.25)',
transform: 'translateY(0)',
}}
size="md"
h="42px"
px={5}
onClick={onWatchlistToggle}
isLoading={watchlistLoading}
leftIcon={
@@ -200,9 +307,13 @@ const SearchActions = memo<{
as={Star}
boxSize={4}
fill={isInWatchlist ? 'currentColor' : 'none'}
filter={isInWatchlist ? 'drop-shadow(0 0 4px rgba(212, 175, 55, 0.6))' : 'none'}
/>
}
fontWeight="bold"
borderRadius="10px"
transition={`all ${FUI_ANIMATION.duration.fast} ${FUI_ANIMATION.easing.default}`}
sx={isInWatchlist ? { animation: `${glowPulse} 3s ease-in-out infinite` } : undefined}
>
{isInWatchlist ? '已自选' : '自选'}
</Button>
@@ -248,13 +359,42 @@ const CompanyHeader: React.FC<CompanyHeaderProps> = memo(({
return (
<Box
bg={THEME.cardBg}
position="relative"
bg={`linear-gradient(180deg, ${FUI_COLORS.bg.elevated} 0%, ${FUI_COLORS.bg.primary} 100%)`}
borderBottom="1px solid"
borderColor={THEME.border}
borderColor={FUI_COLORS.line.default}
px={6}
py={4}
py={5}
backdropFilter={FUI_GLASS.blur.md}
overflow="hidden"
>
{/* 环境光效果 - James Turrell 风格 */}
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
pointerEvents="none"
bg={`radial-gradient(ellipse 80% 50% at 20% 40%, ${FUI_COLORS.ambient.warm}, transparent),
radial-gradient(ellipse 60% 40% at 80% 60%, ${FUI_COLORS.ambient.cool}, transparent)`}
opacity={0.6}
/>
{/* 顶部发光线 */}
<Box
position="absolute"
top={0}
left="10%"
right="10%"
h="1px"
bg={`linear-gradient(90deg, transparent 0%, ${FUI_COLORS.gold[400]} 50%, transparent 100%)`}
opacity={0.4}
/>
<Flex
position="relative"
zIndex={1}
maxW="container.xl"
mx="auto"
justify="space-between"

View File

@@ -382,22 +382,6 @@ export const getMinuteKLineOption = (theme: Theme, minuteData: MinuteData | null
end: 100,
minValueSpan: 20,
},
{
show: true,
xAxisIndex: [0, 1],
type: 'slider',
top: '95%',
start: 70,
end: 100,
height: 20,
handleSize: '100%',
handleStyle: {
color: theme.primary,
},
textStyle: {
color: theme.textMuted,
},
},
],
series: [
{
@@ -811,24 +795,6 @@ export const getMinuteKLineDarkGoldOption = (minuteData: MinuteData | null): ECh
end: 100,
minValueSpan: 20,
},
{
show: true,
xAxisIndex: [0, 1],
type: 'slider',
top: '95%',
start: 70,
end: 100,
height: 20,
handleSize: '100%',
handleStyle: {
color: gold,
},
textStyle: {
color: textMuted,
},
borderColor: borderColor,
fillerColor: 'rgba(212, 175, 55, 0.2)',
},
],
series: [
{

View File

@@ -1,14 +1,18 @@
/**
* 公司详情页面
* 公司详情页面 - FUI 科幻风格
*
* 特性:
* - 黑金主题设计
* - 懒加载 Tab 内容
* - memo 性能优化
* - axios 数据请求
* - Ash Thorp 风格 FUI 设计
* - James Turrell 光影效果
* - Glassmorphism 毛玻璃卡片
* - Linear.app 风格微交互
* - HeroUI 现代组件风格
*/
import React, { memo, useCallback, useRef, useEffect, Suspense } from 'react';
// FUI 动画样式
import './theme/fui-animations.css';
import { useSearchParams } from 'react-router-dom';
import { Box, Spinner, Center } from '@chakra-ui/react';
import SubTabContainer from '@components/SubTabContainer';
@@ -30,7 +34,7 @@ const TabLoadingFallback = memo(() => (
TabLoadingFallback.displayName = 'TabLoadingFallback';
// ============================================
// 主内容区组件
// 主内容区组件 - FUI 风格
// ============================================
interface CompanyContentProps {
@@ -41,12 +45,61 @@ interface CompanyContentProps {
const CompanyContent = memo<CompanyContentProps>(({ stockCode, onTabChange }) => (
<Box maxW="container.xl" mx="auto" px={4} py={6}>
<Box
bg={THEME.cardBg}
position="relative"
bg={`linear-gradient(145deg, rgba(26, 26, 46, 0.95) 0%, rgba(15, 15, 26, 0.98) 100%)`}
borderRadius="xl"
border="1px solid"
borderColor={THEME.border}
borderColor="rgba(212, 175, 55, 0.15)"
overflow="hidden"
backdropFilter="blur(16px)"
boxShadow="0 8px 32px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.05)"
>
{/* 角落装饰 - FUI 风格 */}
<Box
position="absolute"
top="12px"
left="12px"
w="16px"
h="16px"
borderTop="2px solid"
borderLeft="2px solid"
borderColor="rgba(212, 175, 55, 0.4)"
opacity={0.6}
/>
<Box
position="absolute"
top="12px"
right="12px"
w="16px"
h="16px"
borderTop="2px solid"
borderRight="2px solid"
borderColor="rgba(212, 175, 55, 0.4)"
opacity={0.6}
/>
<Box
position="absolute"
bottom="12px"
left="12px"
w="16px"
h="16px"
borderBottom="2px solid"
borderLeft="2px solid"
borderColor="rgba(212, 175, 55, 0.4)"
opacity={0.6}
/>
<Box
position="absolute"
bottom="12px"
right="12px"
w="16px"
h="16px"
borderBottom="2px solid"
borderRight="2px solid"
borderColor="rgba(212, 175, 55, 0.4)"
opacity={0.6}
/>
<Suspense fallback={<TabLoadingFallback />}>
<SubTabContainer
tabs={TAB_CONFIG}
@@ -128,8 +181,30 @@ const CompanyIndex: React.FC = () => {
}, [trackTabChanged]);
return (
<Box bg={THEME.bg} minH="calc(100vh - 60px)">
<Box
position="relative"
bg={THEME.bg}
minH="calc(100vh - 60px)"
overflow="hidden"
>
{/* 全局环境光效果 - James Turrell 风格 */}
<Box
position="fixed"
top={0}
left={0}
right={0}
bottom={0}
pointerEvents="none"
zIndex={0}
bg={`
radial-gradient(ellipse 100% 80% at 50% -20%, rgba(212, 175, 55, 0.08), transparent 50%),
radial-gradient(ellipse 60% 50% at 0% 50%, rgba(100, 200, 255, 0.04), transparent 40%),
radial-gradient(ellipse 60% 50% at 100% 50%, rgba(255, 200, 100, 0.04), transparent 40%)
`}
/>
{/* 顶部搜索栏 */}
<Box position="relative" zIndex={1}>
<CompanyHeader
stockCode={stockCode}
stockInfo={stockInfo}
@@ -139,13 +214,16 @@ const CompanyIndex: React.FC = () => {
onStockChange={handleStockChange}
onWatchlistToggle={handleWatchlistToggle}
/>
</Box>
{/* 主内容区 */}
<Box position="relative" zIndex={1}>
<CompanyContent
stockCode={stockCode}
onTabChange={handleTabChange}
/>
</Box>
</Box>
);
};

View File

@@ -0,0 +1,381 @@
/**
* FUI Elements - 科幻风格 UI 元素集合
*
* 包含:
* - GlowText: 发光文字
* - DataBadge: 数据标签
* - StatusIndicator: 状态指示器
* - Divider: 分隔线
* - ProgressBar: 进度条
*/
import React, { memo } from 'react';
import { Box, Text, HStack, BoxProps, TextProps } from '@chakra-ui/react';
import { FUI_COLORS, FUI_GLOW, FUI_ANIMATION } from '../fui';
// ============================================
// GlowText - 发光文字
// ============================================
export interface GlowTextProps extends Omit<TextProps, 'css'> {
children: React.ReactNode;
/** 发光颜色 */
glowColor?: 'gold' | 'white' | 'positive' | 'negative';
/** 发光强度 */
intensity?: 'low' | 'medium' | 'high';
}
const GLOW_COLORS = {
gold: FUI_COLORS.gold[400],
white: '#FFFFFF',
positive: FUI_COLORS.status.positive,
negative: FUI_COLORS.status.negative,
};
const INTENSITY_MAP = {
low: 0.3,
medium: 0.5,
high: 0.8,
};
export const GlowText: React.FC<GlowTextProps> = memo(({
children,
glowColor = 'gold',
intensity = 'medium',
...props
}) => {
const color = GLOW_COLORS[glowColor];
const alpha = INTENSITY_MAP[intensity];
return (
<Text
color={color}
textShadow={`0 0 ${10 * alpha}px rgba(${glowColor === 'gold' ? '212, 175, 55' : '255, 255, 255'}, ${alpha})`}
transition={`all ${FUI_ANIMATION.duration.normal} ${FUI_ANIMATION.easing.smooth}`}
{...props}
>
{children}
</Text>
);
});
GlowText.displayName = 'GlowText';
// ============================================
// DataBadge - 数据标签
// ============================================
export interface DataBadgeProps extends Omit<BoxProps, 'css'> {
children: React.ReactNode;
/** 标签类型 */
variant?: 'default' | 'success' | 'danger' | 'warning' | 'info';
/** 尺寸 */
size?: 'sm' | 'md' | 'lg';
/** 是否脉冲动画 */
pulse?: boolean;
}
const BADGE_VARIANTS = {
default: {
bg: 'rgba(212, 175, 55, 0.15)',
borderColor: 'rgba(212, 175, 55, 0.3)',
color: FUI_COLORS.gold[400],
},
success: {
bg: 'rgba(34, 197, 94, 0.15)',
borderColor: 'rgba(34, 197, 94, 0.3)',
color: FUI_COLORS.status.negative,
},
danger: {
bg: 'rgba(239, 68, 68, 0.15)',
borderColor: 'rgba(239, 68, 68, 0.3)',
color: FUI_COLORS.status.positive,
},
warning: {
bg: 'rgba(245, 158, 11, 0.15)',
borderColor: 'rgba(245, 158, 11, 0.3)',
color: FUI_COLORS.status.warning,
},
info: {
bg: 'rgba(59, 130, 246, 0.15)',
borderColor: 'rgba(59, 130, 246, 0.3)',
color: FUI_COLORS.status.info,
},
};
const BADGE_SIZES = {
sm: { px: 2, py: 0.5, fontSize: '10px' },
md: { px: 3, py: 1, fontSize: '12px' },
lg: { px: 4, py: 1.5, fontSize: '14px' },
};
export const DataBadge: React.FC<DataBadgeProps> = memo(({
children,
variant = 'default',
size = 'md',
pulse = false,
...props
}) => {
const variantStyle = BADGE_VARIANTS[variant];
const sizeStyle = BADGE_SIZES[size];
return (
<Box
display="inline-flex"
alignItems="center"
bg={variantStyle.bg}
border="1px solid"
borderColor={variantStyle.borderColor}
borderRadius="full"
color={variantStyle.color}
fontWeight="medium"
letterSpacing="0.5px"
transition={`all ${FUI_ANIMATION.duration.fast} ${FUI_ANIMATION.easing.default}`}
sx={pulse ? { animation: 'glowPulse 2s ease-in-out infinite' } : undefined}
{...sizeStyle}
{...props}
>
{children}
</Box>
);
});
DataBadge.displayName = 'DataBadge';
// ============================================
// StatusIndicator - 状态指示器(带脉冲点)
// ============================================
export interface StatusIndicatorProps extends Omit<BoxProps, 'css'> {
/** 状态 */
status: 'active' | 'inactive' | 'warning' | 'error';
/** 标签文字 */
label?: string;
/** 是否显示脉冲动画 */
pulse?: boolean;
}
const STATUS_COLORS = {
active: FUI_COLORS.gold[400],
inactive: FUI_COLORS.text.muted,
warning: FUI_COLORS.status.warning,
error: FUI_COLORS.status.positive,
};
export const StatusIndicator: React.FC<StatusIndicatorProps> = memo(({
status,
label,
pulse = true,
...props
}) => {
const color = STATUS_COLORS[status];
return (
<HStack spacing={2} {...props}>
<Box position="relative">
{/* 脉冲光环 */}
{pulse && status === 'active' && (
<Box
position="absolute"
top="50%"
left="50%"
transform="translate(-50%, -50%)"
w="12px"
h="12px"
borderRadius="full"
bg={color}
opacity={0.3}
sx={{
animation: 'pulse 2s ease-in-out infinite',
'@keyframes pulse': {
'0%, 100%': { transform: 'translate(-50%, -50%) scale(1)', opacity: 0.3 },
'50%': { transform: 'translate(-50%, -50%) scale(1.5)', opacity: 0 },
},
}}
/>
)}
{/* 核心点 */}
<Box
w="8px"
h="8px"
borderRadius="full"
bg={color}
boxShadow={`0 0 8px ${color}`}
/>
</Box>
{label && (
<Text fontSize="sm" color={FUI_COLORS.text.secondary}>
{label}
</Text>
)}
</HStack>
);
});
StatusIndicator.displayName = 'StatusIndicator';
// ============================================
// FUIDivider - FUI 风格分隔线
// ============================================
export interface FUIDividerProps extends Omit<BoxProps, 'css'> {
/** 方向 */
orientation?: 'horizontal' | 'vertical';
/** 是否发光 */
glowing?: boolean;
/** 中间文字 */
label?: string;
}
export const FUIDivider: React.FC<FUIDividerProps> = memo(({
orientation = 'horizontal',
glowing = false,
label,
...props
}) => {
if (orientation === 'vertical') {
return (
<Box
w="1px"
h="100%"
bg={FUI_COLORS.line.default}
boxShadow={glowing ? FUI_GLOW.gold.sm : undefined}
{...props}
/>
);
}
if (label) {
return (
<HStack spacing={3} w="100%" {...props}>
<Box
flex={1}
h="1px"
bg={`linear-gradient(90deg, transparent, ${FUI_COLORS.line.emphasis})`}
/>
<Text
fontSize="xs"
color={FUI_COLORS.gold[400]}
textTransform="uppercase"
letterSpacing="wider"
fontWeight="medium"
>
{label}
</Text>
<Box
flex={1}
h="1px"
bg={`linear-gradient(90deg, ${FUI_COLORS.line.emphasis}, transparent)`}
/>
</HStack>
);
}
return (
<Box
w="100%"
h="1px"
bg={`linear-gradient(90deg, transparent 0%, ${FUI_COLORS.line.emphasis} 50%, transparent 100%)`}
boxShadow={glowing ? FUI_GLOW.gold.sm : undefined}
{...props}
/>
);
});
FUIDivider.displayName = 'FUIDivider';
// ============================================
// FUIProgressBar - FUI 风格进度条
// ============================================
export interface FUIProgressBarProps extends Omit<BoxProps, 'css'> {
/** 进度值 (0-100) */
value: number;
/** 颜色主题 */
colorScheme?: 'gold' | 'positive' | 'negative' | 'gradient';
/** 是否显示发光效果 */
glowing?: boolean;
/** 高度 */
size?: 'sm' | 'md' | 'lg';
/** 是否显示数值 */
showValue?: boolean;
}
const PROGRESS_COLORS = {
gold: FUI_COLORS.gold[400],
positive: FUI_COLORS.status.positive,
negative: FUI_COLORS.status.negative,
gradient: `linear-gradient(90deg, ${FUI_COLORS.gold[500]}, ${FUI_COLORS.gold[300]})`,
};
const PROGRESS_SIZES = {
sm: '4px',
md: '6px',
lg: '8px',
};
export const FUIProgressBar: React.FC<FUIProgressBarProps> = memo(({
value,
colorScheme = 'gold',
glowing = false,
size = 'md',
showValue = false,
...props
}) => {
const clampedValue = Math.min(100, Math.max(0, value));
const color = PROGRESS_COLORS[colorScheme];
const isGradient = colorScheme === 'gradient';
return (
<HStack spacing={2} w="100%" {...props}>
<Box flex={1}>
<Box
position="relative"
w="100%"
h={PROGRESS_SIZES[size]}
bg={FUI_COLORS.bg.primary}
borderRadius="full"
overflow="hidden"
>
<Box
position="absolute"
top={0}
left={0}
h="100%"
w={`${clampedValue}%`}
bg={isGradient ? color : color}
background={isGradient ? color : undefined}
borderRadius="full"
transition={`width ${FUI_ANIMATION.duration.normal} ${FUI_ANIMATION.easing.smooth}`}
boxShadow={glowing ? `0 0 10px ${isGradient ? FUI_COLORS.gold[400] : color}` : undefined}
/>
</Box>
</Box>
{showValue && (
<Text
fontSize="xs"
fontWeight="bold"
color={FUI_COLORS.gold[400]}
minW="40px"
textAlign="right"
>
{clampedValue.toFixed(0)}%
</Text>
)}
</HStack>
);
});
FUIProgressBar.displayName = 'FUIProgressBar';
// ============================================
// 统一导出
// ============================================
export default {
GlowText,
DataBadge,
StatusIndicator,
FUIDivider,
FUIProgressBar,
};

View File

@@ -0,0 +1,207 @@
/**
* GlassCard - Glassmorphism 风格卡片组件
*
* 设计特点:
* - 毛玻璃背景效果
* - 精细边框发光
* - 悬停动画
* - 可选角落装饰FUI 风格)
*/
import React, { memo, forwardRef } from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { FUI_COLORS, FUI_GLASS, FUI_GLOW, FUI_ANIMATION } from '../fui';
// ============================================
// 类型定义
// ============================================
export interface GlassCardProps extends Omit<BoxProps, 'css'> {
children: React.ReactNode;
/** 变体样式 */
variant?: 'default' | 'elevated' | 'outlined' | 'subtle';
/** 是否启用悬停效果 */
hoverable?: boolean;
/** 是否启用发光效果 */
glowing?: boolean;
/** 是否显示角落装饰 */
cornerDecor?: boolean;
/** 是否显示扫描线效果 */
scanline?: boolean;
/** 边框圆角 */
rounded?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
/** 内边距预设 */
padding?: 'none' | 'sm' | 'md' | 'lg';
}
// ============================================
// 样式配置
// ============================================
const VARIANTS = {
default: {
bg: `linear-gradient(135deg, ${FUI_COLORS.bg.elevated} 0%, ${FUI_COLORS.bg.primary} 100%)`,
border: FUI_GLASS.border.default,
backdropFilter: FUI_GLASS.blur.md,
},
elevated: {
bg: `linear-gradient(145deg, ${FUI_COLORS.bg.surface} 0%, ${FUI_COLORS.bg.elevated} 100%)`,
border: FUI_GLASS.border.emphasis,
backdropFilter: FUI_GLASS.blur.lg,
},
outlined: {
bg: 'transparent',
border: FUI_GLASS.border.emphasis,
backdropFilter: 'none',
},
subtle: {
bg: FUI_GLASS.bg.gold,
border: FUI_GLASS.border.subtle,
backdropFilter: FUI_GLASS.blur.sm,
},
};
const ROUNDED_MAP = {
sm: '8px',
md: '12px',
lg: '16px',
xl: '20px',
'2xl': '24px',
};
const PADDING_MAP = {
none: 0,
sm: 3,
md: 4,
lg: 6,
};
// ============================================
// 角落装饰组件
// ============================================
const CornerDecor: React.FC<{ position: 'tl' | 'tr' | 'bl' | 'br' }> = memo(({ position }) => {
const baseStyle = {
position: 'absolute' as const,
width: '12px',
height: '12px',
borderColor: FUI_COLORS.gold[400],
borderStyle: 'solid',
borderWidth: 0,
opacity: 0.6,
};
const positions = {
tl: { top: '8px', left: '8px', borderTopWidth: '2px', borderLeftWidth: '2px' },
tr: { top: '8px', right: '8px', borderTopWidth: '2px', borderRightWidth: '2px' },
bl: { bottom: '8px', left: '8px', borderBottomWidth: '2px', borderLeftWidth: '2px' },
br: { bottom: '8px', right: '8px', borderBottomWidth: '2px', borderRightWidth: '2px' },
};
return <Box sx={{ ...baseStyle, ...positions[position] }} />;
});
CornerDecor.displayName = 'CornerDecor';
// ============================================
// 扫描线覆盖层
// ============================================
const ScanlineOverlay: React.FC = memo(() => (
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
pointerEvents="none"
opacity={0.3}
sx={{
background: `repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(212, 175, 55, 0.02) 2px,
rgba(212, 175, 55, 0.02) 4px
)`,
}}
/>
));
ScanlineOverlay.displayName = 'ScanlineOverlay';
// ============================================
// 主组件
// ============================================
const GlassCard = forwardRef<HTMLDivElement, GlassCardProps>(
(
{
children,
variant = 'default',
hoverable = true,
glowing = false,
cornerDecor = false,
scanline = false,
rounded = 'lg',
padding = 'md',
...props
},
ref
) => {
const variantStyle = VARIANTS[variant];
return (
<Box
ref={ref}
position="relative"
bg={variantStyle.bg}
border={variantStyle.border}
borderRadius={ROUNDED_MAP[rounded]}
backdropFilter={variantStyle.backdropFilter}
p={PADDING_MAP[padding]}
transition={`all ${FUI_ANIMATION.duration.normal} ${FUI_ANIMATION.easing.smooth}`}
overflow="hidden"
_hover={
hoverable
? {
borderColor: FUI_COLORS.line.emphasis,
boxShadow: glowing ? FUI_GLOW.gold.md : FUI_GLOW.gold.sm,
transform: 'translateY(-2px)',
}
: undefined
}
sx={{
// 发光效果
...(glowing && {
boxShadow: FUI_GLOW.gold.sm,
animation: 'glowPulse 3s ease-in-out infinite',
}),
}}
{...props}
>
{/* 扫描线效果 */}
{scanline && <ScanlineOverlay />}
{/* 角落装饰 */}
{cornerDecor && (
<>
<CornerDecor position="tl" />
<CornerDecor position="tr" />
<CornerDecor position="bl" />
<CornerDecor position="br" />
</>
)}
{/* 内容 */}
<Box position="relative" zIndex={1}>
{children}
</Box>
</Box>
);
}
);
GlassCard.displayName = 'GlassCard';
export default memo(GlassCard);

View File

@@ -0,0 +1,21 @@
/**
* FUI 主题组件统一导出
*/
export { default as GlassCard } from './GlassCard';
export type { GlassCardProps } from './GlassCard';
export {
GlowText,
DataBadge,
StatusIndicator,
FUIDivider,
FUIProgressBar,
} from './FUIElements';
export type {
GlowTextProps,
DataBadgeProps,
StatusIndicatorProps,
FUIDividerProps,
FUIProgressBarProps,
} from './FUIElements';

View File

@@ -0,0 +1,141 @@
/**
* FUI 主题动画样式
*
* 在 Company 模块的入口文件中引入此文件:
* import './theme/fui-animations.css';
*/
/* 发光脉冲动画 */
@keyframes glowPulse {
0%, 100% {
box-shadow: 0 0 20px rgba(212, 175, 55, 0.3);
}
50% {
box-shadow: 0 0 30px rgba(212, 175, 55, 0.5), 0 0 60px rgba(212, 175, 55, 0.2);
}
}
/* 淡入动画 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 向上滑入动画 */
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 扫描线动画 */
@keyframes scanline {
0% {
transform: translateY(-100%);
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
transform: translateY(100%);
opacity: 0;
}
}
/* 闪烁动画 */
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* 边框发光动画 */
@keyframes borderGlow {
0%, 100% {
border-color: rgba(212, 175, 55, 0.2);
}
50% {
border-color: rgba(212, 175, 55, 0.5);
}
}
/* 脉冲动画 */
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 0.3;
}
50% {
transform: scale(1.5);
opacity: 0;
}
}
/* 数据流动动画 */
@keyframes dataFlow {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
/* 旋转发光动画 */
@keyframes rotateGlow {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 文字发光动画 */
@keyframes textGlow {
0%, 100% {
text-shadow: 0 0 10px rgba(212, 175, 55, 0.3);
}
50% {
text-shadow: 0 0 20px rgba(212, 175, 55, 0.6), 0 0 30px rgba(212, 175, 55, 0.4);
}
}
/* 浮动动画 */
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
/* 呼吸动画 */
@keyframes breathe {
0%, 100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}

View File

@@ -0,0 +1,336 @@
/**
* FUI (Fantasy User Interface) 科幻主题配置
*
* 设计灵感:
* - Ash Thorp: 精细线条、数据可视化、霓虹发光
* - Linear.app: 极简、精致微交互、清晰层次
* - James Turrell: 柔和光影渐变、沉浸式氛围
* - HeroUI: 现代组件风格
*/
// ============================================
// 核心色彩系统
// ============================================
export const FUI_COLORS = {
// 主色调 - 金色系(保持黑金风格)
gold: {
50: '#FDF8E8',
100: '#F8EDCB',
200: '#F0D78C',
300: '#E8C14D',
400: '#D4AF37', // 主金色
500: '#B8960C',
600: '#8B7209',
700: '#5E4D06',
800: '#312803',
900: '#1A1500',
},
// 环境光色 - James Turrell 光影
ambient: {
cyan: 'rgba(0, 255, 255, 0.15)',
magenta: 'rgba(255, 0, 255, 0.1)',
warm: 'rgba(255, 200, 100, 0.08)',
cool: 'rgba(100, 200, 255, 0.08)',
},
// 背景层次
bg: {
deep: '#0A0A14', // 最深层
primary: '#0F0F1A', // 主背景
elevated: '#1A1A2E', // 抬升层
surface: '#252540', // 表面层
overlay: 'rgba(26, 26, 46, 0.95)', // 覆盖层
},
// 边框 & 线条
line: {
subtle: 'rgba(212, 175, 55, 0.1)',
default: 'rgba(212, 175, 55, 0.2)',
emphasis: 'rgba(212, 175, 55, 0.4)',
glow: 'rgba(212, 175, 55, 0.6)',
},
// 文字
text: {
primary: 'rgba(255, 255, 255, 0.95)',
secondary: 'rgba(255, 255, 255, 0.7)',
muted: 'rgba(255, 255, 255, 0.5)',
dim: 'rgba(255, 255, 255, 0.3)',
},
// 状态色
status: {
positive: '#EF4444', // 涨 - 红
negative: '#22C55E', // 跌 - 绿
warning: '#F59E0B',
info: '#3B82F6',
},
} as const;
// ============================================
// 发光效果Glow Effects
// ============================================
export const FUI_GLOW = {
// 金色发光
gold: {
sm: '0 0 8px rgba(212, 175, 55, 0.3)',
md: '0 0 16px rgba(212, 175, 55, 0.4)',
lg: '0 0 32px rgba(212, 175, 55, 0.5)',
pulse: '0 0 20px rgba(212, 175, 55, 0.6), 0 0 40px rgba(212, 175, 55, 0.3)',
},
// 环境光发光 (Turrell style)
ambient: {
warm: '0 0 60px rgba(255, 200, 100, 0.15)',
cool: '0 0 60px rgba(100, 200, 255, 0.1)',
mixed: '0 0 80px rgba(212, 175, 55, 0.1), 0 0 120px rgba(100, 200, 255, 0.05)',
},
// 文字发光
text: {
gold: '0 0 10px rgba(212, 175, 55, 0.5)',
white: '0 0 10px rgba(255, 255, 255, 0.3)',
},
} as const;
// ============================================
// Glassmorphism 配置
// ============================================
export const FUI_GLASS = {
// 背景模糊
blur: {
sm: 'blur(8px)',
md: 'blur(16px)',
lg: 'blur(24px)',
xl: 'blur(40px)',
},
// 玻璃背景
bg: {
light: 'rgba(255, 255, 255, 0.03)',
medium: 'rgba(255, 255, 255, 0.05)',
dark: 'rgba(0, 0, 0, 0.2)',
gold: 'rgba(212, 175, 55, 0.05)',
},
// 边框
border: {
subtle: '1px solid rgba(255, 255, 255, 0.05)',
default: '1px solid rgba(212, 175, 55, 0.15)',
emphasis: '1px solid rgba(212, 175, 55, 0.3)',
},
} as const;
// ============================================
// 动画配置
// ============================================
export const FUI_ANIMATION = {
// 过渡时间
duration: {
instant: '0.1s',
fast: '0.2s',
normal: '0.3s',
slow: '0.5s',
slower: '0.8s',
},
// 缓动函数
easing: {
default: 'cubic-bezier(0.4, 0, 0.2, 1)',
smooth: 'cubic-bezier(0.25, 0.1, 0.25, 1)',
bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
expo: 'cubic-bezier(0.16, 1, 0.3, 1)',
},
// 预设动画
presets: {
fadeIn: 'fadeIn 0.3s ease-out',
slideUp: 'slideUp 0.3s ease-out',
glow: 'glowPulse 2s ease-in-out infinite',
scanline: 'scanline 3s linear infinite',
shimmer: 'shimmer 2s linear infinite',
},
} as const;
// ============================================
// CSS Keyframes需要在全局样式中注入
// ============================================
export const FUI_KEYFRAMES = `
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes glowPulse {
0%, 100% {
box-shadow: 0 0 20px rgba(212, 175, 55, 0.3);
}
50% {
box-shadow: 0 0 30px rgba(212, 175, 55, 0.5), 0 0 60px rgba(212, 175, 55, 0.2);
}
}
@keyframes scanline {
0% {
transform: translateY(-100%);
opacity: 0;
}
10% { opacity: 1; }
90% { opacity: 1; }
100% {
transform: translateY(100%);
opacity: 0;
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
@keyframes borderGlow {
0%, 100% {
border-color: rgba(212, 175, 55, 0.2);
}
50% {
border-color: rgba(212, 175, 55, 0.5);
}
}
`;
// ============================================
// 组件样式预设
// ============================================
export const FUI_STYLES = {
// Glassmorphism 卡片
glassCard: {
base: {
background: `linear-gradient(135deg, ${FUI_COLORS.bg.elevated} 0%, ${FUI_COLORS.bg.primary} 100%)`,
backdropFilter: FUI_GLASS.blur.md,
border: FUI_GLASS.border.default,
borderRadius: '16px',
transition: `all ${FUI_ANIMATION.duration.normal} ${FUI_ANIMATION.easing.smooth}`,
},
hover: {
borderColor: FUI_COLORS.line.emphasis,
boxShadow: FUI_GLOW.gold.md,
transform: 'translateY(-2px)',
},
},
// FUI 面板(带角落装饰)
fuiPanel: {
base: {
position: 'relative' as const,
background: FUI_COLORS.bg.elevated,
border: FUI_GLASS.border.default,
borderRadius: '12px',
overflow: 'hidden',
},
cornerDecor: {
position: 'absolute' as const,
width: '20px',
height: '20px',
borderColor: FUI_COLORS.gold[400],
borderStyle: 'solid',
},
},
// 扫描线效果
scanlineOverlay: {
position: 'absolute' as const,
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: 'none' as const,
background: `repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(212, 175, 55, 0.03) 2px,
rgba(212, 175, 55, 0.03) 4px
)`,
opacity: 0.5,
},
// 数据标签
dataTag: {
display: 'inline-flex',
alignItems: 'center',
padding: '4px 12px',
background: 'rgba(212, 175, 55, 0.1)',
border: '1px solid rgba(212, 175, 55, 0.3)',
borderRadius: '20px',
fontSize: '12px',
fontWeight: 500,
color: FUI_COLORS.gold[400],
letterSpacing: '0.5px',
},
// 发光按钮
glowButton: {
base: {
background: `linear-gradient(135deg, ${FUI_COLORS.gold[500]} 0%, ${FUI_COLORS.gold[400]} 100%)`,
color: FUI_COLORS.bg.deep,
fontWeight: 'bold',
borderRadius: '8px',
transition: `all ${FUI_ANIMATION.duration.fast} ${FUI_ANIMATION.easing.default}`,
},
hover: {
boxShadow: FUI_GLOW.gold.md,
transform: 'translateY(-1px)',
},
},
// 输入框
input: {
base: {
background: FUI_COLORS.bg.primary,
border: FUI_GLASS.border.default,
borderRadius: '8px',
color: FUI_COLORS.text.primary,
transition: `all ${FUI_ANIMATION.duration.fast} ${FUI_ANIMATION.easing.default}`,
},
focus: {
borderColor: FUI_COLORS.gold[400],
boxShadow: FUI_GLOW.gold.sm,
},
},
} as const;
// ============================================
// 导出完整主题对象
// ============================================
export const FUI_THEME = {
colors: FUI_COLORS,
glow: FUI_GLOW,
glass: FUI_GLASS,
animation: FUI_ANIMATION,
styles: FUI_STYLES,
} as const;
export default FUI_THEME;

View File

@@ -0,0 +1,17 @@
/**
* Company 页面 FUI 主题统一导出
*/
// 主题配置
export { default as FUI_THEME } from './fui';
export {
FUI_COLORS,
FUI_GLOW,
FUI_GLASS,
FUI_ANIMATION,
FUI_KEYFRAMES,
FUI_STYLES,
} from './fui';
// 主题组件
export * from './components';