Compare commits
2 Commits
6a1541ad9c
...
b6bacbfae9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6bacbfae9 | ||
|
|
c852b1d871 |
@@ -26,6 +26,9 @@ docker-compose -f docker-compose.trade.yml run --rm tushare python -m src.main -
|
||||
curl -X POST http://localhost:4001/api/v1/run -H "Content-Type: application/json" \
|
||||
-d '{"symbol":"FG"}'
|
||||
|
||||
# 批量触发所有固定品种今日打分
|
||||
curl -X POST http://localhost:4001/api/v1/run/batch
|
||||
|
||||
# 查询打分列表
|
||||
curl "http://localhost:4001/api/v1/scores?limit=5"
|
||||
|
||||
@@ -42,7 +45,7 @@ docker-compose -f docker-compose.trade.yml exec postgres psql -U trade -d future
|
||||
|
||||
**单进程串行流水线**:`src.main.main()` 先按命令行参数(显式 `ts_code` 优先,否则 `contracts.active_contract(symbol)` 按当月主力自动选)定下合约,再调 `run()` 顺序执行 `fetcher → storage(candles) → scorer → storage(scores)`。无后台任务、无队列,每次 CLI 调用处理一个合约一日。
|
||||
|
||||
**FastAPI 服务**(`src.api`):容器默认以 `uvicorn src.api:app` 启动,暴露 `/api/v1/run`(触发流水线)、`/api/v1/scores`、`/api/v1/scores/{id}`、`/api/v1/contracts`、`/api/v1/candles` 等端点。启动时自动 `storage.init_db()` 建表。API 与 CLI 共用同一套 `fetcher/storage/scorer` 逻辑。
|
||||
**FastAPI 服务**(`src.api`):容器默认以 `uvicorn src.api:app` 启动,暴露 `/api/v1/run`(触发流水线)、`/api/v1/run/batch`(批量打分)、`/api/v1/scores`、`/api/v1/scores/{id}`、`/api/v1/contracts`、`/api/v1/candles` 等端点。启动时自动 `storage.init_db()` 建表。API 与 CLI 共用同一套 `fetcher/storage/scorer` 逻辑。
|
||||
|
||||
**主力轮换规则**(`contracts.py`):每个品种在 `ROLLOVER_RULES` 中维护 `month -> (主力月, 年份偏移)` 表。FG 当前规则:1-3/12 月→05、4-7 月→09、8-11 月→01,其中 8-11 月与 12 月跨年(`year_offset=1`)。新增品种(如 RB、I)只需在该 dict 里加一条,无需改 main 流程。
|
||||
|
||||
|
||||
@@ -38,6 +38,9 @@ docker-compose -f docker-compose.trade.yml run --rm tushare python -m src.main
|
||||
curl -X POST http://localhost:4001/api/v1/run -H "Content-Type: application/json" \
|
||||
-d '{"symbol":"FG"}'
|
||||
|
||||
# 批量触发所有固定品种今日打分
|
||||
curl -X POST http://localhost:4001/api/v1/run/batch
|
||||
|
||||
# 查询最新打分
|
||||
curl "http://localhost:4001/api/v1/scores?limit=5"
|
||||
|
||||
@@ -214,7 +217,7 @@ A: 郑商所用 `.ZCE` 后缀(如 `FG2609.ZCE`),上期所用 `.SHF`,大
|
||||
|
||||
**Q: 如何定时自动跑?**
|
||||
|
||||
A: 通过宿主机 cron / launchd 等定时器调用 `docker-compose -f docker-compose.trade.yml run --rm tushare ...`。也可直接调用 API: `curl -X POST http://localhost:4001/api/v1/run ...`。
|
||||
A: 通过宿主机 cron / launchd 等定时器调用 `docker-compose -f docker-compose.trade.yml run --rm tushare ...`。也可直接调用 API: `curl -X POST http://localhost:4001/api/v1/run ...` 或批量接口 `curl -X POST http://localhost:4001/api/v1/run/batch`。
|
||||
|
||||
## Web 报表(浏览端)
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { useMobile } from '@/composables/useMobile'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const theme = useThemeStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { isMobile } = useMobile()
|
||||
|
||||
const drawerOpen = ref(false)
|
||||
|
||||
const showLayout = computed(() => route.meta.layout !== 'blank' && !!auth.token)
|
||||
|
||||
@@ -21,11 +25,16 @@ function logout() {
|
||||
auth.logout()
|
||||
router.replace('/login')
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
drawerOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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>
|
||||
<el-menu
|
||||
:default-active="route.path"
|
||||
@@ -40,14 +49,45 @@ function logout() {
|
||||
<el-menu-item v-if="auth.isAdmin" index="/admin/users">用户管理</el-menu-item>
|
||||
</el-menu>
|
||||
</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-header class="header">
|
||||
<div class="left">
|
||||
<el-button v-if="isMobile" text class="hamburger" @click="drawerOpen = !drawerOpen">
|
||||
<span style="font-size: 20px; line-height: 1">☰</span>
|
||||
</el-button>
|
||||
<div class="user">
|
||||
<span>{{ auth.user?.username }}</span>
|
||||
<el-tag size="small" :type="auth.isAdmin ? 'danger' : 'info'">
|
||||
{{ auth.isAdmin ? '管理员' : '普通用户' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<el-switch
|
||||
v-model="theme.isDark"
|
||||
@@ -86,12 +126,31 @@ body {
|
||||
.aside {
|
||||
background: #282828;
|
||||
color: #cfd8e3;
|
||||
transition: width 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.aside-light {
|
||||
background: #f9fafb;
|
||||
color: #1f2937;
|
||||
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 {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
@@ -100,6 +159,7 @@ body {
|
||||
font-size: 18px;
|
||||
letter-spacing: 2px;
|
||||
border-bottom: 1px solid #3a3a3a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.aside-light .brand {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
@@ -111,6 +171,15 @@ body {
|
||||
background: var(--el-bg-color);
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -124,4 +193,20 @@ body {
|
||||
.el-menu {
|
||||
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>
|
||||
|
||||
@@ -3,9 +3,11 @@ import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import type { Candle } from '@/api/candles'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { useMobile } from '@/composables/useMobile'
|
||||
|
||||
const props = defineProps<{ data: Candle[] }>()
|
||||
const theme = useThemeStore()
|
||||
const { isMobile } = useMobile()
|
||||
|
||||
const containerRef = ref<HTMLDivElement | null>(null)
|
||||
let chart: echarts.ECharts | null = null
|
||||
@@ -32,8 +34,8 @@ function render() {
|
||||
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%' },
|
||||
{ left: isMobile.value ? 48 : 60, right: isMobile.value ? 12 : 40, top: 40, height: '60%' },
|
||||
{ left: isMobile.value ? 48 : 60, right: isMobile.value ? 12 : 40, top: '78%', height: '18%' },
|
||||
],
|
||||
xAxis: [
|
||||
{ type: 'category', data: dates, scale: true, boundaryGap: false },
|
||||
@@ -100,6 +102,10 @@ watch(
|
||||
render()
|
||||
},
|
||||
)
|
||||
watch(isMobile, () => {
|
||||
ensureChart()
|
||||
render()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -111,4 +117,9 @@ watch(
|
||||
width: 100%;
|
||||
height: 560px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.chart {
|
||||
height: 420px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { getScore, type Score } from '@/api/scores'
|
||||
import { parseTsCode } from '@/utils/contract'
|
||||
import { useMobile } from '@/composables/useMobile'
|
||||
|
||||
const { isMobile } = useMobile()
|
||||
|
||||
const props = defineProps<{ scoreId: number | null }>()
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
@@ -34,9 +37,9 @@ watch(
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<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).contract }}</el-descriptions-item>
|
||||
<el-descriptions-item label="日期">{{ score.trade_date }}</el-descriptions-item>
|
||||
@@ -50,13 +53,14 @@ watch(
|
||||
</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">
|
||||
<el-descriptions-item label="长期(30d × 0.25)" :span="isMobile ? 1 : 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>
|
||||
<div class="table-wrapper">
|
||||
<el-table :data="score.detail?.short_details ?? []" size="small" border class="detail-table">
|
||||
<el-table-column prop="trade_date" label="日期" width="100" />
|
||||
<el-table-column prop="close" label="收盘" />
|
||||
<el-table-column prop="pre_close" label="昨收" />
|
||||
@@ -64,9 +68,10 @@ watch(
|
||||
<el-table-column prop="oi_chg" label="持仓变化" />
|
||||
<el-table-column prop="score" label="单日得分" />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<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="价格收益率">
|
||||
{{ (score.detail.medium_detail.price_return_pct * 100).toFixed(2) }}%
|
||||
</el-descriptions-item>
|
||||
@@ -79,20 +84,20 @@ watch(
|
||||
<el-descriptions-item label="增仓下跌日">
|
||||
{{ score.detail.medium_detail.long_down_days }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="资金意愿分" :span="2">
|
||||
<el-descriptions-item label="资金意愿分" :span="isMobile ? 1 : 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 :column="isMobile ? 1 : 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">
|
||||
<el-descriptions-item label="变化幅度" :span="isMobile ? 1 : 2">
|
||||
{{ (score.detail.long_detail.change_pct * 100).toFixed(2) }}%
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
@@ -104,4 +109,10 @@ watch(
|
||||
.section {
|
||||
margin: 18px 0 8px;
|
||||
}
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.detail-table {
|
||||
min-width: 520px;
|
||||
}
|
||||
</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,7 +103,8 @@ onMounted(reload)
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-table :data="users" v-loading="loading" stripe>
|
||||
<div class="table-wrapper" v-loading="loading">
|
||||
<el-table :data="users" stripe class="user-table">
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="role" label="角色" width="100">
|
||||
@@ -142,6 +143,7 @@ onMounted(reload)
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="createDialog.visible" title="新建账号" width="420px">
|
||||
<el-form label-width="80px">
|
||||
@@ -195,4 +197,19 @@ onMounted(reload)
|
||||
align-items: center;
|
||||
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>
|
||||
|
||||
@@ -87,9 +87,12 @@ async function submit() {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #1f2d3d 0%, #3a506b 100%);
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
width: 360px;
|
||||
max-width: 100%;
|
||||
padding: 36px 32px;
|
||||
background: var(--el-bg-color);
|
||||
color: var(--el-text-color-primary);
|
||||
@@ -106,4 +109,9 @@ async function submit() {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.card {
|
||||
padding: 28px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,9 @@ import { ElMessage } from 'element-plus'
|
||||
import { listContracts } from '@/api/scores'
|
||||
import { listCandles, type Candle } from '@/api/candles'
|
||||
import KLineChart from '@/components/KLineChart.vue'
|
||||
import { useMobile } from '@/composables/useMobile'
|
||||
|
||||
const { isMobile } = useMobile()
|
||||
|
||||
const filter = reactive<{ ts_code: string; range: [string, string] | [] }>({
|
||||
ts_code: '',
|
||||
@@ -40,13 +43,13 @@ onMounted(async () => {
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form :inline="true">
|
||||
<el-form :inline="!isMobile">
|
||||
<el-form-item label="合约">
|
||||
<el-select
|
||||
v-model="filter.ts_code"
|
||||
placeholder="选择合约"
|
||||
filterable
|
||||
style="width: 200px"
|
||||
:style="{ width: isMobile ? '100%' : '200px' }"
|
||||
>
|
||||
<el-option v-for="c in contracts" :key="c" :label="c" :value="c" />
|
||||
</el-select>
|
||||
@@ -59,6 +62,7 @@ onMounted(async () => {
|
||||
range-separator="→"
|
||||
start-placeholder="起"
|
||||
end-placeholder="止"
|
||||
:style="{ width: isMobile ? '100%' : 'auto' }"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
|
||||
@@ -70,9 +70,11 @@ async function submit() {
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #1f2d3d 0%, #3a506b 100%);
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
width: 360px;
|
||||
max-width: 100%;
|
||||
padding: 36px 32px;
|
||||
background: var(--el-bg-color);
|
||||
color: var(--el-text-color-primary);
|
||||
@@ -89,4 +91,9 @@ async function submit() {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.card {
|
||||
padding: 28px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
type RunResponse,
|
||||
} from '@/api/run'
|
||||
import { parseTsCode } from '@/utils/contract'
|
||||
import { useMobile } from '@/composables/useMobile'
|
||||
|
||||
const { isMobile } = useMobile()
|
||||
|
||||
const SYMBOLS = ['FG', 'SA', 'RB', 'MA', 'CF', 'M']
|
||||
|
||||
@@ -139,7 +142,7 @@ onMounted(loadActive)
|
||||
<template #header>
|
||||
<span>打分结果</span>
|
||||
</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).contract }}</el-descriptions-item>
|
||||
<el-descriptions-item label="日期">{{ result.trade_date }}</el-descriptions-item>
|
||||
@@ -151,7 +154,7 @@ onMounted(loadActive)
|
||||
<el-descriptions-item label="综合">
|
||||
<strong>{{ result.composite }}</strong>
|
||||
</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-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
@@ -3,6 +3,9 @@ import { onMounted, reactive, ref } from 'vue'
|
||||
import { listContracts, listScores, type Score } from '@/api/scores'
|
||||
import ScoreDetailDrawer from '@/components/ScoreDetailDrawer.vue'
|
||||
import { parseTsCode } from '@/utils/contract'
|
||||
import { useMobile } from '@/composables/useMobile'
|
||||
|
||||
const { isMobile } = useMobile()
|
||||
|
||||
const filter = reactive<{
|
||||
ts_code?: string
|
||||
@@ -59,14 +62,14 @@ onMounted(async () => {
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form :inline="true">
|
||||
<el-form :inline="!isMobile">
|
||||
<el-form-item label="合约">
|
||||
<el-select
|
||||
v-model="filter.ts_code"
|
||||
placeholder="全部合约"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 200px"
|
||||
:style="{ width: isMobile ? '100%' : '200px' }"
|
||||
>
|
||||
<el-option v-for="c in contracts" :key="c" :label="c" :value="c" />
|
||||
</el-select>
|
||||
@@ -79,6 +82,7 @@ onMounted(async () => {
|
||||
range-separator="→"
|
||||
start-placeholder="起"
|
||||
end-placeholder="止"
|
||||
:style="{ width: isMobile ? '100%' : 'auto' }"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="条数">
|
||||
@@ -87,8 +91,8 @@ onMounted(async () => {
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" style="width: 88px" @click="reload">查询</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="快捷">
|
||||
<el-button-group class="signal-group">
|
||||
<el-form-item label="快捷" class="signal-item">
|
||||
<el-button-group class="signal-group" :size="isMobile ? 'small' : 'default'">
|
||||
<el-button
|
||||
:type="filter.signal === '强烈看多' ? 'success' : ''"
|
||||
@click="toggleSignal('强烈看多')"
|
||||
@@ -118,7 +122,8 @@ onMounted(async () => {
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" stripe class="score-table">
|
||||
<div class="table-wrapper" v-loading="loading">
|
||||
<el-table :data="rows" stripe class="score-table">
|
||||
<el-table-column prop="trade_date" label="日期" width="100" />
|
||||
<el-table-column label="品种" width="80">
|
||||
<template #default="{ row }">
|
||||
@@ -152,6 +157,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<ScoreDetailDrawer
|
||||
:score-id="drawerScoreId"
|
||||
@@ -177,7 +183,21 @@ onMounted(async () => {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.score-table {
|
||||
.table-wrapper {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user