385 lines
13 KiB
TypeScript
385 lines
13 KiB
TypeScript
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;
|