From 346b86340c3bbf0b2fccc6a253a9356edfcfa68c Mon Sep 17 00:00:00 2001 From: Raymond Hu Date: Mon, 17 Aug 2026 21:18:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20=E6=9F=A5=E7=9C=8B=E8=BF=91?= =?UTF-8?q?=E6=B3=81=20button=20with=20AI=20analysis=20modal=20in=20Recomm?= =?UTF-8?q?endStockList?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add StockAnalysisAPI.js utility for GET /api/v0/stock_analysis/ - Add 查看近況 button to each row in the recommend stock table - Show analysis results in a modal: signal badge, summary, positives, risks - Signal badge color-coded (Strong Buy → green, Sell → orange, etc.) - Display data freshness, model name, and analysis timestamp in footer - Add loading spinner during API call, error alert on failure Co-Authored-By: Claude Sonnet 4.6 --- src/assets/css/recommend-stock.css | 41 +++++++ src/pages/RecommendStockList.js | 175 ++++++++++++++++++++++------- src/utils/StockAnalysisAPI.js | 6 + 3 files changed, 179 insertions(+), 43 deletions(-) create mode 100644 src/utils/StockAnalysisAPI.js diff --git a/src/assets/css/recommend-stock.css b/src/assets/css/recommend-stock.css index 9abe98a..95f95ee 100644 --- a/src/assets/css/recommend-stock.css +++ b/src/assets/css/recommend-stock.css @@ -74,6 +74,47 @@ color: #6c757d; } +/* Stock analysis modal */ +.analysis-signal-badge { + display: inline-block; + padding: 0.35rem 1.1rem; + border-radius: 1rem; + font-weight: 600; + font-size: 1rem; +} + +.signal-strong-buy { background-color: #198754; color: white; } +.signal-buy { background-color: #20c997; color: white; } +.signal-neutral { background-color: #6c757d; color: white; } +.signal-sell { background-color: #fd7e14; color: white; } +.signal-strong-sell{ background-color: #dc3545; color: white; } + +.analysis-section-title { + font-weight: 600; + margin-bottom: 0.4rem; +} + +.analysis-summary { + line-height: 1.7; + color: #333; +} + +.analysis-list { + padding-left: 1.2rem; + margin-bottom: 0; +} + +.analysis-list li { + margin-bottom: 0.3rem; + line-height: 1.6; +} + +.analysis-footer { + border-top: 1px solid #dee2e6; + padding-top: 0.75rem; + margin-top: 0.5rem; +} + @media screen and (max-width: 576px) { .recommend-stock-filters { flex-direction: column; diff --git a/src/pages/RecommendStockList.js b/src/pages/RecommendStockList.js index 21ffcfc..e0da9ca 100644 --- a/src/pages/RecommendStockList.js +++ b/src/pages/RecommendStockList.js @@ -1,9 +1,23 @@ import React, { useState, useEffect } from 'react' -import { Container, Table } from 'react-bootstrap' +import { Container, Table, Button, Modal, Spinner, Alert } from 'react-bootstrap' import dayjs from 'dayjs' import * as recommendStockApi from 'utils/RecommendStockAPI' +import * as stockAnalysisApi from 'utils/StockAnalysisAPI' import 'assets/css/recommend-stock.css' +const SIGNAL_CLASS = { + 'Strong Buy': 'strong-buy', + 強力買進: 'strong-buy', + Buy: 'buy', + 買進: 'buy', + Sell: 'sell', + 賣出: 'sell', + 'Strong Sell': 'strong-sell', + 強力賣出: 'strong-sell' +} + +const getSignalClass = (signal) => SIGNAL_CLASS[signal] || 'neutral' + const RecommendStockList = () => { const [recommendStocks, setRecommendStocks] = useState([]) const [filterModels, setFilterModels] = useState([]) @@ -12,11 +26,14 @@ const RecommendStockList = () => { const [statistics, setStatistics] = useState(null) const [loading, setLoading] = useState(false) + const [analysisModal, setAnalysisModal] = useState({ show: false, stockId: null, stockName: null }) + const [analysisData, setAnalysisData] = useState(null) + const [analysisLoading, setAnalysisLoading] = useState(false) + const [analysisError, setAnalysisError] = useState(null) + useEffect(() => { recommendStockApi.getFilterModels() - .then(data => { - setFilterModels(data.filter_models || []) - }) + .then(data => setFilterModels(data.filter_models || [])) .catch(err => console.error('Failed to fetch filter models:', err)) }, []) @@ -28,42 +45,39 @@ const RecommendStockList = () => { const fetchRecommendStocks = () => { setLoading(true) - const params = { - date: selectedDate - } - if (selectedFilterModel) { - params.filterModel = selectedFilterModel - } + const params = { date: selectedDate } + if (selectedFilterModel) params.filterModel = selectedFilterModel recommendStockApi.getRecommendStockList(params) - .then(data => { - setRecommendStocks(data) - setLoading(false) - }) - .catch(err => { - console.error('Failed to fetch recommend stocks:', err) - setRecommendStocks([]) - setLoading(false) - }) + .then(data => { setRecommendStocks(data); setLoading(false) }) + .catch(err => { console.error('Failed to fetch recommend stocks:', err); setRecommendStocks([]); setLoading(false) }) } const fetchStatistics = () => { recommendStockApi.getStatistics(selectedDate) - .then(data => { - setStatistics(data) - }) - .catch(err => { - console.error('Failed to fetch statistics:', err) - setStatistics(null) - }) + .then(data => setStatistics(data)) + .catch(err => { console.error('Failed to fetch statistics:', err); setStatistics(null) }) } - const handleDateChange = (e) => { - setSelectedDate(e.target.value) + const handleViewAnalysis = (stock) => { + setAnalysisModal({ show: true, stockId: stock.stock_id, stockName: stock.stock_name }) + setAnalysisData(null) + setAnalysisError(null) + setAnalysisLoading(true) + + stockAnalysisApi.getStockAnalysis(stock.stock_id) + .then(data => { setAnalysisData(data); setAnalysisLoading(false) }) + .catch(err => { + console.error('Failed to fetch stock analysis:', err) + setAnalysisError('無法取得近況資料,請稍後再試') + setAnalysisLoading(false) + }) } - const handleFilterModelChange = (e) => { - setSelectedFilterModel(e.target.value) + const closeAnalysisModal = () => { + setAnalysisModal({ show: false, stockId: null, stockName: null }) + setAnalysisData(null) + setAnalysisError(null) } return ( @@ -77,16 +91,15 @@ const RecommendStockList = () => { type="date" id="date-select" value={selectedDate} - onChange={handleDateChange} + onChange={(e) => setSelectedDate(e.target.value)} /> -