feat: 开发持仓、股票信息相关接口

This commit is contained in:
R524809
2026-01-12 17:38:55 +08:00
parent 67e4dc6382
commit 838a021ce5
46 changed files with 4407 additions and 12 deletions

View File

@@ -0,0 +1,384 @@
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;