Compare commits

...

2 Commits

Author SHA1 Message Date
fish
b6bacbfae9 适配移动端展示 2026-05-04 21:36:48 +08:00
fish
c852b1d871 同步批量打分接口文档 2026-05-03 22:31:19 +08:00
12 changed files with 306 additions and 108 deletions

View File

@@ -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 流程。

View File

@@ -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 报表(浏览端)

View File

@@ -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,13 +49,44 @@ 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="user">
<span>{{ auth.user?.username }}</span>
<el-tag size="small" :type="auth.isAdmin ? 'danger' : 'info'">
{{ auth.isAdmin ? '管理员' : '普通用户' }}
</el-tag>
<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
@@ -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>

View File

@@ -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>

View File

@@ -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,23 +53,25 @@ 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>
<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>
<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="昨收" />
<el-table-column prop="oi" label="持仓" />
<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>

View 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 }
}

View File

@@ -103,45 +103,47 @@ onMounted(reload)
</div>
</el-card>
<el-table :data="users" v-loading="loading" stripe>
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="username" label="用户名" />
<el-table-column prop="role" label="角色" width="100">
<template #default="{ row }">
<el-tag :type="row.role === 'admin' ? 'danger' : 'info'">{{ row.role }}</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.disabled ? 'warning' : 'success'">
{{ row.disabled ? '已禁用' : '正常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建于" width="180" />
<el-table-column prop="updated_at" label="更新于" width="180" />
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openReset(row)">重置密码</el-button>
<el-button
link
:type="row.disabled ? 'success' : 'warning'"
:disabled="row.id === auth.user?.id"
@click="toggleDisabled(row)"
>
{{ row.disabled ? '启用' : '禁用' }}
</el-button>
<el-button
link
type="danger"
:disabled="row.id === auth.user?.id"
@click="remove(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<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">
<template #default="{ row }">
<el-tag :type="row.role === 'admin' ? 'danger' : 'info'">{{ row.role }}</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.disabled ? 'warning' : 'success'">
{{ row.disabled ? '已禁用' : '正常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建于" width="180" />
<el-table-column prop="updated_at" label="更新于" width="180" />
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openReset(row)">重置密码</el-button>
<el-button
link
:type="row.disabled ? 'success' : 'warning'"
:disabled="row.id === auth.user?.id"
@click="toggleDisabled(row)"
>
{{ row.disabled ? '启用' : '禁用' }}
</el-button>
<el-button
link
type="danger"
:disabled="row.id === auth.user?.id"
@click="remove(row)"
>
删除
</el-button>
</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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,40 +122,42 @@ onMounted(async () => {
</el-form>
</el-card>
<el-table :data="rows" v-loading="loading" stripe class="score-table">
<el-table-column prop="trade_date" label="日期" width="100" />
<el-table-column label="品种" width="80">
<template #default="{ row }">
{{ parseTsCode(row.ts_code).symbol }}
</template>
</el-table-column>
<el-table-column label="合约" width="80">
<template #default="{ row }">
{{ parseTsCode(row.ts_code).contract }}
</template>
</el-table-column>
<el-table-column prop="close" label="收盘" width="90" />
<el-table-column prop="oi" label="持仓" width="100" />
<el-table-column prop="oi_chg" label="持仓变化" width="100" />
<el-table-column prop="short_term" label="短期(7d)" width="90" />
<el-table-column prop="medium_term" label="期(15d)" width="90" />
<el-table-column prop="long_term" label="期(30d)" width="90" />
<el-table-column prop="composite" label="综合" width="80">
<template #default="{ row }">
<strong>{{ row.composite.toFixed(2) }}</strong>
</template>
</el-table-column>
<el-table-column prop="signal" label="信号" min-width="160">
<template #default="{ row }">
<el-tag :type="signalTagType(row.signal)">{{ row.signal }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="drawerScoreId = row.id">明细</el-button>
</template>
</el-table-column>
</el-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 }">
{{ parseTsCode(row.ts_code).symbol }}
</template>
</el-table-column>
<el-table-column label="合约" width="80">
<template #default="{ row }">
{{ parseTsCode(row.ts_code).contract }}
</template>
</el-table-column>
<el-table-column prop="close" label="收盘" width="90" />
<el-table-column prop="oi" label="持仓" width="100" />
<el-table-column prop="oi_chg" label="持仓变化" width="100" />
<el-table-column prop="short_term" label="期(7d)" width="90" />
<el-table-column prop="medium_term" label="期(15d)" width="90" />
<el-table-column prop="long_term" label="长期(30d)" width="90" />
<el-table-column prop="composite" label="综合" width="80">
<template #default="{ row }">
<strong>{{ row.composite.toFixed(2) }}</strong>
</template>
</el-table-column>
<el-table-column prop="signal" label="信号" min-width="160">
<template #default="{ row }">
<el-tag :type="signalTagType(row.signal)">{{ row.signal }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="drawerScoreId = row.id">明细</el-button>
</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>