新增 Web 浏览端(Go+Vue 报表系统)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fish
2026-05-03 14:34:50 +08:00
parent bf8f578761
commit 750584e619
47 changed files with 2557 additions and 18 deletions

View File

@@ -0,0 +1,97 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import * as echarts from 'echarts'
import type { Candle } from '@/api/candles'
const props = defineProps<{ data: Candle[] }>()
const containerRef = ref<HTMLDivElement | null>(null)
let chart: echarts.ECharts | null = null
function render() {
if (!chart) return
const dates = props.data.map((c) => c.trade_date)
// ECharts K 线顺序: [open, close, low, high]
const ohlc = props.data.map((c) => [c.open, c.close, c.low, c.high])
const oi = props.data.map((c) => c.oi)
chart.setOption(
{
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
legend: { data: ['K 线', '持仓量'], top: 0 },
grid: [
{ left: 60, right: 40, top: 40, height: '60%' },
{ left: 60, right: 40, top: '78%', height: '18%' },
],
xAxis: [
{ type: 'category', data: dates, scale: true, boundaryGap: false },
{ type: 'category', gridIndex: 1, data: dates, scale: true, boundaryGap: false, axisLabel: { show: false } },
],
yAxis: [
{ scale: true, splitArea: { show: true } },
{ gridIndex: 1, scale: true, splitNumber: 3 },
],
dataZoom: [
{ type: 'inside', xAxisIndex: [0, 1] },
{ type: 'slider', xAxisIndex: [0, 1], height: 18, bottom: 6 },
],
series: [
{
name: 'K 线',
type: 'candlestick',
data: ohlc,
itemStyle: {
color: '#ec3a3a',
color0: '#26a69a',
borderColor: '#ec3a3a',
borderColor0: '#26a69a',
},
},
{
name: '持仓量',
type: 'line',
xAxisIndex: 1,
yAxisIndex: 1,
data: oi,
smooth: true,
showSymbol: false,
lineStyle: { color: '#5470c6' },
areaStyle: { opacity: 0.15, color: '#5470c6' },
},
],
},
true,
)
}
function resize() {
chart?.resize()
}
onMounted(() => {
if (containerRef.value) {
chart = echarts.init(containerRef.value)
render()
window.addEventListener('resize', resize)
}
})
onBeforeUnmount(() => {
window.removeEventListener('resize', resize)
chart?.dispose()
chart = null
})
watch(() => props.data, render, { deep: true })
</script>
<template>
<div ref="containerRef" class="chart"></div>
</template>
<style scoped>
.chart {
width: 100%;
height: 560px;
}
</style>

View File

@@ -0,0 +1,105 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { getScore, type Score } from '@/api/scores'
const props = defineProps<{ scoreId: number | null }>()
const emit = defineEmits<{ (e: 'close'): void }>()
const score = ref<Score | null>(null)
const loading = ref(false)
const visible = computed({
get: () => props.scoreId !== null,
set: (v) => {
if (!v) emit('close')
},
})
watch(
() => props.scoreId,
async (id) => {
if (id === null) {
score.value = null
return
}
loading.value = true
try {
score.value = await getScore(id)
} finally {
loading.value = false
}
},
)
</script>
<template>
<el-drawer v-model="visible" title="打分明细" size="640px" destroy-on-close>
<div v-loading="loading" v-if="score">
<el-descriptions :column="2" border>
<el-descriptions-item label="合约">{{ score.ts_code }}</el-descriptions-item>
<el-descriptions-item label="日期">{{ score.trade_date }}</el-descriptions-item>
<el-descriptions-item label="收盘">{{ score.close }}</el-descriptions-item>
<el-descriptions-item label="持仓">{{ score.oi }}</el-descriptions-item>
<el-descriptions-item label="综合">
<strong>{{ score.composite.toFixed(2) }}</strong>
</el-descriptions-item>
<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="2">
{{ score.long_term.toFixed(2) }}
</el-descriptions-item>
</el-descriptions>
<h4 class="section">短期 7 日逐日打分</h4>
<el-table :data="score.detail?.short_details ?? []" size="small" border>
<el-table-column prop="trade_date" label="日期" width="100" />
<el-table-column prop="close" label="收盘" />
<el-table-column prop="pre_close" label="昨收" />
<el-table-column prop="oi" label="持仓" />
<el-table-column prop="oi_chg" label="持仓变化" />
<el-table-column prop="score" label="单日得分" />
</el-table>
<h4 class="section">中期(15d)细节</h4>
<el-descriptions :column="2" border v-if="score.detail?.medium_detail">
<el-descriptions-item label="价格收益率">
{{ (score.detail.medium_detail.price_return_pct * 100).toFixed(2) }}%
</el-descriptions-item>
<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>
<el-descriptions-item label="增仓下跌日">
{{ score.detail.medium_detail.long_down_days }}
</el-descriptions-item>
<el-descriptions-item label="资金意愿分" :span="2">
{{ score.detail.medium_detail.fund_signal }}
</el-descriptions-item>
</el-descriptions>
<h4 class="section">长期(30d)细节</h4>
<el-descriptions :column="2" border v-if="score.detail?.long_detail">
<el-descriptions-item label="30 日均持仓">
{{ score.detail.long_detail.avg_oi.toFixed(0) }}
</el-descriptions-item>
<el-descriptions-item label="30 日前持仓">
{{ score.detail.long_detail.oi_before.toFixed(0) }}
</el-descriptions-item>
<el-descriptions-item label="变化幅度" :span="2">
{{ (score.detail.long_detail.change_pct * 100).toFixed(2) }}%
</el-descriptions-item>
</el-descriptions>
</div>
</el-drawer>
</template>
<style scoped>
.section {
margin: 18px 0 8px;
}
</style>