Compare commits

...

3 Commits

Author SHA1 Message Date
zdl
3382dd1036 feat: UI调整 2025-12-10 10:09:24 +08:00
zdl
9423094af2 pref: 移除 useColorModeValue
UI调整
2025-12-09 19:26:52 +08:00
zdl
4f38505a80 style: StockQuoteCard 黑金主题 UI 调整
颜色配置:
- 背景:纯黑 #000000
- 边框/标签:金色 #C9A961
- 主要文字:亮金 #F4D03F
- 涨:红色 #F44336(红涨绿跌)
- 跌:绿色 #4CAF50

字体大小:
- 股票价格:48px bold
- 股票名称/代码:24px bold
- 涨跌幅 Badge:20px bold
- 关键指标数值:16px bold
- 标签文字:14px

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 18:54:19 +08:00
10 changed files with 275 additions and 174 deletions

View File

@@ -0,0 +1,84 @@
/**
* FavoriteButton - 通用关注/收藏按钮组件(图标按钮)
*/
import React from 'react';
import { IconButton, Tooltip, Spinner } from '@chakra-ui/react';
import { Star } from 'lucide-react';
export interface FavoriteButtonProps {
/** 是否已关注 */
isFavorite: boolean;
/** 加载状态 */
isLoading?: boolean;
/** 点击回调 */
onClick: () => void;
/** 按钮大小 */
size?: 'sm' | 'md' | 'lg';
/** 颜色主题 */
colorScheme?: 'gold' | 'default';
/** 是否显示 tooltip */
showTooltip?: boolean;
}
// 颜色配置
const COLORS = {
gold: {
active: '#F4D03F', // 已关注 - 亮金色
inactive: '#C9A961', // 未关注 - 暗金色
hoverBg: 'whiteAlpha.100',
},
default: {
active: 'yellow.400',
inactive: 'gray.400',
hoverBg: 'gray.100',
},
};
const FavoriteButton: React.FC<FavoriteButtonProps> = ({
isFavorite,
isLoading = false,
onClick,
size = 'sm',
colorScheme = 'gold',
showTooltip = true,
}) => {
const colors = COLORS[colorScheme];
const currentColor = isFavorite ? colors.active : colors.inactive;
const label = isFavorite ? '取消关注' : '加入自选';
const iconButton = (
<IconButton
aria-label={label}
icon={
isLoading ? (
<Spinner size="sm" color={currentColor} />
) : (
<Star
size={size === 'sm' ? 18 : size === 'md' ? 20 : 24}
fill={isFavorite ? currentColor : 'none'}
stroke={currentColor}
/>
)
}
variant="ghost"
color={currentColor}
size={size}
onClick={onClick}
isDisabled={isLoading}
_hover={{ bg: colors.hoverBg }}
/>
);
if (showTooltip) {
return (
<Tooltip label={label} placement="top">
{iconButton}
</Tooltip>
);
}
return iconButton;
};
export default FavoriteButton;

View File

@@ -1,5 +1,5 @@
// src/views/Company/components/CompanyHeader/SearchBar.js
// 股票搜索栏组件
// 股票搜索栏组件 - 金色主题
import React from 'react';
import {
@@ -30,7 +30,7 @@ const SearchBar = ({
<HStack spacing={3}>
<InputGroup size="lg" maxW="300px">
<InputLeftElement pointerEvents="none">
<SearchIcon color="gray.400" />
<SearchIcon color="#C9A961" />
</InputLeftElement>
<Input
placeholder="输入股票代码"
@@ -38,17 +38,27 @@ const SearchBar = ({
onChange={(e) => onInputChange(e.target.value)}
onKeyPress={onKeyPress}
borderRadius="md"
color="white"
borderColor="#C9A961"
_placeholder={{ color: '#C9A961' }}
_focus={{
borderColor: 'blue.500',
boxShadow: '0 0 0 1px #3182ce',
borderColor: '#F4D03F',
boxShadow: '0 0 0 1px #F4D03F',
}}
_hover={{
borderColor: '#F4D03F',
}}
/>
</InputGroup>
<Button
colorScheme="blue"
size="lg"
onClick={onSearch}
leftIcon={<SearchIcon />}
bg="#1A202C"
color="#C9A961"
borderWidth="1px"
borderColor="#C9A961"
_hover={{ bg: '#1a1a1a', borderColor: '#F4D03F', color: '#F4D03F' }}
>
查询
</Button>

View File

@@ -1,35 +0,0 @@
// src/views/Company/components/CompanyHeader/WatchlistButton.js
// 自选股按钮组件
import React from 'react';
import { Button } from '@chakra-ui/react';
import { StarIcon } from '@chakra-ui/icons';
/**
* 自选股按钮组件
*
* @param {Object} props
* @param {boolean} props.isInWatchlist - 是否已在自选股中
* @param {boolean} props.isLoading - 是否正在加载
* @param {Function} props.onClick - 点击回调
*/
const WatchlistButton = ({
isInWatchlist,
isLoading,
onClick,
}) => {
return (
<Button
colorScheme={isInWatchlist ? 'yellow' : 'teal'}
variant={isInWatchlist ? 'solid' : 'outline'}
size="lg"
onClick={onClick}
leftIcon={<StarIcon />}
isLoading={isLoading}
>
{isInWatchlist ? '已关注' : '关注'}
</Button>
);
};
export default WatchlistButton;

View File

@@ -9,82 +9,50 @@ import {
VStack,
Heading,
Text,
Badge,
} from '@chakra-ui/react';
import SearchBar from './SearchBar';
import WatchlistButton from './WatchlistButton';
/**
* 公司详情页面头部区域组件
*
* 包含:
* - 页面标题和描述
* - 页面标题和描述(金色主题)
* - 股票搜索栏
* - 自选股按钮
* - 当前股票代码显示
*
* @param {Object} props
* @param {string} props.stockCode - 当前股票代码
* @param {string} props.inputCode - 搜索输入框值
* @param {Function} props.onInputChange - 输入变化回调
* @param {Function} props.onSearch - 搜索回调
* @param {Function} props.onKeyPress - 键盘事件回调
* @param {boolean} props.isInWatchlist - 是否在自选股中
* @param {boolean} props.isWatchlistLoading - 自选股操作加载中
* @param {Function} props.onWatchlistToggle - 自选股切换回调
* @param {string} props.bgColor - 背景颜色
*/
const CompanyHeader = ({
stockCode,
inputCode,
onInputChange,
onSearch,
onKeyPress,
isInWatchlist,
isWatchlistLoading,
onWatchlistToggle,
bgColor,
}) => {
return (
<Card bg={bgColor} shadow="md">
<CardBody>
<HStack justify="space-between" align="center">
{/* 标题区域 */}
{/* 标题区域 - 金色主题 */}
<VStack align="start" spacing={1}>
<Heading size="lg">个股详情</Heading>
<Text color="gray.600" fontSize="sm">
<Heading size="lg" color="#F4D03F">个股详情</Heading>
<Text color="#C9A961" fontSize="sm">
查看股票实时行情财务数据和盈利预测
</Text>
</VStack>
{/* 操作区域 */}
<HStack spacing={3}>
{/* 搜索栏 */}
<SearchBar
inputCode={inputCode}
onInputChange={onInputChange}
onSearch={onSearch}
onKeyPress={onKeyPress}
/>
{/* 自选股按钮 */}
<WatchlistButton
isInWatchlist={isInWatchlist}
isLoading={isWatchlistLoading}
onClick={onWatchlistToggle}
/>
</HStack>
</HStack>
{/* 当前股票信息 */}
<HStack mt={4} spacing={4}>
<Badge colorScheme="blue" fontSize="md" px={3} py={1}>
股票代码: {stockCode}
</Badge>
<Text fontSize="sm" color="gray.600">
更新时间: {new Date().toLocaleString()}
</Text>
{/* 搜索栏 */}
<SearchBar
inputCode={inputCode}
onInputChange={onInputChange}
onSearch={onSearch}
onKeyPress={onKeyPress}
/>
</HStack>
</CardBody>
</Card>

View File

@@ -1,5 +1,5 @@
// src/views/Company/components/CompanyTabs/TabNavigation.js
// Tab 导航组件 - 动态渲染 Tab 按钮
// Tab 导航组件 - 动态渲染 Tab 按钮(黑金主题)
import React from 'react';
import {
@@ -10,31 +10,41 @@ import {
Text,
} from '@chakra-ui/react';
import { COMPANY_TABS, TAB_SELECTED_STYLE } from '../../constants';
import { COMPANY_TABS } from '../../constants';
// 黑金主题颜色配置
const THEME_COLORS = {
bg: '#1A202C', // 背景纯黑
selectedBg: '#C9A961', // 选中项金色背景
selectedText: '#FFFFFF', // 选中项白色文字
unselectedText: '#999999', // 未选中项深灰色
};
/**
* Tab 导航组件
*
* @param {Object} props
* @param {string} props.tabBg - Tab 列表背景色
* @param {string} props.activeBg - 激活状态背景色
* Tab 导航组件(黑金主题)
*/
const TabNavigation = ({ tabBg, activeBg }) => {
const TabNavigation = () => {
return (
<TabList p={4} bg={tabBg}>
<TabList py={4} bg={THEME_COLORS.bg} borderTopLeftRadius="16px" borderTopRightRadius="16px">
{COMPANY_TABS.map((tab, index) => (
<Tab
key={tab.key}
color={THEME_COLORS.unselectedText}
borderRadius="full"
px={4}
py={2}
_selected={{
bg: activeBg,
color: 'white',
...TAB_SELECTED_STYLE,
bg: THEME_COLORS.selectedBg,
color: THEME_COLORS.selectedText,
}}
_hover={{
color: THEME_COLORS.selectedText,
}}
mr={index < COMPANY_TABS.length - 1 ? 2 : 0}
>
<HStack spacing={2}>
<Icon as={tab.icon} />
<Text>{tab.name}</Text>
<Icon as={tab.icon} boxSize="18px" />
<Text fontSize="15px">{tab.name}</Text>
</HStack>
</Tab>
))}

View File

@@ -46,9 +46,8 @@ const TAB_COMPONENTS = {
* @param {Object} props
* @param {string} props.stockCode - 当前股票代码
* @param {Function} props.onTabChange - Tab 变更回调 (index, tabName, prevIndex) => void
* @param {string} props.bgColor - 背景颜色
*/
const CompanyTabs = ({ stockCode, onTabChange, bgColor }) => {
const CompanyTabs = ({ stockCode, onTabChange }) => {
const [currentIndex, setCurrentIndex] = useState(0);
/**
@@ -65,7 +64,7 @@ const CompanyTabs = ({ stockCode, onTabChange, bgColor }) => {
};
return (
<Card bg={bgColor} shadow="lg">
<Card shadow="lg" bg='#1A202C'>
<CardBody p={0}>
<Tabs
variant="soft-rounded"
@@ -74,8 +73,8 @@ const CompanyTabs = ({ stockCode, onTabChange, bgColor }) => {
index={currentIndex}
onChange={handleTabChange}
>
{/* Tab 导航 */}
<TabNavigation tabBg="gray.50" activeBg="blue.500" />
{/* Tab 导航(黑金主题) */}
<TabNavigation />
<Divider />
@@ -84,7 +83,7 @@ const CompanyTabs = ({ stockCode, onTabChange, bgColor }) => {
{COMPANY_TABS.map((tab) => {
const Component = TAB_COMPONENTS[tab.key];
return (
<TabPanel key={tab.key} p={6}>
<TabPanel key={tab.key}>
<Component stockCode={stockCode} />
</TabPanel>
);

View File

@@ -16,8 +16,12 @@ import {
Badge,
Progress,
Skeleton,
IconButton,
Tooltip,
} from '@chakra-ui/react';
import { Share2 } from 'lucide-react';
import FavoriteButton from '@components/FavoriteButton';
import type { StockQuoteCardProps } from './types';
import { mockStockQuoteData } from './mockData';
@@ -50,17 +54,28 @@ const formatNetInflow = (value: number): string => {
const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
data = mockStockQuoteData,
isLoading = false,
isInWatchlist = false,
isWatchlistLoading = false,
onWatchlistToggle,
onShare,
}) => {
// 颜色配置
const cardBg = 'white';
const borderColor = 'gray.200';
const labelColor = 'gray.500';
const valueColor = 'gray.800';
const sectionTitleColor = 'gray.600';
// 处理分享点击
const handleShare = () => {
onShare?.();
};
// 涨跌颜色
const priceColor = data.changePercent >= 0 ? 'green.500' : 'red.500';
const inflowColor = data.mainNetInflow >= 0 ? 'green.500' : 'red.500';
// 黑金主题颜色配置
const cardBg = '#1A202C';
const borderColor = '#C9A961';
const labelColor = '#C9A961';
const valueColor = '#F4D03F';
const sectionTitleColor = '#F4D03F';
// 涨跌颜色(红涨绿跌)
const upColor = '#F44336'; // 涨 - 红色
const downColor = '#4CAF50'; // 跌 - 绿色
const priceColor = data.changePercent >= 0 ? upColor : downColor;
const inflowColor = data.mainNetInflow >= 0 ? upColor : downColor;
if (isLoading) {
return (
@@ -74,31 +89,48 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
return (
<Card bg={cardBg} shadow="sm" borderWidth="1px" borderColor={borderColor}>
<CardBody py={4} px={6}>
{/* 顶部:股票名称 + 更新时间 */}
<CardBody>
{/* 顶部:股票名称 + 关注/分享按钮 + 更新时间 */}
<Flex justify="space-between" align="center" mb={4}>
<HStack spacing={3}>
<Text fontSize="xl" fontWeight="bold" color={valueColor}>
{data.name}
{/* 左侧:名称(代码) | 指数标签 */}
<HStack spacing={2}>
<Text fontSize="22px" fontWeight="bold" color={valueColor}>
{data.name}{data.code}
</Text>
{data.indexTags.length > 0 && (
<>
<Text color={labelColor} fontSize="22px" fontWeight="light">|</Text>
<Text fontSize="16px" color={labelColor}>
{data.indexTags.join('、')}
</Text>
</>
)}
</HStack>
{/* 右侧:关注 + 分享 + 时间 */}
<HStack spacing={3}>
<FavoriteButton
isFavorite={isInWatchlist}
isLoading={isWatchlistLoading}
onClick={onWatchlistToggle || (() => {})}
colorScheme="gold"
size="sm"
/>
<Tooltip label="分享" placement="top">
<IconButton
aria-label="分享"
icon={<Share2 size={18} />}
variant="ghost"
color={labelColor}
size="sm"
onClick={handleShare}
_hover={{ bg: 'whiteAlpha.100' }}
/>
</Tooltip>
<Text fontSize="14px" color={labelColor}>
{data.updateTime.split(' ')[1]}
</Text>
<Text fontSize="md" color={labelColor}>
({data.code})
</Text>
{data.indexTags.map((tag) => (
<Badge
key={tag}
variant="outline"
colorScheme="gray"
fontSize="xs"
px={2}
>
{tag}
</Badge>
))}
</HStack>
<Text fontSize="sm" color={labelColor}>
{data.updateTime}
</Text>
</Flex>
{/* 三栏布局 */}
@@ -106,66 +138,85 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
{/* 左栏:价格信息 */}
<Box flex="1">
<HStack align="baseline" spacing={3} mb={3}>
<Text fontSize="3xl" fontWeight="bold" color={priceColor}>
<Text fontSize="48px" fontWeight="bold" color={priceColor}>
{formatPrice(data.currentPrice)}
</Text>
<Badge
colorScheme={data.changePercent >= 0 ? 'green' : 'red'}
fontSize="md"
px={2}
py={0.5}
bg={data.changePercent >= 0 ? upColor : downColor}
color="#FFFFFF"
fontSize="20px"
fontWeight="bold"
px={3}
py={1}
borderRadius="md"
>
{formatChangePercent(data.changePercent)}
</Badge>
</HStack>
<HStack spacing={6} color={labelColor} fontSize="sm">
<Text>
{/* 次要行情:今开 | 昨收 | 最高 | 最低 */}
<HStack spacing={4} fontSize="14px" flexWrap="wrap">
<Text color={labelColor}>
<Text as="span" color={valueColor} fontWeight="medium">
<Text as="span" color={valueColor} fontWeight="bold">
{formatPrice(data.todayOpen)}
</Text>
</Text>
<Text>
<Text color={borderColor}>|</Text>
<Text color={labelColor}>
<Text as="span" color={valueColor} fontWeight="medium">
<Text as="span" color={valueColor} fontWeight="bold">
{formatPrice(data.yesterdayClose)}
</Text>
</Text>
<Text color={borderColor}>|</Text>
<Text color={labelColor}>
<Text as="span" color={upColor} fontWeight="bold">
{formatPrice(data.todayHigh)}
</Text>
</Text>
<Text color={borderColor}>|</Text>
<Text color={labelColor}>
<Text as="span" color={downColor} fontWeight="bold">
{formatPrice(data.todayLow)}
</Text>
</Text>
</HStack>
</Box>
{/* 中栏:关键指标 */}
<Box flex="1" borderLeftWidth="1px" borderColor={borderColor} pl={8}>
<Text
fontSize="sm"
fontWeight="medium"
fontSize="14px"
fontWeight="bold"
color={sectionTitleColor}
mb={3}
>
</Text>
<VStack align="stretch" spacing={2} fontSize="sm">
<VStack align="stretch" spacing={2} fontSize="14px">
<HStack justify="space-between">
<Text color={labelColor}>(PE)</Text>
<Text color={valueColor} fontWeight="medium">
<Text color={valueColor} fontWeight="bold" fontSize="16px">
{data.pe.toFixed(2)}
</Text>
</HStack>
<HStack justify="space-between">
<Text color={labelColor}>(PB)</Text>
<Text color={valueColor} fontWeight="medium">
<Text color={valueColor} fontWeight="bold" fontSize="16px">
{data.pb.toFixed(2)}
</Text>
</HStack>
<HStack justify="space-between">
<Text color={labelColor}></Text>
<Text color={valueColor} fontWeight="medium">
<Text color={valueColor} fontWeight="bold" fontSize="16px">
{data.marketCap}
</Text>
</HStack>
<HStack justify="space-between">
<Text color={labelColor}>52</Text>
<Text color={valueColor} fontWeight="medium">
<Text color={valueColor} fontWeight="bold" fontSize="16px">
{formatPrice(data.week52Low)}-{formatPrice(data.week52High)}
</Text>
</HStack>
@@ -175,23 +226,23 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
{/* 右栏:主力动态 */}
<Box flex="1" borderLeftWidth="1px" borderColor={borderColor} pl={8}>
<Text
fontSize="sm"
fontWeight="medium"
fontSize="14px"
fontWeight="bold"
color={sectionTitleColor}
mb={3}
>
</Text>
<VStack align="stretch" spacing={2} fontSize="sm">
<VStack align="stretch" spacing={2} fontSize="14px">
<HStack justify="space-between">
<Text color={labelColor}></Text>
<Text color={inflowColor} fontWeight="medium">
<Text color={inflowColor} fontWeight="bold" fontSize="16px">
{formatNetInflow(data.mainNetInflow)}
</Text>
</HStack>
<HStack justify="space-between">
<Text color={labelColor}></Text>
<Text color={valueColor} fontWeight="medium">
<Text color={valueColor} fontWeight="bold" fontSize="16px">
{data.institutionHolding.toFixed(2)}%
</Text>
</HStack>
@@ -200,13 +251,15 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
<Progress
value={data.buyRatio}
size="sm"
colorScheme="green"
bg="red.400"
sx={{
'& > div': { bg: upColor },
}}
bg={downColor}
borderRadius="full"
/>
<HStack justify="space-between" mt={1} fontSize="xs">
<Text color="green.500">{data.buyRatio}%</Text>
<Text color="red.500">{data.sellRatio}%</Text>
<HStack justify="space-between" mt={1} fontSize="14px">
<Text color={upColor}>{data.buyRatio}%</Text>
<Text color={downColor}>{data.sellRatio}%</Text>
</HStack>
</Box>
</VStack>

View File

@@ -14,6 +14,8 @@ export const mockStockQuoteData: StockQuoteCardData = {
changePercent: 3.65,
todayOpen: 2156.0,
yesterdayClose: 2101.0,
todayHigh: 2185.0,
todayLow: 2150.0,
// 关键指标
pe: 38.62,
@@ -30,4 +32,7 @@ export const mockStockQuoteData: StockQuoteCardData = {
// 更新时间
updateTime: '2025-12-03 14:30:25',
// 自选状态
isFavorite: false,
};

View File

@@ -16,6 +16,8 @@ export interface StockQuoteCardData {
changePercent: number; // 涨跌幅(百分比,如 3.65 表示 +3.65%
todayOpen: number; // 今开
yesterdayClose: number; // 昨收
todayHigh: number; // 今日最高
todayLow: number; // 今日最低
// 关键指标
pe: number; // 市盈率
@@ -32,6 +34,9 @@ export interface StockQuoteCardData {
// 更新时间
updateTime: string; // 格式YYYY-MM-DD HH:mm:ss
// 自选状态
isFavorite?: boolean; // 是否已加入自选
}
/**
@@ -40,4 +45,10 @@ export interface StockQuoteCardData {
export interface StockQuoteCardProps {
data?: StockQuoteCardData;
isLoading?: boolean;
// 自选股相关(与 WatchlistButton 接口保持一致)
isInWatchlist?: boolean; // 是否在自选股中
isWatchlistLoading?: boolean; // 自选股操作加载中
onWatchlistToggle?: () => void; // 自选股切换回调
// 分享
onShare?: () => void; // 分享回调
}

View File

@@ -64,30 +64,26 @@ const CompanyIndex = () => {
}, [stockCode, trackStockSearched]);
return (
<Container maxW="container.xl" py={5}>
<VStack align="stretch" spacing={5}>
{/* 页面头部:标题、搜索、自选股按钮 */}
<Container maxW="container.xl" py={0} bg='#1A202C'>
<VStack align="stretch" spacing={0}>
{/* 页面头部:标题、搜索 */}
<CompanyHeader
stockCode={stockCode}
inputCode={inputCode}
onInputChange={setInputCode}
onSearch={handleSearch}
onKeyPress={handleKeyPress}
bgColor="#1A202C"
/>
{/* 股票行情卡片:价格、关键指标、主力动态、自选股按钮 */}
<StockQuoteCard
isInWatchlist={isInWatchlist}
isWatchlistLoading={isWatchlistLoading}
onWatchlistToggle={handleWatchlistToggle}
bgColor="white"
/>
{/* 股票行情卡片:价格、关键指标、主力动态 */}
<StockQuoteCard />
{/* Tab 切换区域:概览、行情、财务、预测 */}
<CompanyTabs
stockCode={stockCode}
onTabChange={trackTabChanged}
bgColor="white"
/>
<CompanyTabs stockCode={stockCode} onTabChange={trackTabChanged} bgColor="#1A202C"/>
</VStack>
</Container>
);