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 (
)
}
// ============== 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 (
)
}
// ============== 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}
)}
}
onClick={handleAnalyze}
loading={analyzing}
disabled={analyzing}
>
{analyzing ? '分析中...' : '生成今日建议'}
{analyzing && progress && (
)}
{/* Date filter */}
{/* Recommendations list */}
{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 (
)
}