打分列表改为品种打分,移除同步数据功能
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -1,48 +1,61 @@
|
|||||||
<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,
|
|
||||||
range: [],
|
|
||||||
signal: undefined,
|
|
||||||
limit: 200,
|
|
||||||
})
|
|
||||||
|
|
||||||
const contracts = ref<string[]>([])
|
const SYMBOLS_BY_EXCHANGE: Record<string, string[]> = {
|
||||||
const rows = ref<Score[]>([])
|
ZCE: ['FG', 'SA', 'MA', 'CF'],
|
||||||
const loading = ref(false)
|
SHF: ['RB'],
|
||||||
const drawerScoreId = ref<number | null>(null)
|
DCE: ['M'],
|
||||||
|
|
||||||
async function reload(silent = false) {
|
|
||||||
if (!silent) loading.value = true
|
|
||||||
try {
|
|
||||||
const [start, end] = filter.range || []
|
|
||||||
rows.value = await listScores({
|
|
||||||
ts_code: filter.ts_code,
|
|
||||||
start: start || undefined,
|
|
||||||
end: end || undefined,
|
|
||||||
signal: filter.signal,
|
|
||||||
limit: filter.limit,
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
if (!silent) loading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSignal(s: string) {
|
// 品种打分
|
||||||
filter.signal = filter.signal === s ? undefined : s
|
const selectedExchange = ref('')
|
||||||
reload(true)
|
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] || []
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(selectedExchange, () => {
|
||||||
|
selectedSymbol.value = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleScore() {
|
||||||
|
if (!selectedSymbol.value) {
|
||||||
|
ElMessage.warning('请选择品种')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scoring.value = true
|
||||||
|
scoreResult.value = null
|
||||||
|
try {
|
||||||
|
const resp = await runPipeline({ symbol: selectedSymbol.value })
|
||||||
|
scoreResult.value = resp
|
||||||
|
ElMessage.success('打分完成')
|
||||||
|
await nextTick()
|
||||||
|
resultRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
} catch (err: any) {
|
||||||
|
const msg = err?.response?.data?.error || err.message || '请求失败'
|
||||||
|
ElMessage.error(msg)
|
||||||
|
} finally {
|
||||||
|
scoring.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function signalTagType(s: string) {
|
function signalTagType(s: string) {
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user