解决了上证指数无法实时显示的问题
This commit is contained in:
@@ -84,6 +84,16 @@ def get_a_volume_7days():
|
||||
return jsonify(service.get_a_volume_7days())
|
||||
|
||||
|
||||
@fund_master_bp.route('/indices/intraday', methods=['GET'])
|
||||
def get_indices_intraday():
|
||||
"""
|
||||
获取多指数分时数据(上证、深证、沪深300)
|
||||
GET /api/market/indices/intraday
|
||||
"""
|
||||
service = get_fund_master_service()
|
||||
return jsonify(service.get_indices_intraday())
|
||||
|
||||
|
||||
@fund_master_bp.route('/sse', methods=['GET'])
|
||||
def get_sse_30min():
|
||||
"""
|
||||
|
||||
@@ -552,75 +552,108 @@ class FundMasterService:
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e), "data": []}
|
||||
|
||||
# ==================== 近30分钟上证指数 ====================
|
||||
def get_sse_30min(self) -> dict:
|
||||
# ==================== 市场指数分时数据 ====================
|
||||
def _get_eastmoney_intraday(self, secid: str, name: str) -> list:
|
||||
"""
|
||||
获取近30分钟上证指数分时数据
|
||||
数据源:百度股市通
|
||||
|
||||
Returns:
|
||||
dict: {'success': bool, 'data': list, 'update_time': str}
|
||||
获取东方财富分时数据(内部通用方法)
|
||||
"""
|
||||
cache_key = 'sse_30min'
|
||||
cached = self._get_cache(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
try:
|
||||
url = "https://finance.pae.baidu.com/vapi/v1/getquotation"
|
||||
url = "http://push2.eastmoney.com/api/qt/stock/trends2/get"
|
||||
params = {
|
||||
"srcid": "5353",
|
||||
"all": "1",
|
||||
"pointType": "string",
|
||||
"group": "quotation_index_minute",
|
||||
"query": "000001",
|
||||
"code": "000001",
|
||||
"market_type": "ab",
|
||||
"newFormat": "1",
|
||||
"name": "上证指数",
|
||||
"finClientType": "pc"
|
||||
"fields1": "f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12,f13",
|
||||
"fields2": "f51,f52,f53,f54,f55,f56,f57,f58",
|
||||
"secid": secid,
|
||||
"ndays": "1",
|
||||
"iscr": "0",
|
||||
"iscca": "0"
|
||||
}
|
||||
|
||||
response = self.baidu_session.get(url, params=params, timeout=10, verify=False)
|
||||
response = requests.get(url, params=params, timeout=10)
|
||||
data = response.json()
|
||||
|
||||
if str(response.json().get("ResultCode")) == "0":
|
||||
market_data = response.json()["Result"]["newMarketData"]["marketData"][0]["p"]
|
||||
points = market_data.split(";")[-30:] # 最近30分钟
|
||||
if data and data.get("data") and data["data"].get("trends"):
|
||||
trends = data["data"]["trends"]
|
||||
pre_close = data["data"].get("prePrice", 0)
|
||||
|
||||
result = []
|
||||
for point in points:
|
||||
for point in trends:
|
||||
# 格式: time, open, close, high, low, volume, amount, avg
|
||||
parts = point.split(",")
|
||||
if len(parts) >= 6:
|
||||
# 成交量、成交额单位转换
|
||||
volume = parts[4]
|
||||
amount = parts[5]
|
||||
if len(parts) >= 3:
|
||||
time_str = parts[0].split(" ")[1] # 取 HH:MM
|
||||
price = float(parts[2])
|
||||
|
||||
# 计算涨跌
|
||||
change = 0
|
||||
change_pct = "0.00%"
|
||||
if pre_close:
|
||||
change = round(price - pre_close, 2)
|
||||
pct = (change / pre_close) * 100
|
||||
change_pct = f"{round(pct, 2)}%"
|
||||
|
||||
# 成交量处理
|
||||
volume = parts[5]
|
||||
try:
|
||||
volume = f"{round(float(volume) / 10000, 2)}万手"
|
||||
amount = f"{round(float(amount) / 10000 / 10000, 2)}亿"
|
||||
vol_num = float(volume)
|
||||
if vol_num > 10000:
|
||||
volume = f"{round(vol_num / 10000, 2)}万"
|
||||
except:
|
||||
pass
|
||||
|
||||
result.append({
|
||||
"time": parts[0],
|
||||
"price": parts[1],
|
||||
"change": parts[2],
|
||||
"change_pct": f"{parts[3]}%",
|
||||
"volume": volume,
|
||||
"amount": amount
|
||||
"time": time_str,
|
||||
"price": str(price),
|
||||
"change": f"{'+' if change > 0 else ''}{change}",
|
||||
"change_pct": change_pct,
|
||||
"volume": volume
|
||||
})
|
||||
|
||||
data = {
|
||||
"success": True,
|
||||
"data": result,
|
||||
"update_time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
self._set_cache(cache_key, data, 'sse_30min')
|
||||
return data
|
||||
|
||||
return {"success": False, "error": "获取上证指数数据失败", "data": []}
|
||||
|
||||
return result
|
||||
return []
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e), "data": []}
|
||||
print(f"Error fetching intraday for {secid}: {e}")
|
||||
return []
|
||||
|
||||
def get_indices_intraday(self) -> dict:
|
||||
"""
|
||||
获取多指数分时数据(上证、深证、沪深300)
|
||||
|
||||
Returns:
|
||||
dict: {'sh': [], 'sz': [], 'hs300': [], 'update_time': str}
|
||||
"""
|
||||
cache_key = 'indices_intraday'
|
||||
cached = self._get_cache(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
sh_data = self._get_eastmoney_intraday("1.000001", "上证指数")
|
||||
sz_data = self._get_eastmoney_intraday("0.399001", "深证成指")
|
||||
hs300_data = self._get_eastmoney_intraday("1.000300", "沪深300")
|
||||
|
||||
data = {
|
||||
"success": True,
|
||||
"data": {
|
||||
"sh": sh_data,
|
||||
"sz": sz_data,
|
||||
"hs300": hs300_data
|
||||
},
|
||||
"update_time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
self._set_cache(cache_key, data, 'sse_30min') # 复用 sse_30min 的 TTL (1分钟)
|
||||
return data
|
||||
|
||||
def get_sse_30min(self) -> dict:
|
||||
"""
|
||||
获取上证指数分时数据(兼容旧接口,但提供全天数据)
|
||||
"""
|
||||
# 直接复用新的分时数据获取逻辑,但只返回上证数据
|
||||
full_data = self.get_indices_intraday()
|
||||
if full_data["success"]:
|
||||
return {
|
||||
"success": True,
|
||||
"data": full_data["data"]["sh"],
|
||||
"update_time": full_data["update_time"]
|
||||
}
|
||||
return {"success": False, "error": "获取上证指数数据失败", "data": []}
|
||||
|
||||
# ==================== 汇总数据接口 ====================
|
||||
def get_market_overview(self) -> dict:
|
||||
|
||||
@@ -247,8 +247,8 @@ export default {
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #667eea;
|
||||
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--primary-color: #7B8D9E;
|
||||
--primary-gradient: linear-gradient(135deg, #9CADBD 0%, #7B8D9E 100%);
|
||||
--success-color: #10b981;
|
||||
--danger-color: #ef4444;
|
||||
--warning-color: #f59e0b;
|
||||
|
||||
@@ -324,7 +324,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
.analyze-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #9CADBD 0%, #7B8D9E 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
|
||||
@@ -105,17 +105,17 @@ export default {
|
||||
},
|
||||
splitArea: {
|
||||
areaStyle: {
|
||||
color: ['rgba(102, 126, 234, 0.05)', 'rgba(102, 126, 234, 0.1)']
|
||||
color: ['rgba(123, 141, 158, 0.05)', 'rgba(123, 141, 158, 0.1)']
|
||||
}
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(102, 126, 234, 0.3)'
|
||||
color: 'rgba(123, 141, 158, 0.3)'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(102, 126, 234, 0.3)'
|
||||
color: 'rgba(123, 141, 158, 0.3)'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -125,14 +125,14 @@ export default {
|
||||
value: data,
|
||||
name: '能力评分',
|
||||
areaStyle: {
|
||||
color: 'rgba(102, 126, 234, 0.3)'
|
||||
color: 'rgba(123, 141, 158, 0.3)'
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#667eea',
|
||||
color: '#7B8D9E',
|
||||
width: 2
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#667eea'
|
||||
color: '#7B8D9E'
|
||||
}
|
||||
}]
|
||||
}]
|
||||
@@ -174,7 +174,7 @@ export default {
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #9CADBD 0%, #7B8D9E 100%);
|
||||
padding: 10px 14px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
|
||||
@@ -189,7 +189,7 @@ export default {
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #9CADBD 0%, #7B8D9E 100%);
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -288,7 +288,7 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.fund-basic-info {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #9CADBD 0%, #7B8D9E 100%);
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
|
||||
@@ -189,7 +189,7 @@ export default {
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #9CADBD 0%, #7B8D9E 100%);
|
||||
padding: 12px 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ export default {
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #9CADBD 0%, #7B8D9E 100%);
|
||||
padding: 12px 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export default {
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #9CADBD 0%, #7B8D9E 100%);
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -281,7 +281,7 @@ export default {
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #9CADBD 0%, #7B8D9E 100%);
|
||||
color: white;
|
||||
padding: 10px 16px;
|
||||
flex-shrink: 0;
|
||||
@@ -320,7 +320,7 @@ export default {
|
||||
|
||||
.range-btn.active {
|
||||
background: white;
|
||||
color: #667eea;
|
||||
color: #7B8D9E;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
@@ -145,12 +145,12 @@ export default {
|
||||
}
|
||||
|
||||
.period-tab:hover {
|
||||
border-color: #667eea;
|
||||
color: #667eea;
|
||||
border-color: #7B8D9E;
|
||||
color: #7B8D9E;
|
||||
}
|
||||
|
||||
.period-tab.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #9CADBD 0%, #7B8D9E 100%);
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ export default {
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #9CADBD 0%, #7B8D9E 100%);
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -206,7 +206,7 @@ export default {
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #9CADBD 0%, #7B8D9E 100%);
|
||||
padding: 10px 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<template>
|
||||
<div class="market-overview-container">
|
||||
<!-- 1. 近30分钟上证指数 (置顶 & 折线图) -->
|
||||
<!-- 1. 市场指数实时走势 (置顶 & 折线图) -->
|
||||
<div class="market-section" v-if="showSSE30Min">
|
||||
<div class="section-header">
|
||||
<h3>📉 上证指数实时走势 (近30分)</h3>
|
||||
<h3>📉 市场指数实时走势</h3>
|
||||
<div class="tab-group">
|
||||
<span
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="update-tag" v-if="updateTime">{{ updateTime.split(' ')[1] }} 更新</span>
|
||||
</div>
|
||||
<div class="chart-container sse-chart-container">
|
||||
<v-chart class="chart" :option="sseOption" autoresize v-if="sse30Min.length" />
|
||||
<div v-else class="empty-state">A股未开盘</div>
|
||||
<v-chart class="chart" :option="currentChartOption" autoresize v-if="hasCurrentData" />
|
||||
<div v-else class="empty-state">暂无数据 ({{ activeTabName }})</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -154,11 +164,22 @@ export default {
|
||||
const goldRealtime = ref([])
|
||||
const goldHistory = ref([])
|
||||
const aVolume = ref([])
|
||||
const sse30Min = ref([])
|
||||
const updateTime = ref('')
|
||||
const goldHistoryExpanded = ref(true)
|
||||
let refreshTimer = null
|
||||
|
||||
// 指数分时数据
|
||||
const indicesIntraday = ref({ sh: [], sz: [], hs300: [] })
|
||||
const activeTab = ref('sh')
|
||||
const tabs = [
|
||||
{ key: 'sh', name: '上证指数' },
|
||||
{ key: 'sz', name: '深证成指' },
|
||||
{ key: 'hs300', name: '沪深300' }
|
||||
]
|
||||
|
||||
const activeTabName = computed(() => tabs.find(t => t.key === activeTab.value)?.name || '')
|
||||
const hasCurrentData = computed(() => indicesIntraday.value[activeTab.value]?.length > 0)
|
||||
|
||||
// 指数分组
|
||||
const indices = computed(() => {
|
||||
const all = marketIndex.value
|
||||
@@ -169,12 +190,13 @@ export default {
|
||||
}
|
||||
})
|
||||
|
||||
// 上证指数图表配置
|
||||
const sseOption = computed(() => {
|
||||
if (!sse30Min.value.length) return {}
|
||||
// 当前选中的指数图表配置
|
||||
const currentChartOption = computed(() => {
|
||||
const data = indicesIntraday.value[activeTab.value]
|
||||
if (!data || !data.length) return {}
|
||||
|
||||
const times = sse30Min.value.map(i => i.time.split(' ')[1] || i.time)
|
||||
const prices = sse30Min.value.map(i => parseFloat(i.price))
|
||||
const times = data.map(i => i.time)
|
||||
const prices = data.map(i => parseFloat(i.price))
|
||||
// 计算涨跌色:基于第一笔数据
|
||||
const basePrice = prices[0]
|
||||
const isUp = prices[prices.length - 1] >= basePrice
|
||||
@@ -187,7 +209,7 @@ export default {
|
||||
formatter: (params) => {
|
||||
const p = params[0]
|
||||
if (!p) return ''
|
||||
const item = sse30Min.value[p.dataIndex]
|
||||
const item = data[p.dataIndex]
|
||||
return `
|
||||
<div>${item.time}</div>
|
||||
<div style="font-weight:bold;color:${color}">${item.price}</div>
|
||||
@@ -297,9 +319,15 @@ export default {
|
||||
if (data.market_index?.success) marketIndex.value = data.market_index.data
|
||||
if (data.gold_realtime?.success) goldRealtime.value = data.gold_realtime.data
|
||||
if (data.a_volume_7days?.success) aVolume.value = data.a_volume_7days.data.reverse() // 按时间正序
|
||||
if (data.sse_30min?.success) sse30Min.value = data.sse_30min.data
|
||||
updateTime.value = data.update_time
|
||||
}
|
||||
|
||||
// 获取多指数分时数据
|
||||
const intradayRes = await marketAPI.getIndicesIntraday()
|
||||
if (intradayRes.data.success) {
|
||||
indicesIntraday.value = intradayRes.data.data
|
||||
}
|
||||
|
||||
if (props.showGoldHistory) {
|
||||
const historyRes = await marketAPI.getGoldHistory(10)
|
||||
if (historyRes.data.success) goldHistory.value = historyRes.data.data
|
||||
@@ -344,9 +372,11 @@ export default {
|
||||
loading, fetchAll,
|
||||
marketIndex, indices,
|
||||
goldRealtime, goldHistory, goldHistoryExpanded,
|
||||
aVolume, updateTime, sse30Min,
|
||||
aVolume, updateTime,
|
||||
formatDate, getChangeClass, getUpDnClass,
|
||||
sseOption, volumeOption
|
||||
volumeOption,
|
||||
// New returns
|
||||
tabs, activeTab, activeTabName, hasCurrentData, currentChartOption
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,6 +411,34 @@ export default {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tab-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tab-group span {
|
||||
font-size: 0.85em;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab-group span:hover {
|
||||
color: #1890ff;
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
.tab-group span.active {
|
||||
color: #1890ff;
|
||||
font-weight: bold;
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
/* 上证指数 */
|
||||
.sse-chart-container {
|
||||
height: 200px;
|
||||
|
||||
@@ -239,6 +239,11 @@ export const marketAPI = {
|
||||
// 获取近30分钟上证指数
|
||||
getSSE30min() {
|
||||
return api.get('/market/sse')
|
||||
},
|
||||
|
||||
// 获取多指数分时数据
|
||||
getIndicesIntraday() {
|
||||
return api.get('/market/indices/intraday')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user