AI分析功能:LLM Key 改为数据库管理,支持管理员后台配置
This commit is contained in:
@@ -12,6 +12,43 @@ const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
const score = ref<Score | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
// AI 分析
|
||||
const aiLoading = ref(false)
|
||||
const aiContent = ref('')
|
||||
const aiError = ref('')
|
||||
let es: EventSource | null = null
|
||||
|
||||
function closeAI() {
|
||||
es?.close()
|
||||
es = null
|
||||
aiLoading.value = false
|
||||
}
|
||||
|
||||
async function askAI() {
|
||||
if (!score.value) return
|
||||
closeAI()
|
||||
aiLoading.value = true
|
||||
aiContent.value = ''
|
||||
aiError.value = ''
|
||||
|
||||
const url = `/api/v1/ai/analyze?ts_code=${encodeURIComponent(score.value.ts_code)}&trade_date=${encodeURIComponent(score.value.trade_date)}`
|
||||
es = new EventSource(url)
|
||||
es.addEventListener('token', (e) => {
|
||||
aiContent.value += e.data
|
||||
})
|
||||
es.addEventListener('error', (e) => {
|
||||
aiError.value = (e as any)?.data || '请求失败'
|
||||
closeAI()
|
||||
})
|
||||
es.addEventListener('done', () => {
|
||||
closeAI()
|
||||
})
|
||||
es.onerror = () => {
|
||||
if (!aiContent.value) aiError.value = '连接中断'
|
||||
closeAI()
|
||||
}
|
||||
}
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.scoreId !== null,
|
||||
set: (v) => {
|
||||
@@ -22,6 +59,9 @@ const visible = computed({
|
||||
watch(
|
||||
() => props.scoreId,
|
||||
async (id) => {
|
||||
closeAI()
|
||||
aiContent.value = ''
|
||||
aiError.value = ''
|
||||
if (id === null) {
|
||||
score.value = null
|
||||
return
|
||||
@@ -59,7 +99,7 @@ const quadrantLabel = (q: string) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-drawer v-model="visible" title="打分明细" :size="isMobile ? '92%' : '680px'" destroy-on-close>
|
||||
<el-drawer v-model="visible" title="打分明细" :size="isMobile ? '92%' : '720px'" destroy-on-close>
|
||||
<div v-loading="loading" v-if="score">
|
||||
<el-descriptions :column="isMobile ? 1 : 2" border>
|
||||
<el-descriptions-item label="品种">{{ parseTsCode(score.ts_code).symbol }}</el-descriptions-item>
|
||||
@@ -73,11 +113,21 @@ const quadrantLabel = (q: string) => {
|
||||
<el-descriptions-item label="信号">
|
||||
<el-tag>{{ score.signal }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="短期(7d × 0.4)">{{ score.short_term.toFixed(2) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="中期(15d × 0.35)">{{ score.medium_term.toFixed(2) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="长期(30d × 0.25)" :span="isMobile ? 1 : 2">
|
||||
<el-descriptions-item label="短期(7d)">{{ score.short_term.toFixed(2) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="中期(15d)">{{ score.medium_term.toFixed(2) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="长期(30d)" :span="isMobile ? 1 : 2">
|
||||
{{ score.long_term.toFixed(2) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="score.detail?.composite_delta != null" label="Δ1d">
|
||||
<span :style="{ color: score.detail.composite_delta >= 0 ? '#e4393c' : '#1ca11c' }">
|
||||
{{ score.detail.composite_delta >= 0 ? '+' : '' }}{{ score.detail.composite_delta.toFixed(1) }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="score.detail?.composite_delta_5d != null" label="Δ5d">
|
||||
<span :style="{ color: score.detail.composite_delta_5d >= 0 ? '#e4393c' : '#1ca11c' }">
|
||||
{{ score.detail.composite_delta_5d >= 0 ? '+' : '' }}{{ score.detail.composite_delta_5d.toFixed(1) }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<h4 class="section">短期 7 日逐日打分</h4>
|
||||
@@ -119,7 +169,7 @@ const quadrantLabel = (q: string) => {
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<h4 class="section">中期(15d)细节</h4>
|
||||
<h4 class="section">中期(15d)资金意愿</h4>
|
||||
<el-descriptions :column="isMobile ? 1 : 2" border v-if="score.detail?.medium_detail">
|
||||
<el-descriptions-item label="价格收益率">
|
||||
{{ score.detail.medium_detail.price_return_pct.toFixed(2) }}%
|
||||
@@ -127,19 +177,25 @@ const quadrantLabel = (q: string) => {
|
||||
<el-descriptions-item label="价格信号分">
|
||||
{{ score.detail.medium_detail.price_signal.toFixed(2) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="增仓上涨日">
|
||||
{{ score.detail.medium_detail.long_up_days }}
|
||||
<el-descriptions-item label="增仓上涨">
|
||||
{{ score.detail.medium_detail.accumulation_days }} 天
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="增仓下跌日">
|
||||
{{ score.detail.medium_detail.long_down_days }}
|
||||
<el-descriptions-item label="增仓下跌">
|
||||
{{ score.detail.medium_detail.distribution_days }} 天
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="减仓上涨">
|
||||
{{ score.detail.medium_detail.covering_days }} 天
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="减仓下跌">
|
||||
{{ score.detail.medium_detail.liquidation_days }} 天
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="资金意愿分" :span="isMobile ? 1 : 2">
|
||||
{{ score.detail.medium_detail.fund_signal.toFixed(1) }}
|
||||
<span class="formula-hint">(50 + (增仓涨 - 增仓跌)/{{ score.detail.medium_detail.window ?? 15 }} × 50)</span>
|
||||
<span class="formula-hint">(四象限加权合成)</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<h4 class="section">长期(30d)细节</h4>
|
||||
<h4 class="section">长期(30d)结构</h4>
|
||||
<el-descriptions :column="isMobile ? 1 : 2" border v-if="score.detail?.long_detail">
|
||||
<el-descriptions-item label="OI 趋势分">
|
||||
{{ score.detail.long_detail.oi_score?.toFixed(1) ?? '-' }}
|
||||
@@ -155,14 +211,14 @@ const quadrantLabel = (q: string) => {
|
||||
<el-descriptions-item label="30 日前收盘">
|
||||
{{ score.detail.long_detail.price_before_30d?.toFixed(2) ?? '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="30 日均持仓">
|
||||
{{ score.detail.long_detail.avg_oi.toFixed(0) }}
|
||||
<el-descriptions-item label="当前 OI">
|
||||
{{ score.detail.long_detail.oi_now?.toFixed(0) ?? '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="30 日前持仓">
|
||||
{{ score.detail.long_detail.oi_before.toFixed(0) }}
|
||||
<el-descriptions-item label="30 日前 OI">
|
||||
{{ score.detail.long_detail.oi_before?.toFixed(0) ?? '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="OI 变化幅度" :span="isMobile ? 1 : 2">
|
||||
{{ score.detail.long_detail.change_pct.toFixed(2) }}%
|
||||
{{ score.detail.long_detail.oi_change_pct?.toFixed(2) ?? '-' }}%
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
@@ -174,10 +230,57 @@ const quadrantLabel = (q: string) => {
|
||||
<el-descriptions-item label="ATR%">
|
||||
{{ (score.detail.volatility.atr_pct * 100).toFixed(2) }}%
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="波动率惩罚系数" :span="isMobile ? 1 : 2">
|
||||
<el-descriptions-item label="综合波动风险" v-if="score.detail.volatility.vol_risk !== undefined">
|
||||
{{ (score.detail.volatility.vol_risk * 100).toFixed(2) }}%
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="惩罚系数">
|
||||
{{ score.detail.volatility.vol_penalty.toFixed(3) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<template v-if="score.detail?.adaptive_weights">
|
||||
<h4 class="section">自适应权重</h4>
|
||||
<el-descriptions :column="isMobile ? 1 : 2" border>
|
||||
<el-descriptions-item label="趋势强度">
|
||||
{{ score.detail.adaptive_weights.trend_strength.toFixed(2) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="趋势因子">
|
||||
{{ (score.detail.adaptive_weights.trend_factor * 100).toFixed(0) }}%
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="短期权重">
|
||||
{{ (score.detail.adaptive_weights.w_short * 100).toFixed(0) }}%
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="中期权重">
|
||||
{{ (score.detail.adaptive_weights.w_medium * 100).toFixed(0) }}%
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="长期权重" :span="isMobile ? 1 : 2">
|
||||
{{ (score.detail.adaptive_weights.w_long * 100).toFixed(0) }}%
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<div class="ai-section">
|
||||
<div v-if="!aiLoading && !aiContent && !aiError">
|
||||
<el-button type="primary" :loading="aiLoading" @click="askAI">
|
||||
🤖 AI 分析当前打分
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="aiLoading || aiContent" class="ai-card">
|
||||
<div class="ai-header">
|
||||
<span>🤖 AI 分析</span>
|
||||
<el-button text size="small" @click="closeAI" v-if="aiLoading">取消</el-button>
|
||||
</div>
|
||||
<div class="ai-body">
|
||||
<div v-if="aiContent" class="ai-text" v-html="aiContent.replace(/\n/g, '<br>')"></div>
|
||||
<div v-if="aiError" class="ai-error">{{ aiError }}</div>
|
||||
<div v-if="aiLoading && !aiContent" class="ai-loading">
|
||||
<el-icon class="is-loading"><span>⏳</span></el-icon> 正在分析...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
@@ -197,4 +300,34 @@ const quadrantLabel = (q: string) => {
|
||||
font-size: 12px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.ai-section {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.ai-card {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ai-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: var(--el-fill-color-light);
|
||||
font-weight: 600;
|
||||
}
|
||||
.ai-body {
|
||||
padding: 12px;
|
||||
}
|
||||
.ai-text {
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.ai-error {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
.ai-loading {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user