feat: 开发持仓、股票信息相关接口
This commit is contained in:
384
apps/web/src/pages/stock-daily-price/StockDailyPricePage.tsx
Normal file
384
apps/web/src/pages/stock-daily-price/StockDailyPricePage.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user