打分列表改为品种打分,移除同步数据功能

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fish
2026-05-10 15:19:00 +08:00
parent 465feaa833
commit e6351750cf
2 changed files with 279 additions and 151 deletions

View File

@@ -6,7 +6,6 @@ import { useAuthStore } from '@/stores/auth'
import { useThemeStore } from '@/stores/theme' import { useThemeStore } from '@/stores/theme'
import { useMobile } from '@/composables/useMobile' import { useMobile } from '@/composables/useMobile'
import { resetAllData } from '@/api/admin' import { resetAllData } from '@/api/admin'
import { runBatch } from '@/api/run'
const auth = useAuthStore() const auth = useAuthStore()
const theme = useThemeStore() const theme = useThemeStore()
@@ -16,7 +15,6 @@ const { isMobile } = useMobile()
const drawerOpen = ref(false) const drawerOpen = ref(false)
const resetting = ref(false) const resetting = ref(false)
const syncing = ref(false)
const showLayout = computed(() => route.meta.layout !== 'blank' && !!auth.token) const showLayout = computed(() => route.meta.layout !== 'blank' && !!auth.token)
@@ -60,22 +58,6 @@ async function handleReset() {
} }
} }
async function handleSync() {
syncing.value = true
try {
await runBatch()
ElMessage.success('同步完成')
if (route.path === '/scores') {
router.go(0)
} else {
router.push('/scores')
}
} catch {
ElMessage.error('同步失败')
} finally {
syncing.value = false
}
}
</script> </script>
<template> <template>
@@ -90,12 +72,9 @@ async function handleSync() {
:text-color="menuColors.text" :text-color="menuColors.text"
:active-text-color="menuColors.active" :active-text-color="menuColors.active"
> >
<el-menu-item index="/scores">打分列表</el-menu-item> <el-menu-item index="/scores">品种打分</el-menu-item>
<el-menu-item index="/chart">K 线 / 持仓</el-menu-item> <el-menu-item index="/chart">K 线 / 持仓</el-menu-item>
<el-menu-item index="/contract-full">合约全景</el-menu-item> <el-menu-item index="/contract-full">合约全景</el-menu-item>
<el-menu-item v-if="auth.isAdmin" :index="() => {}" @click="handleSync" :disabled="syncing">
同步数据
</el-menu-item>
<el-menu-item index="/run">手动打分</el-menu-item> <el-menu-item index="/run">手动打分</el-menu-item>
<el-menu-item v-if="auth.isAdmin" index="/admin/users">用户管理</el-menu-item> <el-menu-item v-if="auth.isAdmin" index="/admin/users">用户管理</el-menu-item>
<el-menu-item v-if="auth.isAdmin" :index="() => {}" @click="handleReset" :disabled="resetting"> <el-menu-item v-if="auth.isAdmin" :index="() => {}" @click="handleReset" :disabled="resetting">
@@ -121,12 +100,9 @@ async function handleSync() {
:active-text-color="menuColors.active" :active-text-color="menuColors.active"
@select="closeDrawer" @select="closeDrawer"
> >
<el-menu-item index="/scores">打分列表</el-menu-item> <el-menu-item index="/scores">品种打分</el-menu-item>
<el-menu-item index="/chart">K 线 / 持仓</el-menu-item> <el-menu-item index="/chart">K 线 / 持仓</el-menu-item>
<el-menu-item index="/contract-full">合约全景</el-menu-item> <el-menu-item index="/contract-full">合约全景</el-menu-item>
<el-menu-item v-if="auth.isAdmin" :index="() => {}" @click="handleSync" :disabled="syncing">
同步数据
</el-menu-item>
<el-menu-item index="/run">手动打分</el-menu-item> <el-menu-item index="/run">手动打分</el-menu-item>
<el-menu-item v-if="auth.isAdmin" index="/admin/users">用户管理</el-menu-item> <el-menu-item v-if="auth.isAdmin" index="/admin/users">用户管理</el-menu-item>
</el-menu> </el-menu>

View File

@@ -1,50 +1,63 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, reactive, ref } from 'vue' import { nextTick, onMounted, reactive, ref, watch, computed } from 'vue'
import { listContracts, listScores, type Score } from '@/api/scores' import { ElMessage } from 'element-plus'
import { listScores, listContracts, type Score } from '@/api/scores'
import { runPipeline, type RunResponse } from '@/api/run'
import ScoreDetailDrawer from '@/components/ScoreDetailDrawer.vue' import ScoreDetailDrawer from '@/components/ScoreDetailDrawer.vue'
import { parseTsCode } from '@/utils/contract' import { parseTsCode } from '@/utils/contract'
import { useMobile } from '@/composables/useMobile' import { useMobile } from '@/composables/useMobile'
const { isMobile } = useMobile() const { isMobile } = useMobile()
const filter = reactive<{ const EXCHANGES = [
ts_code?: string { code: 'ZCE', name: '郑商所' },
range: [string, string] | [] { code: 'SHF', name: '上期所' },
signal?: string { code: 'DCE', name: '大商所' },
limit: number ]
}>({
ts_code: undefined, const SYMBOLS_BY_EXCHANGE: Record<string, string[]> = {
range: [], ZCE: ['FG', 'SA', 'MA', 'CF'],
signal: undefined, SHF: ['RB'],
limit: 200, DCE: ['M'],
}
// 品种打分
const selectedExchange = ref('')
const selectedSymbol = ref('')
const scoring = ref(false)
const scoreResult = ref<RunResponse | null>(null)
const resultRef = ref<HTMLElement | null>(null)
const availableSymbols = computed(() => {
if (!selectedExchange.value) return []
return SYMBOLS_BY_EXCHANGE[selectedExchange.value] || []
}) })
const contracts = ref<string[]>([]) watch(selectedExchange, () => {
const rows = ref<Score[]>([]) selectedSymbol.value = ''
const loading = ref(false) })
const drawerScoreId = ref<number | null>(null)
async function reload(silent = false) { async function handleScore() {
if (!silent) loading.value = true if (!selectedSymbol.value) {
ElMessage.warning('请选择品种')
return
}
scoring.value = true
scoreResult.value = null
try { try {
const [start, end] = filter.range || [] const resp = await runPipeline({ symbol: selectedSymbol.value })
rows.value = await listScores({ scoreResult.value = resp
ts_code: filter.ts_code, ElMessage.success('打分完成')
start: start || undefined, await nextTick()
end: end || undefined, resultRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' })
signal: filter.signal, } catch (err: any) {
limit: filter.limit, const msg = err?.response?.data?.error || err.message || '请求失败'
}) ElMessage.error(msg)
} finally { } finally {
if (!silent) loading.value = false scoring.value = false
} }
} }
function toggleSignal(s: string) {
filter.signal = filter.signal === s ? undefined : s
reload(true)
}
function signalTagType(s: string) { function signalTagType(s: string) {
if (s.includes('强烈看多')) return 'success' if (s.includes('强烈看多')) return 'success'
if (s.includes('偏多')) return '' if (s.includes('偏多')) return ''
@@ -61,19 +74,146 @@ function signalIcon(s: string) {
return '' return ''
} }
// 历史查询(折叠)
const showHistory = ref(false)
const historyFilter = reactive<{
ts_code?: string
range: [string, string] | []
signal?: string
limit: number
}>({
ts_code: undefined,
range: [],
signal: undefined,
limit: 50,
})
const contracts = ref<string[]>([])
const historyRows = ref<Score[]>([])
const historyLoading = ref(false)
const drawerScoreId = ref<number | null>(null)
async function reloadHistory(silent = false) {
if (!silent) historyLoading.value = true
try {
const [start, end] = historyFilter.range || []
historyRows.value = await listScores({
ts_code: historyFilter.ts_code,
start: start || undefined,
end: end || undefined,
signal: historyFilter.signal,
limit: historyFilter.limit,
})
} finally {
if (!silent) historyLoading.value = false
}
}
function toggleSignal(s: string) {
historyFilter.signal = historyFilter.signal === s ? undefined : s
reloadHistory(true)
}
onMounted(async () => { onMounted(async () => {
contracts.value = await listContracts().catch(() => []) contracts.value = await listContracts().catch(() => [])
await reload()
}) })
</script> </script>
<template> <template>
<div class="page"> <div class="page">
<el-card shadow="never" class="filter-card"> <!-- 品种打分 -->
<el-card shadow="never">
<template #header>
<span>品种打分</span>
</template>
<el-form :inline="!isMobile">
<el-form-item label="交易所">
<el-select
v-model="selectedExchange"
placeholder="选择交易所"
clearable
:style="{ width: isMobile ? '100%' : '160px' }"
>
<el-option
v-for="ex in EXCHANGES"
:key="ex.code"
:label="ex.name"
:value="ex.code"
/>
</el-select>
</el-form-item>
<el-form-item label="品种">
<el-select
v-model="selectedSymbol"
placeholder="选择品种"
:disabled="!selectedExchange"
:style="{ width: isMobile ? '100%' : '120px' }"
>
<el-option
v-for="s in availableSymbols"
:key="s"
:label="s"
:value="s"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="scoring"
:disabled="!selectedSymbol"
@click="handleScore"
>
打分
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 打分结果 -->
<div v-if="scoreResult" ref="resultRef">
<el-card shadow="never" class="result-card">
<template #header>
<span>打分结果</span>
</template>
<el-descriptions :column="isMobile ? 1 : 2" border>
<el-descriptions-item label="品种">
{{ parseTsCode(scoreResult.ts_code).symbol }}
</el-descriptions-item>
<el-descriptions-item label="合约">
{{ parseTsCode(scoreResult.ts_code).contract }}
</el-descriptions-item>
<el-descriptions-item label="日期">{{ scoreResult.trade_date }}</el-descriptions-item>
<el-descriptions-item label="收盘">{{ scoreResult.close }}</el-descriptions-item>
<el-descriptions-item label="持仓">{{ scoreResult.oi }}</el-descriptions-item>
<el-descriptions-item label="短期(7d)">{{ scoreResult.short_term }}</el-descriptions-item>
<el-descriptions-item label="中期(15d)">{{ scoreResult.medium_term }}</el-descriptions-item>
<el-descriptions-item label="长期(30d)">{{ scoreResult.long_term }}</el-descriptions-item>
<el-descriptions-item label="综合">
<strong>{{ scoreResult.composite }}</strong>
</el-descriptions-item>
<el-descriptions-item label="信号" :span="isMobile ? 1 : 2">
<el-tag :type="signalTagType(scoreResult.signal)">
{{ signalIcon(scoreResult.signal) }} {{ scoreResult.signal }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-card>
</div>
<!-- 历史查询折叠 -->
<el-card shadow="never">
<template #header>
<div class="history-header" @click="showHistory = !showHistory">
<span>历史打分查询</span>
<span>{{ showHistory ? '▲' : '▼' }}</span>
</div>
</template>
<div v-if="showHistory">
<el-form :inline="!isMobile"> <el-form :inline="!isMobile">
<el-form-item label="合约"> <el-form-item label="合约">
<el-select <el-select
v-model="filter.ts_code" v-model="historyFilter.ts_code"
placeholder="全部合约" placeholder="全部合约"
clearable clearable
filterable filterable
@@ -84,7 +224,7 @@ onMounted(async () => {
</el-form-item> </el-form-item>
<el-form-item label="日期"> <el-form-item label="日期">
<el-date-picker <el-date-picker
v-model="filter.range" v-model="historyFilter.range"
type="daterange" type="daterange"
value-format="YYYYMMDD" value-format="YYYYMMDD"
range-separator="" range-separator=""
@@ -94,33 +234,35 @@ onMounted(async () => {
/> />
</el-form-item> </el-form-item>
<el-form-item label="条数"> <el-form-item label="条数">
<el-input-number v-model="filter.limit" :min="10" :max="500" :step="50" /> <el-input-number v-model="historyFilter.limit" :min="10" :max="500" :step="50" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :loading="loading" style="width: 88px" @click="reload">查询</el-button> <el-button type="primary" :loading="historyLoading" style="width: 88px" @click="reloadHistory">
查询
</el-button>
</el-form-item> </el-form-item>
<el-form-item label="快捷" class="signal-item"> <el-form-item label="快捷" class="signal-item">
<el-button-group class="signal-group" :size="isMobile ? 'small' : 'default'"> <el-button-group class="signal-group" :size="isMobile ? 'small' : 'default'">
<el-button <el-button
:type="filter.signal === '强烈看多' ? 'success' : ''" :type="historyFilter.signal === '强烈看多' ? 'success' : ''"
@click="toggleSignal('强烈看多')" @click="toggleSignal('强烈看多')"
> >
强烈看多 强烈看多
</el-button> </el-button>
<el-button <el-button
:type="filter.signal === '偏多' ? 'primary' : ''" :type="historyFilter.signal === '偏多' ? 'primary' : ''"
@click="toggleSignal('偏多')" @click="toggleSignal('偏多')"
> >
偏多 偏多
</el-button> </el-button>
<el-button <el-button
:type="filter.signal === '偏空' ? 'warning' : ''" :type="historyFilter.signal === '偏空' ? 'warning' : ''"
@click="toggleSignal('偏空')" @click="toggleSignal('偏空')"
> >
偏空 偏空
</el-button> </el-button>
<el-button <el-button
:type="filter.signal === '强烈看空' ? 'danger' : ''" :type="historyFilter.signal === '强烈看空' ? 'danger' : ''"
@click="toggleSignal('强烈看空')" @click="toggleSignal('强烈看空')"
> >
强烈看空 强烈看空
@@ -128,10 +270,9 @@ onMounted(async () => {
</el-button-group> </el-button-group>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card>
<div class="table-wrapper" v-loading="loading"> <div class="table-wrapper" v-loading="historyLoading">
<el-table :data="rows" stripe class="score-table"> <el-table :data="historyRows" stripe class="score-table">
<el-table-column prop="trade_date" label="日期" width="100" /> <el-table-column prop="trade_date" label="日期" width="100" />
<el-table-column label="品种" width="80"> <el-table-column label="品种" width="80">
<template #default="{ row }"> <template #default="{ row }">
@@ -156,7 +297,9 @@ onMounted(async () => {
</el-table-column> </el-table-column>
<el-table-column prop="signal" label="信号" min-width="160"> <el-table-column prop="signal" label="信号" min-width="160">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="signalTagType(row.signal)">{{ signalIcon(row.signal) }} {{ row.signal }}</el-tag> <el-tag :type="signalTagType(row.signal)">
{{ signalIcon(row.signal) }} {{ row.signal }}
</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="80" fixed="right"> <el-table-column label="操作" width="80" fixed="right">
@@ -166,11 +309,10 @@ onMounted(async () => {
</el-table-column> </el-table-column>
</el-table> </el-table>
</div> </div>
</div>
</el-card>
<ScoreDetailDrawer <ScoreDetailDrawer :score-id="drawerScoreId" @close="drawerScoreId = null" />
:score-id="drawerScoreId"
@close="drawerScoreId = null"
/>
</div> </div>
</template> </template>
@@ -180,6 +322,16 @@ onMounted(async () => {
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
} }
.result-card {
margin-top: 4px;
}
.history-header {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
user-select: none;
}
.filter-card :deep(.el-card__body) { .filter-card :deep(.el-card__body) {
padding: 12px 16px; padding: 12px 16px;
} }