适配移动端展示
This commit is contained in:
@@ -1,13 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useThemeStore } from '@/stores/theme'
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
import { useMobile } from '@/composables/useMobile'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const theme = useThemeStore()
|
const theme = useThemeStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const { isMobile } = useMobile()
|
||||||
|
|
||||||
|
const drawerOpen = ref(false)
|
||||||
|
|
||||||
const showLayout = computed(() => route.meta.layout !== 'blank' && !!auth.token)
|
const showLayout = computed(() => route.meta.layout !== 'blank' && !!auth.token)
|
||||||
|
|
||||||
@@ -21,11 +25,16 @@ function logout() {
|
|||||||
auth.logout()
|
auth.logout()
|
||||||
router.replace('/login')
|
router.replace('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeDrawer() {
|
||||||
|
drawerOpen.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-container v-if="showLayout" class="app">
|
<el-container v-if="showLayout" class="app">
|
||||||
<el-aside width="220px" class="aside" :class="{ 'aside-light': !theme.isDark }">
|
<!-- desktop sidebar -->
|
||||||
|
<el-aside v-if="!isMobile" width="220px" class="aside" :class="{ 'aside-light': !theme.isDark }">
|
||||||
<div class="brand">期货报告</div>
|
<div class="brand">期货报告</div>
|
||||||
<el-menu
|
<el-menu
|
||||||
:default-active="route.path"
|
:default-active="route.path"
|
||||||
@@ -40,13 +49,44 @@ function logout() {
|
|||||||
<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>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
|
||||||
|
<!-- mobile drawer overlay -->
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="drawerOpen" class="drawer-mask" @click="closeDrawer"></div>
|
||||||
|
<el-aside
|
||||||
|
:width="drawerOpen ? '220px' : '0px'"
|
||||||
|
class="aside mobile-aside"
|
||||||
|
:class="{ 'aside-light': !theme.isDark, open: drawerOpen }"
|
||||||
|
>
|
||||||
|
<div class="brand">期货报告</div>
|
||||||
|
<el-menu
|
||||||
|
:default-active="route.path"
|
||||||
|
router
|
||||||
|
:background-color="menuColors.bg"
|
||||||
|
:text-color="menuColors.text"
|
||||||
|
:active-text-color="menuColors.active"
|
||||||
|
@select="closeDrawer"
|
||||||
|
>
|
||||||
|
<el-menu-item index="/scores">打分列表</el-menu-item>
|
||||||
|
<el-menu-item index="/chart">K 线 / 持仓</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>
|
||||||
|
</el-aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header class="header">
|
<el-header class="header">
|
||||||
<div class="user">
|
<div class="left">
|
||||||
<span>{{ auth.user?.username }}</span>
|
<el-button v-if="isMobile" text class="hamburger" @click="drawerOpen = !drawerOpen">
|
||||||
<el-tag size="small" :type="auth.isAdmin ? 'danger' : 'info'">
|
<span style="font-size: 20px; line-height: 1">☰</span>
|
||||||
{{ auth.isAdmin ? '管理员' : '普通用户' }}
|
</el-button>
|
||||||
</el-tag>
|
<div class="user">
|
||||||
|
<span>{{ auth.user?.username }}</span>
|
||||||
|
<el-tag size="small" :type="auth.isAdmin ? 'danger' : 'info'">
|
||||||
|
{{ auth.isAdmin ? '管理员' : '普通用户' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<el-switch
|
<el-switch
|
||||||
@@ -86,12 +126,31 @@ body {
|
|||||||
.aside {
|
.aside {
|
||||||
background: #282828;
|
background: #282828;
|
||||||
color: #cfd8e3;
|
color: #cfd8e3;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.aside-light {
|
.aside-light {
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
border-right: 1px solid var(--el-border-color-light);
|
border-right: 1px solid var(--el-border-color-light);
|
||||||
}
|
}
|
||||||
|
.mobile-aside {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2001;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
.mobile-aside.open {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
.drawer-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2000;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
.brand {
|
.brand {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -100,6 +159,7 @@ body {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
border-bottom: 1px solid #3a3a3a;
|
border-bottom: 1px solid #3a3a3a;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.aside-light .brand {
|
.aside-light .brand {
|
||||||
border-bottom: 1px solid #e5e7eb;
|
border-bottom: 1px solid #e5e7eb;
|
||||||
@@ -111,6 +171,15 @@ body {
|
|||||||
background: var(--el-bg-color);
|
background: var(--el-bg-color);
|
||||||
border-bottom: 1px solid var(--el-border-color-light);
|
border-bottom: 1px solid var(--el-border-color-light);
|
||||||
}
|
}
|
||||||
|
.left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.hamburger {
|
||||||
|
padding: 4px !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
.user {
|
.user {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -124,4 +193,20 @@ body {
|
|||||||
.el-menu {
|
.el-menu {
|
||||||
border-right: none !important;
|
border-right: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* mobile overrides */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header {
|
||||||
|
padding: 0 12px !important;
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.user span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.el-main {
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
import type { Candle } from '@/api/candles'
|
import type { Candle } from '@/api/candles'
|
||||||
import { useThemeStore } from '@/stores/theme'
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
import { useMobile } from '@/composables/useMobile'
|
||||||
|
|
||||||
const props = defineProps<{ data: Candle[] }>()
|
const props = defineProps<{ data: Candle[] }>()
|
||||||
const theme = useThemeStore()
|
const theme = useThemeStore()
|
||||||
|
const { isMobile } = useMobile()
|
||||||
|
|
||||||
const containerRef = ref<HTMLDivElement | null>(null)
|
const containerRef = ref<HTMLDivElement | null>(null)
|
||||||
let chart: echarts.ECharts | null = null
|
let chart: echarts.ECharts | null = null
|
||||||
@@ -32,8 +34,8 @@ function render() {
|
|||||||
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
|
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
|
||||||
legend: { data: ['K 线', '持仓量'], top: 0 },
|
legend: { data: ['K 线', '持仓量'], top: 0 },
|
||||||
grid: [
|
grid: [
|
||||||
{ left: 60, right: 40, top: 40, height: '60%' },
|
{ left: isMobile.value ? 48 : 60, right: isMobile.value ? 12 : 40, top: 40, height: '60%' },
|
||||||
{ left: 60, right: 40, top: '78%', height: '18%' },
|
{ left: isMobile.value ? 48 : 60, right: isMobile.value ? 12 : 40, top: '78%', height: '18%' },
|
||||||
],
|
],
|
||||||
xAxis: [
|
xAxis: [
|
||||||
{ type: 'category', data: dates, scale: true, boundaryGap: false },
|
{ type: 'category', data: dates, scale: true, boundaryGap: false },
|
||||||
@@ -100,6 +102,10 @@ watch(
|
|||||||
render()
|
render()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
watch(isMobile, () => {
|
||||||
|
ensureChart()
|
||||||
|
render()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -111,4 +117,9 @@ watch(
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 560px;
|
height: 560px;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chart {
|
||||||
|
height: 420px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { getScore, type Score } from '@/api/scores'
|
import { getScore, type Score } from '@/api/scores'
|
||||||
import { parseTsCode } from '@/utils/contract'
|
import { parseTsCode } from '@/utils/contract'
|
||||||
|
import { useMobile } from '@/composables/useMobile'
|
||||||
|
|
||||||
|
const { isMobile } = useMobile()
|
||||||
|
|
||||||
const props = defineProps<{ scoreId: number | null }>()
|
const props = defineProps<{ scoreId: number | null }>()
|
||||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||||
@@ -34,9 +37,9 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-drawer v-model="visible" title="打分明细" size="640px" destroy-on-close>
|
<el-drawer v-model="visible" title="打分明细" :size="isMobile ? '92%' : '640px'" destroy-on-close>
|
||||||
<div v-loading="loading" v-if="score">
|
<div v-loading="loading" v-if="score">
|
||||||
<el-descriptions :column="2" border>
|
<el-descriptions :column="isMobile ? 1 : 2" border>
|
||||||
<el-descriptions-item label="品种">{{ parseTsCode(score.ts_code).symbol }}</el-descriptions-item>
|
<el-descriptions-item label="品种">{{ parseTsCode(score.ts_code).symbol }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="合约">{{ parseTsCode(score.ts_code).contract }}</el-descriptions-item>
|
<el-descriptions-item label="合约">{{ parseTsCode(score.ts_code).contract }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="日期">{{ score.trade_date }}</el-descriptions-item>
|
<el-descriptions-item label="日期">{{ score.trade_date }}</el-descriptions-item>
|
||||||
@@ -50,23 +53,25 @@ watch(
|
|||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="短期(7d × 0.4)">{{ score.short_term.toFixed(2) }}</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="中期(15d × 0.35)">{{ score.medium_term.toFixed(2) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="长期(30d × 0.25)" :span="2">
|
<el-descriptions-item label="长期(30d × 0.25)" :span="isMobile ? 1 : 2">
|
||||||
{{ score.long_term.toFixed(2) }}
|
{{ score.long_term.toFixed(2) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
<h4 class="section">短期 7 日逐日打分</h4>
|
<h4 class="section">短期 7 日逐日打分</h4>
|
||||||
<el-table :data="score.detail?.short_details ?? []" size="small" border>
|
<div class="table-wrapper">
|
||||||
<el-table-column prop="trade_date" label="日期" width="100" />
|
<el-table :data="score.detail?.short_details ?? []" size="small" border class="detail-table">
|
||||||
<el-table-column prop="close" label="收盘" />
|
<el-table-column prop="trade_date" label="日期" width="100" />
|
||||||
<el-table-column prop="pre_close" label="昨收" />
|
<el-table-column prop="close" label="收盘" />
|
||||||
<el-table-column prop="oi" label="持仓" />
|
<el-table-column prop="pre_close" label="昨收" />
|
||||||
<el-table-column prop="oi_chg" label="持仓变化" />
|
<el-table-column prop="oi" label="持仓" />
|
||||||
<el-table-column prop="score" label="单日得分" />
|
<el-table-column prop="oi_chg" label="持仓变化" />
|
||||||
</el-table>
|
<el-table-column prop="score" label="单日得分" />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4 class="section">中期(15d)细节</h4>
|
<h4 class="section">中期(15d)细节</h4>
|
||||||
<el-descriptions :column="2" border v-if="score.detail?.medium_detail">
|
<el-descriptions :column="isMobile ? 1 : 2" border v-if="score.detail?.medium_detail">
|
||||||
<el-descriptions-item label="价格收益率">
|
<el-descriptions-item label="价格收益率">
|
||||||
{{ (score.detail.medium_detail.price_return_pct * 100).toFixed(2) }}%
|
{{ (score.detail.medium_detail.price_return_pct * 100).toFixed(2) }}%
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -79,20 +84,20 @@ watch(
|
|||||||
<el-descriptions-item label="增仓下跌日">
|
<el-descriptions-item label="增仓下跌日">
|
||||||
{{ score.detail.medium_detail.long_down_days }}
|
{{ score.detail.medium_detail.long_down_days }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="资金意愿分" :span="2">
|
<el-descriptions-item label="资金意愿分" :span="isMobile ? 1 : 2">
|
||||||
{{ score.detail.medium_detail.fund_signal }}
|
{{ score.detail.medium_detail.fund_signal }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
<h4 class="section">长期(30d)细节</h4>
|
<h4 class="section">长期(30d)细节</h4>
|
||||||
<el-descriptions :column="2" border v-if="score.detail?.long_detail">
|
<el-descriptions :column="isMobile ? 1 : 2" border v-if="score.detail?.long_detail">
|
||||||
<el-descriptions-item label="30 日均持仓">
|
<el-descriptions-item label="30 日均持仓">
|
||||||
{{ score.detail.long_detail.avg_oi.toFixed(0) }}
|
{{ score.detail.long_detail.avg_oi.toFixed(0) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="30 日前持仓">
|
<el-descriptions-item label="30 日前持仓">
|
||||||
{{ score.detail.long_detail.oi_before.toFixed(0) }}
|
{{ score.detail.long_detail.oi_before.toFixed(0) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="变化幅度" :span="2">
|
<el-descriptions-item label="变化幅度" :span="isMobile ? 1 : 2">
|
||||||
{{ (score.detail.long_detail.change_pct * 100).toFixed(2) }}%
|
{{ (score.detail.long_detail.change_pct * 100).toFixed(2) }}%
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
@@ -104,4 +109,10 @@ watch(
|
|||||||
.section {
|
.section {
|
||||||
margin: 18px 0 8px;
|
margin: 18px 0 8px;
|
||||||
}
|
}
|
||||||
|
.table-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.detail-table {
|
||||||
|
min-width: 520px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
26
web/frontend/src/composables/useMobile.ts
Normal file
26
web/frontend/src/composables/useMobile.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const isMobile = ref(false)
|
||||||
|
|
||||||
|
function check() {
|
||||||
|
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||||
|
}
|
||||||
|
|
||||||
|
let listeners = 0
|
||||||
|
|
||||||
|
export function useMobile() {
|
||||||
|
onMounted(() => {
|
||||||
|
if (listeners === 0) {
|
||||||
|
check()
|
||||||
|
window.addEventListener('resize', check)
|
||||||
|
}
|
||||||
|
listeners++
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
listeners--
|
||||||
|
if (listeners === 0) {
|
||||||
|
window.removeEventListener('resize', check)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return { isMobile }
|
||||||
|
}
|
||||||
@@ -103,45 +103,47 @@ onMounted(reload)
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-table :data="users" v-loading="loading" stripe>
|
<div class="table-wrapper" v-loading="loading">
|
||||||
<el-table-column prop="id" label="ID" width="60" />
|
<el-table :data="users" stripe class="user-table">
|
||||||
<el-table-column prop="username" label="用户名" />
|
<el-table-column prop="id" label="ID" width="60" />
|
||||||
<el-table-column prop="role" label="角色" width="100">
|
<el-table-column prop="username" label="用户名" />
|
||||||
<template #default="{ row }">
|
<el-table-column prop="role" label="角色" width="100">
|
||||||
<el-tag :type="row.role === 'admin' ? 'danger' : 'info'">{{ row.role }}</el-tag>
|
<template #default="{ row }">
|
||||||
</template>
|
<el-tag :type="row.role === 'admin' ? 'danger' : 'info'">{{ row.role }}</el-tag>
|
||||||
</el-table-column>
|
</template>
|
||||||
<el-table-column label="状态" width="100">
|
</el-table-column>
|
||||||
<template #default="{ row }">
|
<el-table-column label="状态" width="100">
|
||||||
<el-tag :type="row.disabled ? 'warning' : 'success'">
|
<template #default="{ row }">
|
||||||
{{ row.disabled ? '已禁用' : '正常' }}
|
<el-tag :type="row.disabled ? 'warning' : 'success'">
|
||||||
</el-tag>
|
{{ row.disabled ? '已禁用' : '正常' }}
|
||||||
</template>
|
</el-tag>
|
||||||
</el-table-column>
|
</template>
|
||||||
<el-table-column prop="created_at" label="创建于" width="180" />
|
</el-table-column>
|
||||||
<el-table-column prop="updated_at" label="更新于" width="180" />
|
<el-table-column prop="created_at" label="创建于" width="180" />
|
||||||
<el-table-column label="操作" width="280" fixed="right">
|
<el-table-column prop="updated_at" label="更新于" width="180" />
|
||||||
<template #default="{ row }">
|
<el-table-column label="操作" width="280" fixed="right">
|
||||||
<el-button link type="primary" @click="openReset(row)">重置密码</el-button>
|
<template #default="{ row }">
|
||||||
<el-button
|
<el-button link type="primary" @click="openReset(row)">重置密码</el-button>
|
||||||
link
|
<el-button
|
||||||
:type="row.disabled ? 'success' : 'warning'"
|
link
|
||||||
:disabled="row.id === auth.user?.id"
|
:type="row.disabled ? 'success' : 'warning'"
|
||||||
@click="toggleDisabled(row)"
|
:disabled="row.id === auth.user?.id"
|
||||||
>
|
@click="toggleDisabled(row)"
|
||||||
{{ row.disabled ? '启用' : '禁用' }}
|
>
|
||||||
</el-button>
|
{{ row.disabled ? '启用' : '禁用' }}
|
||||||
<el-button
|
</el-button>
|
||||||
link
|
<el-button
|
||||||
type="danger"
|
link
|
||||||
:disabled="row.id === auth.user?.id"
|
type="danger"
|
||||||
@click="remove(row)"
|
:disabled="row.id === auth.user?.id"
|
||||||
>
|
@click="remove(row)"
|
||||||
删除
|
>
|
||||||
</el-button>
|
删除
|
||||||
</template>
|
</el-button>
|
||||||
</el-table-column>
|
</template>
|
||||||
</el-table>
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="createDialog.visible" title="新建账号" width="420px">
|
<el-dialog v-model="createDialog.visible" title="新建账号" width="420px">
|
||||||
<el-form label-width="80px">
|
<el-form label-width="80px">
|
||||||
@@ -195,4 +197,19 @@ onMounted(reload)
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--el-text-color-regular);
|
color: var(--el-text-color-regular);
|
||||||
}
|
}
|
||||||
|
.table-wrapper {
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.user-table {
|
||||||
|
min-width: 960px;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.head {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -87,9 +87,12 @@ async function submit() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: linear-gradient(135deg, #1f2d3d 0%, #3a506b 100%);
|
background: linear-gradient(135deg, #1f2d3d 0%, #3a506b 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.card {
|
.card {
|
||||||
width: 360px;
|
width: 360px;
|
||||||
|
max-width: 100%;
|
||||||
padding: 36px 32px;
|
padding: 36px 32px;
|
||||||
background: var(--el-bg-color);
|
background: var(--el-bg-color);
|
||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
@@ -106,4 +109,9 @@ async function submit() {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.card {
|
||||||
|
padding: 28px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { ElMessage } from 'element-plus'
|
|||||||
import { listContracts } from '@/api/scores'
|
import { listContracts } from '@/api/scores'
|
||||||
import { listCandles, type Candle } from '@/api/candles'
|
import { listCandles, type Candle } from '@/api/candles'
|
||||||
import KLineChart from '@/components/KLineChart.vue'
|
import KLineChart from '@/components/KLineChart.vue'
|
||||||
|
import { useMobile } from '@/composables/useMobile'
|
||||||
|
|
||||||
|
const { isMobile } = useMobile()
|
||||||
|
|
||||||
const filter = reactive<{ ts_code: string; range: [string, string] | [] }>({
|
const filter = reactive<{ ts_code: string; range: [string, string] | [] }>({
|
||||||
ts_code: '',
|
ts_code: '',
|
||||||
@@ -40,13 +43,13 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<el-card shadow="never" class="filter-card">
|
<el-card shadow="never" class="filter-card">
|
||||||
<el-form :inline="true">
|
<el-form :inline="!isMobile">
|
||||||
<el-form-item label="合约">
|
<el-form-item label="合约">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="filter.ts_code"
|
v-model="filter.ts_code"
|
||||||
placeholder="选择合约"
|
placeholder="选择合约"
|
||||||
filterable
|
filterable
|
||||||
style="width: 200px"
|
:style="{ width: isMobile ? '100%' : '200px' }"
|
||||||
>
|
>
|
||||||
<el-option v-for="c in contracts" :key="c" :label="c" :value="c" />
|
<el-option v-for="c in contracts" :key="c" :label="c" :value="c" />
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -59,6 +62,7 @@ onMounted(async () => {
|
|||||||
range-separator="→"
|
range-separator="→"
|
||||||
start-placeholder="起"
|
start-placeholder="起"
|
||||||
end-placeholder="止"
|
end-placeholder="止"
|
||||||
|
:style="{ width: isMobile ? '100%' : 'auto' }"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
|
|||||||
@@ -70,9 +70,11 @@ async function submit() {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: linear-gradient(135deg, #1f2d3d 0%, #3a506b 100%);
|
background: linear-gradient(135deg, #1f2d3d 0%, #3a506b 100%);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.card {
|
.card {
|
||||||
width: 360px;
|
width: 360px;
|
||||||
|
max-width: 100%;
|
||||||
padding: 36px 32px;
|
padding: 36px 32px;
|
||||||
background: var(--el-bg-color);
|
background: var(--el-bg-color);
|
||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
@@ -89,4 +91,9 @@ async function submit() {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.card {
|
||||||
|
padding: 28px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import {
|
|||||||
type RunResponse,
|
type RunResponse,
|
||||||
} from '@/api/run'
|
} from '@/api/run'
|
||||||
import { parseTsCode } from '@/utils/contract'
|
import { parseTsCode } from '@/utils/contract'
|
||||||
|
import { useMobile } from '@/composables/useMobile'
|
||||||
|
|
||||||
|
const { isMobile } = useMobile()
|
||||||
|
|
||||||
const SYMBOLS = ['FG', 'SA', 'RB', 'MA', 'CF', 'M']
|
const SYMBOLS = ['FG', 'SA', 'RB', 'MA', 'CF', 'M']
|
||||||
|
|
||||||
@@ -139,7 +142,7 @@ onMounted(loadActive)
|
|||||||
<template #header>
|
<template #header>
|
||||||
<span>打分结果</span>
|
<span>打分结果</span>
|
||||||
</template>
|
</template>
|
||||||
<el-descriptions :column="2" border>
|
<el-descriptions :column="isMobile ? 1 : 2" border>
|
||||||
<el-descriptions-item label="品种">{{ parseTsCode(result.ts_code).symbol }}</el-descriptions-item>
|
<el-descriptions-item label="品种">{{ parseTsCode(result.ts_code).symbol }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="合约">{{ parseTsCode(result.ts_code).contract }}</el-descriptions-item>
|
<el-descriptions-item label="合约">{{ parseTsCode(result.ts_code).contract }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="日期">{{ result.trade_date }}</el-descriptions-item>
|
<el-descriptions-item label="日期">{{ result.trade_date }}</el-descriptions-item>
|
||||||
@@ -151,7 +154,7 @@ onMounted(loadActive)
|
|||||||
<el-descriptions-item label="综合">
|
<el-descriptions-item label="综合">
|
||||||
<strong>{{ result.composite }}</strong>
|
<strong>{{ result.composite }}</strong>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="信号" :span="2">
|
<el-descriptions-item label="信号" :span="isMobile ? 1 : 2">
|
||||||
<el-tag :type="signalTagType(result.signal)">{{ result.signal }}</el-tag>
|
<el-tag :type="signalTagType(result.signal)">{{ result.signal }}</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { onMounted, reactive, ref } from 'vue'
|
|||||||
import { listContracts, listScores, type Score } from '@/api/scores'
|
import { listContracts, listScores, type Score } from '@/api/scores'
|
||||||
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'
|
||||||
|
|
||||||
|
const { isMobile } = useMobile()
|
||||||
|
|
||||||
const filter = reactive<{
|
const filter = reactive<{
|
||||||
ts_code?: string
|
ts_code?: string
|
||||||
@@ -59,14 +62,14 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<el-card shadow="never" class="filter-card">
|
<el-card shadow="never" class="filter-card">
|
||||||
<el-form :inline="true">
|
<el-form :inline="!isMobile">
|
||||||
<el-form-item label="合约">
|
<el-form-item label="合约">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="filter.ts_code"
|
v-model="filter.ts_code"
|
||||||
placeholder="全部合约"
|
placeholder="全部合约"
|
||||||
clearable
|
clearable
|
||||||
filterable
|
filterable
|
||||||
style="width: 200px"
|
:style="{ width: isMobile ? '100%' : '200px' }"
|
||||||
>
|
>
|
||||||
<el-option v-for="c in contracts" :key="c" :label="c" :value="c" />
|
<el-option v-for="c in contracts" :key="c" :label="c" :value="c" />
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -79,6 +82,7 @@ onMounted(async () => {
|
|||||||
range-separator="→"
|
range-separator="→"
|
||||||
start-placeholder="起"
|
start-placeholder="起"
|
||||||
end-placeholder="止"
|
end-placeholder="止"
|
||||||
|
:style="{ width: isMobile ? '100%' : 'auto' }"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="条数">
|
<el-form-item label="条数">
|
||||||
@@ -87,8 +91,8 @@ onMounted(async () => {
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" :loading="loading" style="width: 88px" @click="reload">查询</el-button>
|
<el-button type="primary" :loading="loading" style="width: 88px" @click="reload">查询</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="快捷">
|
<el-form-item label="快捷" class="signal-item">
|
||||||
<el-button-group class="signal-group">
|
<el-button-group class="signal-group" :size="isMobile ? 'small' : 'default'">
|
||||||
<el-button
|
<el-button
|
||||||
:type="filter.signal === '强烈看多' ? 'success' : ''"
|
:type="filter.signal === '强烈看多' ? 'success' : ''"
|
||||||
@click="toggleSignal('强烈看多')"
|
@click="toggleSignal('强烈看多')"
|
||||||
@@ -118,40 +122,42 @@ onMounted(async () => {
|
|||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-table :data="rows" v-loading="loading" stripe class="score-table">
|
<div class="table-wrapper" v-loading="loading">
|
||||||
<el-table-column prop="trade_date" label="日期" width="100" />
|
<el-table :data="rows" stripe class="score-table">
|
||||||
<el-table-column label="品种" width="80">
|
<el-table-column prop="trade_date" label="日期" width="100" />
|
||||||
<template #default="{ row }">
|
<el-table-column label="品种" width="80">
|
||||||
{{ parseTsCode(row.ts_code).symbol }}
|
<template #default="{ row }">
|
||||||
</template>
|
{{ parseTsCode(row.ts_code).symbol }}
|
||||||
</el-table-column>
|
</template>
|
||||||
<el-table-column label="合约" width="80">
|
</el-table-column>
|
||||||
<template #default="{ row }">
|
<el-table-column label="合约" width="80">
|
||||||
{{ parseTsCode(row.ts_code).contract }}
|
<template #default="{ row }">
|
||||||
</template>
|
{{ parseTsCode(row.ts_code).contract }}
|
||||||
</el-table-column>
|
</template>
|
||||||
<el-table-column prop="close" label="收盘" width="90" />
|
</el-table-column>
|
||||||
<el-table-column prop="oi" label="持仓" width="100" />
|
<el-table-column prop="close" label="收盘" width="90" />
|
||||||
<el-table-column prop="oi_chg" label="持仓变化" width="100" />
|
<el-table-column prop="oi" label="持仓" width="100" />
|
||||||
<el-table-column prop="short_term" label="短期(7d)" width="90" />
|
<el-table-column prop="oi_chg" label="持仓变化" width="100" />
|
||||||
<el-table-column prop="medium_term" label="中期(15d)" width="90" />
|
<el-table-column prop="short_term" label="短期(7d)" width="90" />
|
||||||
<el-table-column prop="long_term" label="长期(30d)" width="90" />
|
<el-table-column prop="medium_term" label="中期(15d)" width="90" />
|
||||||
<el-table-column prop="composite" label="综合" width="80">
|
<el-table-column prop="long_term" label="长期(30d)" width="90" />
|
||||||
<template #default="{ row }">
|
<el-table-column prop="composite" label="综合" width="80">
|
||||||
<strong>{{ row.composite.toFixed(2) }}</strong>
|
<template #default="{ row }">
|
||||||
</template>
|
<strong>{{ row.composite.toFixed(2) }}</strong>
|
||||||
</el-table-column>
|
</template>
|
||||||
<el-table-column prop="signal" label="信号" min-width="160">
|
</el-table-column>
|
||||||
<template #default="{ row }">
|
<el-table-column prop="signal" label="信号" min-width="160">
|
||||||
<el-tag :type="signalTagType(row.signal)">{{ row.signal }}</el-tag>
|
<template #default="{ row }">
|
||||||
</template>
|
<el-tag :type="signalTagType(row.signal)">{{ row.signal }}</el-tag>
|
||||||
</el-table-column>
|
</template>
|
||||||
<el-table-column label="操作" width="80" fixed="right">
|
</el-table-column>
|
||||||
<template #default="{ row }">
|
<el-table-column label="操作" width="80" fixed="right">
|
||||||
<el-button type="primary" link @click="drawerScoreId = row.id">明细</el-button>
|
<template #default="{ row }">
|
||||||
</template>
|
<el-button type="primary" link @click="drawerScoreId = row.id">明细</el-button>
|
||||||
</el-table-column>
|
</template>
|
||||||
</el-table>
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ScoreDetailDrawer
|
<ScoreDetailDrawer
|
||||||
:score-id="drawerScoreId"
|
:score-id="drawerScoreId"
|
||||||
@@ -177,7 +183,21 @@ onMounted(async () => {
|
|||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.score-table {
|
.table-wrapper {
|
||||||
background: var(--el-bg-color);
|
background: var(--el-bg-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.score-table {
|
||||||
|
min-width: 960px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.signal-item {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.signal-group {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user