Files
invest-mind-store/apps/web/src/pages/stock-daily-price/StockDailyPricePage.tsx
2026-01-12 17:38:55 +08:00

385 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useRef } from 'react';
import {
Table,
Button,
Input,
Select,
Space,
Card,
Form,
Row,
Col,
App as AntdApp,
Tag,
DatePicker,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons';
import { stockDailyPriceService } from '@/services/stock-daily-price';
import type { StockDailyPrice, QueryStockDailyPriceRequest } from '@/types/stock-daily-price';
import { MARKET_OPTIONS, getMarketText } from '@/types/stock-daily-price';
import dayjs from 'dayjs';
import './StockDailyPricePage.css';
const { Option } = Select;
const { RangePicker } = DatePicker;
const StockDailyPricePage = () => {
const { message: messageApi } = AntdApp.useApp();
const [prices, setPrices] = useState<StockDailyPrice[]>([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
const [form] = Form.useForm();
const formRef = useRef<QueryStockDailyPriceRequest>({});
// 初始化默认查询最近7天
useEffect(() => {
const endDate = dayjs();
const startDate = endDate.subtract(6, 'day'); // 最近7天包含今天
form.setFieldsValue({
dateRange: [startDate, endDate],
});
formRef.current = {
startDate: startDate.format('YYYY-MM-DD'),
endDate: endDate.format('YYYY-MM-DD'),
};
loadData(formRef.current, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 加载数据
const loadData = async (params?: QueryStockDailyPriceRequest, resetPage = false) => {
setLoading(true);
try {
const currentPage = resetPage ? 1 : pagination.current;
const pageSize = pagination.pageSize;
const queryParams: QueryStockDailyPriceRequest = {
page: currentPage,
limit: pageSize,
sortBy: 'tradeDate',
sortOrder: 'DESC',
...formRef.current,
...params,
};
const response = await stockDailyPriceService.getStockDailyPriceList(queryParams);
setPrices(response.list);
setPagination((prev) => ({
...prev,
current: response.pagination.current_page,
pageSize: response.pagination.page_size,
total: response.pagination.total,
}));
} catch (error: any) {
messageApi.error(error.message || '加载股票价格列表失败');
} finally {
setLoading(false);
}
};
// 当分页改变时,重新加载数据
useEffect(() => {
if (pagination.current > 0 && pagination.pageSize > 0) {
loadData();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pagination.current, pagination.pageSize]);
// 查询
const handleSearch = () => {
const values = form.getFieldsValue();
const queryParams: QueryStockDailyPriceRequest = {
stockCode: values.stockCode || undefined,
stockName: values.stockName || undefined,
market: values.market || undefined,
};
// 处理日期范围
if (values.dateRange && values.dateRange.length === 2) {
queryParams.startDate = values.dateRange[0].format('YYYY-MM-DD');
queryParams.endDate = values.dateRange[1].format('YYYY-MM-DD');
}
formRef.current = queryParams;
loadData(queryParams, true);
};
// 重置
const handleReset = () => {
form.resetFields();
// 重置为默认的最近7天
const endDate = dayjs();
const startDate = endDate.subtract(6, 'day');
form.setFieldsValue({
dateRange: [startDate, endDate],
});
formRef.current = {
startDate: startDate.format('YYYY-MM-DD'),
endDate: endDate.format('YYYY-MM-DD'),
};
loadData(formRef.current, true);
};
// 格式化价格
const formatPrice = (price?: number) => {
if (price === null || price === undefined) return '-';
return price.toFixed(2);
};
// 格式化金额
const formatAmount = (amount?: number) => {
if (amount === null || amount === undefined) return '-';
if (amount >= 100000000) {
return `${(amount / 100000000).toFixed(2)}亿`;
}
if (amount >= 10000) {
return `${(amount / 10000).toFixed(2)}`;
}
return amount.toFixed(2);
};
// 格式化成交量
const formatVolume = (volume?: number) => {
if (volume === null || volume === undefined) return '-';
if (volume >= 10000) {
return `${(volume / 10000).toFixed(2)}万手`;
}
return `${volume}`;
};
// 表格列定义
const columns: ColumnsType<StockDailyPrice> = [
{
title: '股票代码',
dataIndex: 'stockCode',
key: 'stockCode',
width: 120,
fixed: 'left',
},
{
title: '股票名称',
dataIndex: 'stockName',
key: 'stockName',
width: 150,
fixed: 'left',
},
{
title: '市场',
dataIndex: 'market',
key: 'market',
width: 100,
render: (market: string) => <Tag color="blue">{getMarketText(market)}</Tag>,
},
{
title: '交易日期',
dataIndex: 'tradeDate',
key: 'tradeDate',
width: 120,
render: (date: Date) => dayjs(date).format('YYYY-MM-DD'),
},
{
title: '开盘价',
dataIndex: 'openPrice',
key: 'openPrice',
width: 100,
align: 'right',
render: formatPrice,
},
{
title: '收盘价',
dataIndex: 'closePrice',
key: 'closePrice',
width: 100,
align: 'right',
render: formatPrice,
},
{
title: '最高价',
dataIndex: 'highPrice',
key: 'highPrice',
width: 100,
align: 'right',
render: formatPrice,
},
{
title: '最低价',
dataIndex: 'lowPrice',
key: 'lowPrice',
width: 100,
align: 'right',
render: formatPrice,
},
{
title: '涨跌额',
dataIndex: 'changeAmount',
key: 'changeAmount',
width: 100,
align: 'right',
render: (amount?: number) => {
if (amount === null || amount === undefined) return '-';
const color = amount >= 0 ? '#ff4d4f' : '#52c41a';
return <span style={{ color }}>{formatPrice(amount)}</span>;
},
},
{
title: '涨跌幅',
dataIndex: 'changePercent',
key: 'changePercent',
width: 100,
align: 'right',
render: (percent?: number) => {
if (percent === null || percent === undefined) return '-';
const color = percent >= 0 ? '#ff4d4f' : '#52c41a';
return (
<span style={{ color }}>
{percent >= 0 ? '+' : ''}
{percent.toFixed(2)}%
</span>
);
},
},
{
title: '成交量',
dataIndex: 'volume',
key: 'volume',
width: 120,
align: 'right',
render: formatVolume,
},
{
title: '成交额',
dataIndex: 'amount',
key: 'amount',
width: 150,
align: 'right',
render: formatAmount,
},
{
title: '换手率',
dataIndex: 'turnoverRate',
key: 'turnoverRate',
width: 100,
align: 'right',
render: (rate?: number) => {
if (rate === null || rate === undefined) return '-';
return `${rate.toFixed(2)}%`;
},
},
{
title: '市盈率',
dataIndex: 'peRatio',
key: 'peRatio',
width: 100,
align: 'right',
render: formatPrice,
},
{
title: '市净率',
dataIndex: 'pbRatio',
key: 'pbRatio',
width: 100,
align: 'right',
render: formatPrice,
},
];
return (
<div className="stock-daily-price-page">
<Card>
{/* 查询表单 */}
<Form form={form} layout="inline" className="stock-daily-price-search-form">
<Row gutter={16} style={{ width: '100%' }}>
<Col span={6}>
<Form.Item name="stockCode" label="股票代码">
<Input
placeholder="请输入股票代码"
allowClear
onPressEnter={handleSearch}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="stockName" label="股票名称">
<Input
placeholder="请输入股票名称"
allowClear
onPressEnter={handleSearch}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="market" label="市场">
<Select
placeholder="请选择市场"
allowClear
style={{ width: '100%' }}
>
{MARKET_OPTIONS.map((option) => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16} style={{ width: '100%' }}>
<Col span={6}>
<Form.Item name="dateRange" label="日期范围" layout="horizontal">
<RangePicker style={{ width: '100%' }} format="YYYY-MM-DD" />
</Form.Item>
</Col>
<Col span={18}>
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Space>
<Button
type="primary"
icon={<SearchOutlined />}
onClick={handleSearch}
>
</Button>
<Button icon={<ReloadOutlined />} onClick={handleReset}>
</Button>
</Space>
</Form.Item>
</Col>
</Row>
</Form>
{/* 表格 */}
<Table
columns={columns}
dataSource={prices}
rowKey="id"
loading={loading}
pagination={{
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
showSizeChanger: true,
showTotal: (total) => `${total}`,
onChange: (page, pageSize) => {
setPagination((prev) => ({
...prev,
current: page,
pageSize: pageSize || 10,
}));
},
}}
scroll={{ x: 1600 }}
/>
</Card>
</div>
);
};
export default StockDailyPricePage;