import { useState, useEffect, useCallback, useRef, useMemo } from 'react' import { Table, Button, Input, Select, Space, Row, Col, Card, Progress, message, Popconfirm, Modal, Tabs, Tooltip, Form, Typography, } from 'antd' import { PlusOutlined, DeleteOutlined, PlayCircleOutlined, UploadOutlined, DownloadOutlined, SyncOutlined, AccountBookOutlined, } from '@ant-design/icons' import { portfolioApi } from '../services/portfolioApi' import DecisionBadge from '../components/DecisionBadge' const { Text } = Typography const DEFAULT_ACCOUNT = '默认账户' const formatMoney = (v) => v == null ? '—' : `¥${v.toFixed(2)}`; const formatPct = (v) => v == null ? '—' : `${v >= 0 ? '+' : ''}${v.toFixed(2)}%`; // ============== Tab 1: Watchlist ============== function WatchlistTab() { const [data, setData] = useState([]) const [loading, setLoading] = useState(true) const [addOpen, setAddOpen] = useState(false) const [form] = Form.useForm() const fetch_ = useCallback(async () => { setLoading(true) try { const res = await portfolioApi.getWatchlist() setData(res.watchlist || []) } catch { message.error('加载失败') } finally { setLoading(false) } }, []) useEffect(() => { fetch_() }, [fetch_]) const handleAdd = async (vals) => { try { await portfolioApi.addToWatchlist(vals.ticker, vals.name || vals.ticker) message.success('已添加') setAddOpen(false) form.resetFields() fetch_() } catch (e) { message.error(e.message) } } const handleDelete = async (ticker) => { try { await portfolioApi.removeFromWatchlist(ticker) message.success('已移除') fetch_() } catch (e) { message.error(e.message) } } const columns = [ { title: '代码', dataIndex: 'ticker', key: 'ticker', width: 120, render: t => {t} }, { title: '名称', dataIndex: 'name', key: 'name', render: t => {t} }, { title: '添加日期', dataIndex: 'added_at', key: 'added_at', width: 120 }, { title: '操作', key: 'action', width: 100, render: (_, r) => ( handleDelete(r.ticker)} okText="确认" cancelText="取消"> ), }, ] return (
自选股列表
{data.length === 0 && !loading && (
暂无自选股
点击上方"添加"将股票加入自选
)} { setAddOpen(false); form.resetFields() }} footer={null}>
) } // ============== Tab 2: Positions ============== function PositionsTab() { const [data, setData] = useState([]) const [accounts, setAccounts] = useState([DEFAULT_ACCOUNT]) const [account, setAccount] = useState(null) const [loading, setLoading] = useState(true) const [addOpen, setAddOpen] = useState(false) const [form] = Form.useForm() const fetchPositions = useCallback(async () => { setLoading(true) try { const [posRes, accRes] = await Promise.all([ portfolioApi.getPositions(account), portfolioApi.getAccounts(), ]) setData(posRes.positions || []) setAccounts(accRes.accounts || [DEFAULT_ACCOUNT]) } catch { message.error('加载失败') } finally { setLoading(false) } }, [account]) useEffect(() => { fetchPositions() }, [fetchPositions]) const handleAdd = async (vals) => { try { await portfolioApi.addPosition({ ...vals, account: account || DEFAULT_ACCOUNT }) message.success('已添加') setAddOpen(false) form.resetFields() fetchPositions() } catch (e) { message.error(e.message) } } const handleDelete = async (ticker, positionId) => { try { await portfolioApi.removePosition(ticker, positionId, account) message.success('已移除') fetchPositions() } catch (e) { message.error(e.message) } } const handleExport = async () => { try { const blob = await portfolioApi.exportPositions(account) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url; a.download = 'positions.csv'; a.click() URL.revokeObjectURL(url) } catch (e) { message.error(e.message) } } const totalPnl = useMemo(() => data.reduce((s, p) => s + (p.unrealized_pnl || 0), 0), [data]) const columns = [ { title: '代码', dataIndex: 'ticker', key: 'ticker', width: 110, render: t => {t} }, { title: '账户', dataIndex: 'account', key: 'account', width: 100 }, { title: '数量', dataIndex: 'shares', key: 'shares', align: 'right', width: 80, render: v => {v} }, { title: '成本价', dataIndex: 'cost_price', key: 'cost_price', align: 'right', width: 90, render: v => {formatMoney(v)} }, { title: '现价', dataIndex: 'current_price', key: 'current_price', align: 'right', width: 90, render: v => {formatMoney(v)} }, { title: '浮亏浮盈', key: 'pnl', align: 'right', width: 110, render: (_, r) => { const pnl = r.unrealized_pnl const pct = r.unrealized_pnl_pct const color = pnl == null ? undefined : pnl >= 0 ? 'var(--color-buy)' : 'var(--color-sell)' return ( {pnl == null ? '—' : `${pnl >= 0 ? '+' : ''}${formatMoney(pnl)}`}
{pct == null ? '' : formatPct(pct)}
) }, }, { title: '买入日期', dataIndex: 'purchase_date', key: 'purchase_date', width: 100, }, { title: '操作', key: 'action', width: 80, render: (_, r) => ( handleDelete(r.ticker, r.position_id)} okText="确认" cancelText="取消"> ), }, ] return (
账户
{data.length === 0 && !loading && (
暂无持仓
点击"添加持仓"录入您的股票仓位
)} { setAddOpen(false); form.resetFields() }} footer={null}>
) } // ============== Tab 3: Recommendations ============== function RecommendationsTab() { const [data, setData] = useState([]) const [loading, setLoading] = useState(true) const [analyzing, setAnalyzing] = useState(false) const [taskId, setTaskId] = useState(null) const [wsConnected, setWsConnected] = useState(false) const [progress, setProgress] = useState(null) const [selectedDate, setSelectedDate] = useState(null) const [dates, setDates] = useState([]) const wsRef = useRef(null) const fetchRecs = useCallback(async (date) => { setLoading(true) try { const res = await portfolioApi.getRecommendations(date) setData(res.recommendations || []) if (!date) { const d = [...new Set((res.recommendations || []).map(r => r.analysis_date))].sort().reverse() setDates(d) } } catch { message.error('加载失败') } finally { setLoading(false) } }, []) useEffect(() => { fetchRecs(selectedDate) }, [fetchRecs, selectedDate]) const connectWs = useCallback((tid) => { if (wsRef.current) wsRef.current.close() const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const host = window.location.host const ws = new WebSocket(`${protocol}//${host}/ws/analysis/${tid}`) ws.onopen = () => setWsConnected(true) ws.onmessage = (e) => { const d = JSON.parse(e.data) if (d.type === 'progress') { setProgress(d) if (d.status === 'completed' || d.status === 'failed') { setAnalyzing(false) setTaskId(null) setProgress(null) fetchRecs(selectedDate) } } } ws.onclose = () => setWsConnected(false) wsRef.current = ws }, [fetchRecs, selectedDate]) const handleAnalyze = async () => { try { const res = await portfolioApi.startAnalysis() setTaskId(res.task_id) setAnalyzing(true) setProgress({ completed: 0, total: res.total, status: 'running' }) connectWs(res.task_id) message.info('开始批量分析...') } catch (e) { message.error(e.message) } } useEffect(() => { return () => { if (wsRef.current) { wsRef.current.close() wsRef.current = null } } }, []) const columns = [ { title: '代码', dataIndex: 'ticker', key: 'ticker', width: 110, render: t => {t} }, { title: '名称', dataIndex: 'name', key: 'name', render: t => {t} }, { title: '决策', dataIndex: 'decision', key: 'decision', width: 80, render: d => , }, { title: '分析日期', dataIndex: 'analysis_date', key: 'analysis_date', width: 120 }, ] return (
{/* Analysis card */}
今日建议
{analyzing && progress && ( {progress.completed || 0} / {progress.total || 0} )}
{analyzing && progress && ( )}
{/* Date filter */}
{data.length === 0 && !loading && (
暂无建议
点击上方"生成今日建议"开始批量分析
)} ) } // ============== Main ============== export default function PortfolioPanel() { const [activeTab, setActiveTab] = useState('watchlist') const items = [ { key: 'watchlist', label: '自选股', children: }, { key: 'positions', label: '持仓', children: }, { key: 'recommendations', label: '今日建议', children: }, ] return ( ) }