Restyle the web UI in a light theme, step 1: theme, top nav, demo trading
Follow the Stitch "Daylight" design: light canvas, white panels with hairline borders, deep cyan primary, Inter for UI text and JetBrains Mono for numbers (bundled via @fontsource-variable). - Override Element Plus variables in one place and derive light/dark shades with color-mix; pages use the UnoCSS Tailwind palette. - Replace the sidebar with a horizontal top navigation bar. - Rebuild the demo trading page: status panel with metric tiles, positions table, and a three-column layout (symbols, plan card, open orders and timeline); restyle the analysis drawer. - Fix the stale risk wording in the trading capital dialog.
This commit is contained in:
1 parent
859408386c
commit
c018b3693a
10 files changed
+700
-202
No files matched your search
@@ -10,6 +10,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"@fontsource-variable/inter": "^5.3.0",
|
||||||
|
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||||
"axios": "^1.20.0",
|
"axios": "^1.20.0",
|
||||||
"dayjs": "^1.11.23",
|
"dayjs": "^1.11.23",
|
||||||
"element-plus": "^2.14.7",
|
"element-plus": "^2.14.7",
|
||||||
|
|||||||
Generated
+16
@@ -11,6 +11,12 @@ importers:
|
|||||||
'@element-plus/icons-vue':
|
'@element-plus/icons-vue':
|
||||||
specifier: ^2.3.2
|
specifier: ^2.3.2
|
||||||
version: 2.3.2([email protected]([email protected]))
|
version: 2.3.2([email protected]([email protected]))
|
||||||
|
'@fontsource-variable/inter':
|
||||||
|
specifier: ^5.3.0
|
||||||
|
version: 5.3.0
|
||||||
|
'@fontsource-variable/jetbrains-mono':
|
||||||
|
specifier: ^5.3.0
|
||||||
|
version: 5.3.0
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.20.0
|
specifier: ^1.20.0
|
||||||
version: 1.20.0([email protected])
|
version: 1.20.0([email protected])
|
||||||
@@ -303,6 +309,12 @@ packages:
|
|||||||
'@floating-ui/[email protected]':
|
'@floating-ui/[email protected]':
|
||||||
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
|
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
|
||||||
|
|
||||||
|
'@fontsource-variable/[email protected]':
|
||||||
|
resolution: {integrity: sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==}
|
||||||
|
|
||||||
|
'@fontsource-variable/[email protected]':
|
||||||
|
resolution: {integrity: sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==}
|
||||||
|
|
||||||
'@humanfs/[email protected]':
|
'@humanfs/[email protected]':
|
||||||
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
|
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
@@ -2889,6 +2901,10 @@ snapshots:
|
|||||||
|
|
||||||
'@floating-ui/[email protected]': {}
|
'@floating-ui/[email protected]': {}
|
||||||
|
|
||||||
|
'@fontsource-variable/[email protected]': {}
|
||||||
|
|
||||||
|
'@fontsource-variable/[email protected]': {}
|
||||||
|
|
||||||
'@humanfs/[email protected]':
|
'@humanfs/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanfs/types': 0.15.0
|
'@humanfs/types': 0.15.0
|
||||||
|
|||||||
+42
-19
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { SwitchButton, TrendCharts } from "@element-plus/icons-vue"
|
||||||
import { useUserStore } from "@/pinia/user"
|
import { useUserStore } from "@/pinia/user"
|
||||||
import { menuRoutes } from "@/router"
|
import { menuRoutes } from "@/router"
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
@@ -13,27 +13,50 @@ async function logout() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-container class="h-full">
|
<div class="h-full flex flex-col">
|
||||||
<el-aside width="200px" class="border-r border-r-solid border-[var(--el-border-color)]">
|
<header class="h-14 flex shrink-0 items-center gap-10 border-b border-b-slate-200 border-b-solid bg-white px-6 shadow-sm">
|
||||||
<div class="h-14 flex-center text-lg font-bold">
|
<div class="flex items-center gap-2 text-lg text-slate-900 font-bold">
|
||||||
|
<el-icon :size="22" color="var(--el-color-primary)">
|
||||||
|
<TrendCharts />
|
||||||
|
</el-icon>
|
||||||
AlphaPulse
|
AlphaPulse
|
||||||
</div>
|
</div>
|
||||||
<el-menu :default-active="route.path" router class="border-r-0!">
|
<nav class="h-full flex gap-7 text-sm">
|
||||||
<el-menu-item v-for="item in menuRoutes" :key="item.path" :index="item.path">
|
<RouterLink v-for="item in menuRoutes" :key="item.path" :to="item.path" class="nav-link" active-class="active">
|
||||||
{{ item.meta.title }}
|
{{ item.meta.title }}
|
||||||
</el-menu-item>
|
</RouterLink>
|
||||||
</el-menu>
|
</nav>
|
||||||
</el-aside>
|
<div class="flex-1" />
|
||||||
<el-container>
|
<div class="flex items-center gap-2 text-sm text-slate-700">
|
||||||
<el-header class="flex-y-center justify-end gap-3 border-b border-b-solid border-[var(--el-border-color)]">
|
<span class="h-8 w-8 flex-center border border-sky-200 rounded-full border-solid bg-sky-50 text-xs text-[var(--el-color-primary)] font-bold">
|
||||||
<span>{{ userStore.username }}</span>
|
{{ userStore.username?.slice(0, 2).toUpperCase() }}
|
||||||
<el-button link @click="logout">
|
</span>
|
||||||
退出
|
{{ userStore.username }}
|
||||||
|
</div>
|
||||||
|
<el-button link :icon="SwitchButton" @click="logout">
|
||||||
|
退出登录
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-header>
|
</header>
|
||||||
<el-main>
|
<main class="min-h-0 flex-1 overflow-auto p-4">
|
||||||
<router-view />
|
<router-view />
|
||||||
</el-main>
|
</main>
|
||||||
</el-container>
|
</div>
|
||||||
</el-container>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.nav-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.nav-link:hover {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
.nav-link.active {
|
||||||
|
border-bottom-color: var(--el-color-primary);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,6 +3,8 @@ import App from "@/App.vue"
|
|||||||
import { router } from "@/router"
|
import { router } from "@/router"
|
||||||
import "element-plus/dist/index.css"
|
import "element-plus/dist/index.css"
|
||||||
import "element-plus/theme-chalk/dark/css-vars.css"
|
import "element-plus/theme-chalk/dark/css-vars.css"
|
||||||
|
import "@fontsource-variable/inter"
|
||||||
|
import "@fontsource-variable/jetbrains-mono"
|
||||||
import "@/styles/index.css"
|
import "@/styles/index.css"
|
||||||
import "virtual:uno.css"
|
import "virtual:uno.css"
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ const toolResults = computed(() => {
|
|||||||
return results
|
return results
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const duration = computed(() => detail.value?.finished_at ? Math.round((new Date(detail.value.finished_at).getTime() - new Date(detail.value.started_at).getTime()) / 1000) : null)
|
||||||
|
const toolCallCount = computed(() => (detail.value?.transcript ?? []).reduce((n, m) => n + (m.role === "assistant" ? m.tool_calls.length : 0), 0))
|
||||||
|
|
||||||
function pretty(raw: string | undefined) {
|
function pretty(raw: string | undefined) {
|
||||||
if (raw === undefined)
|
if (raw === undefined)
|
||||||
return "无结果"
|
return "无结果"
|
||||||
@@ -38,31 +41,92 @@ function pretty(raw: string | undefined) {
|
|||||||
<template>
|
<template>
|
||||||
<el-drawer v-model="visible" :title="detail ? `分析 #${detail.id} · ${detail.symbol}` : '分析'" size="55%">
|
<el-drawer v-model="visible" :title="detail ? `分析 #${detail.id} · ${detail.symbol}` : '分析'" size="55%">
|
||||||
<div v-if="detail" class="flex flex-col gap-3 text-sm">
|
<div v-if="detail" class="flex flex-col gap-3 text-sm">
|
||||||
<div class="text-xs text-[var(--el-text-color-secondary)]">
|
<div class="grid grid-cols-4 gap-2">
|
||||||
{{ formatTime(detail.started_at) }} – {{ formatTime(detail.finished_at) }} · {{ detail.reasons.join(";") }}
|
<div class="meta">
|
||||||
· {{ detail.model }}{{ detail.reasoning_effort ? ` / ${detail.reasoning_effort}` : "" }}
|
<div class="meta-label">
|
||||||
· {{ detail.usage?.input_tokens.toLocaleString() }} / {{ detail.usage?.output_tokens.toLocaleString() }} token
|
分析 ID
|
||||||
{{ detail.dry_run ? "· 试运行" : "" }}
|
</div>
|
||||||
|
<div class="num text-slate-900">
|
||||||
|
#{{ detail.id }}{{ detail.dry_run ? " · 试运行" : "" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<div class="meta-label">
|
||||||
|
时间
|
||||||
|
</div>
|
||||||
|
<div class="num text-slate-900">
|
||||||
|
{{ formatTime(detail.started_at, "MM-DD HH:mm:ss") }}
|
||||||
|
</div>
|
||||||
|
<div class="num text-xs text-slate-500">
|
||||||
|
{{ duration === null ? "未结束" : `耗时 ${duration}s` }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<div class="meta-label">
|
||||||
|
token(入 / 出)
|
||||||
|
</div>
|
||||||
|
<div class="num text-slate-900">
|
||||||
|
{{ detail.usage?.input_tokens.toLocaleString() ?? "-" }} / {{ detail.usage?.output_tokens.toLocaleString() ?? "-" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<div class="meta-label">
|
||||||
|
模型 / 思考强度
|
||||||
|
</div>
|
||||||
|
<div class="num truncate text-slate-900" :title="detail.model ?? ''">
|
||||||
|
{{ detail.model ?? "-" }}
|
||||||
|
</div>
|
||||||
|
<div class="num text-xs text-slate-500">
|
||||||
|
{{ detail.reasoning_effort ?? "跟随配置" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-slate-500">
|
||||||
|
触发:{{ detail.reasons.join(";") }}
|
||||||
</div>
|
</div>
|
||||||
<el-alert v-if="detail.error" type="error" :closable="false" :title="detail.error" />
|
<el-alert v-if="detail.error" type="error" :closable="false" :title="detail.error" />
|
||||||
|
<el-card v-if="detail.text" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
模型分析
|
||||||
|
</template>
|
||||||
<!-- markdown-it 已关闭 html,模型输出中的标签按文本显示 -->
|
<!-- markdown-it 已关闭 html,模型输出中的标签按文本显示 -->
|
||||||
<div v-if="detail.text" class="markdown-body" v-html="renderMarkdown(detail.text)" />
|
<div class="markdown-body" v-html="renderMarkdown(detail.text)" />
|
||||||
|
</el-card>
|
||||||
|
<el-card shadow="never" body-class="py-0">
|
||||||
|
<template #header>
|
||||||
|
发给模型的数据与工具调用({{ toolCallCount }})
|
||||||
|
</template>
|
||||||
<el-collapse>
|
<el-collapse>
|
||||||
<template v-for="(m, i) in detail.transcript ?? []" :key="i">
|
<template v-for="(m, i) in detail.transcript ?? []" :key="i">
|
||||||
<el-collapse-item v-if="m.role === 'user'" :name="`u${i}`" :title="i === 0 ? '发给模型的数据(账户、持仓、预取行情)' : '程序提示'">
|
<el-collapse-item v-if="m.role === 'user'" :name="`u${i}`" :title="i === 0 ? '发给模型的数据(账户、持仓、预取行情)' : '程序提示'">
|
||||||
<pre class="m-0 max-h-120 overflow-auto whitespace-pre-wrap break-all text-xs">{{ m.text }}</pre>
|
<pre class="m-0 max-h-120 overflow-auto whitespace-pre-wrap break-all rounded bg-slate-50 p-2 text-xs">{{ m.text }}</pre>
|
||||||
</el-collapse-item>
|
</el-collapse-item>
|
||||||
<template v-else-if="m.role === 'assistant'">
|
<template v-else-if="m.role === 'assistant'">
|
||||||
<el-collapse-item v-for="call in m.tool_calls" :key="call.id" :name="call.id">
|
<el-collapse-item v-for="call in m.tool_calls" :key="call.id" :name="call.id">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="inline-block max-w-[90%] truncate text-xs font-mono">{{ call.name }} {{ JSON.stringify(call.arguments) }}</span>
|
<span class="inline-block max-w-[90%] truncate text-xs font-mono">{{ call.name }} {{ JSON.stringify(call.arguments) }}</span>
|
||||||
</template>
|
</template>
|
||||||
<pre class="m-0 max-h-120 overflow-auto whitespace-pre-wrap break-all text-xs">{{ pretty(toolResults.get(call.id)) }}</pre>
|
<pre class="m-0 max-h-120 overflow-auto whitespace-pre-wrap break-all rounded bg-slate-50 p-2 text-xs">{{ pretty(toolResults.get(call.id)) }}</pre>
|
||||||
</el-collapse-item>
|
</el-collapse-item>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-collapse>
|
</el-collapse>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
<div v-else v-loading="true" class="h-40" />
|
<div v-else v-loading="true" class="h-40" />
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.meta {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
.meta-label {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Analysis, OrderRow, PlanCondition, TradingSymbol } from "@/apis/trading"
|
import type { Analysis, OrderRow, PlanCondition, TradingSymbol } from "@/apis/trading"
|
||||||
|
import { Document, Lightning } from "@element-plus/icons-vue"
|
||||||
import { CLOSE_REASON_LABELS, PLAN_STATUS_LABELS, TAKE_PROFIT_STEPS } from "@/apis/trading"
|
import { CLOSE_REASON_LABELS, PLAN_STATUS_LABELS, TAKE_PROFIT_STEPS } from "@/apis/trading"
|
||||||
import { formatTime } from "@/utils/text"
|
import { formatTime } from "@/utils/text"
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ const decision = computed(() => props.analysis?.decision ?? null)
|
|||||||
const sizing = computed(() => props.analysis?.sizing ?? null)
|
const sizing = computed(() => props.analysis?.sizing ?? null)
|
||||||
const busy = computed(() => props.requesting || props.symbol.analyzing || !!props.symbol.analysis_requested_at)
|
const busy = computed(() => props.requesting || props.symbol.analyzing || !!props.symbol.analysis_requested_at)
|
||||||
const elapsed = computed(() => props.analysis ? Math.max(0, Math.round((now.value - new Date(props.analysis.started_at).getTime()) / 1000)) : 0)
|
const elapsed = computed(() => props.analysis ? Math.max(0, Math.round((now.value - new Date(props.analysis.started_at).getTime()) / 1000)) : 0)
|
||||||
|
const duration = computed(() => props.analysis?.finished_at ? Math.round((new Date(props.analysis.finished_at).getTime() - new Date(props.analysis.started_at).getTime()) / 1000) : null)
|
||||||
|
|
||||||
function filled(purpose: "entry" | "tp", leg: number) {
|
function filled(purpose: "entry" | "tp", leg: number) {
|
||||||
const total = props.orders.filter(o => o.purpose === purpose && o.leg === leg).reduce((sum, o) => sum + Number(o.filled_qty), 0)
|
const total = props.orders.filter(o => o.purpose === purpose && o.leg === leg).reduce((sum, o) => sum + Number(o.filled_qty), 0)
|
||||||
@@ -49,43 +51,62 @@ const ladder = computed(() => sizing.value?.entry && entryRow.value
|
|||||||
move: `${long.value ? "+" : "−"}${pct * 100}%`,
|
move: `${long.value ? "+" : "−"}${pct * 100}%`,
|
||||||
ratio: `${Math.round(ratio * 100)}%`,
|
ratio: `${Math.round(ratio * 100)}%`,
|
||||||
price: props.orders.filter(o => o.purpose === "tp" && o.leg === leg).at(-1)?.price ?? "-",
|
price: props.orders.filter(o => o.purpose === "tp" && o.leg === leg).at(-1)?.price ?? "-",
|
||||||
filled: filled("tp", leg)
|
filled: filled("tp", leg),
|
||||||
|
active: ACTIVE_ORDER_STATUSES.includes(props.orders.filter(o => o.purpose === "tp" && o.leg === leg).at(-1)?.status ?? "")
|
||||||
}))
|
}))
|
||||||
: [])
|
: [])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="border border-[var(--el-border-color)] rounded border-solid p-3">
|
<el-card shadow="never" body-class="flex flex-col gap-3 text-sm">
|
||||||
<div class="mb-2 flex items-center gap-2">
|
<template #header>
|
||||||
<span class="font-bold">{{ symbol.symbol }}</span>
|
<div class="flex items-center gap-3">
|
||||||
<span class="text-xs text-[var(--el-text-color-secondary)]">短线做波动(区间内挂限价单,最长持仓 4 小时)</span>
|
<span class="num h-9 w-9 flex-center border border-sky-200 rounded-lg border-solid bg-sky-50 text-base text-[var(--el-color-primary)]">
|
||||||
|
{{ symbol.symbol.slice(0, 1) }}
|
||||||
|
</span>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="flex items-center gap-2 text-base text-slate-900">
|
||||||
|
<span class="num">{{ symbol.symbol }}</span>交易计划
|
||||||
|
<el-tag v-if="analysis?.plan_status" type="success" size="small">
|
||||||
|
{{ PLAN_STATUS_LABELS[analysis.plan_status] }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-slate-500 font-normal">
|
||||||
|
短线做波动(区间内挂限价单,最长持仓 4 小时)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<el-button v-if="analysis && analysis.status !== 'running'" link type="primary" @click="emit('detail', analysis.id)">
|
<el-button v-if="analysis && analysis.status !== 'running'" :icon="Document" @click="emit('detail', analysis.id)">
|
||||||
查看完整分析
|
查看完整分析
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="small" type="primary" :loading="busy" @click="emit('analyze')">
|
<el-button type="primary" :icon="Lightning" :loading="busy" @click="emit('analyze')">
|
||||||
{{ busy ? "分析中" : "立即分析" }}
|
{{ busy ? "分析中" : "立即分析" }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div v-if="!analysis" class="text-sm text-[var(--el-text-color-secondary)]">
|
<div v-if="!analysis" class="border border-slate-200 rounded-lg border-solid bg-slate-50 px-3 py-2 text-slate-500">
|
||||||
还没有分析。引擎停止时点「立即分析」为试运行:计划只记录,不下单。
|
还没有分析。引擎停止时点「立即分析」为试运行:计划只记录,不下单。
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="analysis.status === 'running'" class="text-sm text-[var(--el-text-color-secondary)]">
|
<div v-else-if="analysis.status === 'running'" class="flex items-center gap-2 border border-amber-200 rounded-lg border-solid bg-amber-50 px-3 py-2 text-amber-700">
|
||||||
|
<span class="h-2 w-2 animate-pulse rounded-full bg-amber-500" />
|
||||||
模型分析中,已用 {{ elapsed }} 秒(通常 1–3 分钟)…
|
模型分析中,已用 {{ elapsed }} 秒(通常 1–3 分钟)…
|
||||||
</div>
|
</div>
|
||||||
<el-alert v-else-if="analysis.status === 'failed'" type="error" :closable="false" show-icon>
|
<el-alert v-else-if="analysis.status === 'failed'" type="error" :closable="false" show-icon>
|
||||||
{{ formatTime(analysis.started_at) }} 分析失败:{{ analysis.error }}
|
{{ formatTime(analysis.started_at) }} 分析失败:{{ analysis.error }}
|
||||||
</el-alert>
|
</el-alert>
|
||||||
|
|
||||||
<div v-else-if="decision" class="flex flex-col gap-2 text-sm">
|
<template v-else-if="decision">
|
||||||
|
<!-- 分析概要与结论 -->
|
||||||
|
<div class="flex flex-col gap-2 border border-sky-100 rounded-lg border-solid bg-sky-50/50 p-3">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<span class="flex items-center gap-1 text-emerald-700 font-semibold">
|
||||||
|
<span class="h-2 w-2 rounded-full bg-emerald-500" />
|
||||||
|
分析完成<template v-if="duration !== null">(耗时 {{ duration }}s)</template>
|
||||||
|
</span>
|
||||||
<el-tag v-if="analysis.dry_run" type="info" size="small">
|
<el-tag v-if="analysis.dry_run" type="info" size="small">
|
||||||
试运行
|
试运行
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-tag v-if="analysis.plan_status" type="success" effect="dark" size="small">
|
|
||||||
{{ PLAN_STATUS_LABELS[analysis.plan_status] }}
|
|
||||||
</el-tag>
|
|
||||||
<el-tag size="small">
|
<el-tag size="small">
|
||||||
{{ ACTION_LABELS[decision.action] }}
|
{{ ACTION_LABELS[decision.action] }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
@@ -95,15 +116,17 @@ const ladder = computed(() => sizing.value?.entry && entryRow.value
|
|||||||
<el-tag v-if="decision.confidence" type="warning" size="small">
|
<el-tag v-if="decision.confidence" type="warning" size="small">
|
||||||
置信度{{ CONFIDENCE_LABELS[decision.confidence] }}
|
置信度{{ CONFIDENCE_LABELS[decision.confidence] }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span class="text-xs text-[var(--el-text-color-secondary)]">
|
<span class="num ml-auto text-xs text-slate-500">{{ formatTime(analysis.started_at) }}</span>
|
||||||
{{ formatTime(analysis.started_at) }} · {{ analysis.reasons.join(";") }} · {{ analysis.model }}{{ analysis.reasoning_effort ? ` / ${analysis.reasoning_effort}` : "" }}
|
|
||||||
· {{ analysis.usage?.input_tokens.toLocaleString() }} / {{ analysis.usage?.output_tokens.toLocaleString() }} token
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="font-bold">
|
<div class="border-l-4 border-l-[var(--el-color-primary)] rounded-md border-l-solid bg-white px-3 py-2 text-slate-900 leading-relaxed">
|
||||||
{{ decision.summary }}
|
<span class="mr-2 text-[var(--el-color-primary)] font-semibold">结论</span>{{ decision.summary }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="latest && latest.id !== analysis.id" class="text-xs text-[var(--el-text-color-secondary)]">
|
<div class="num text-xs text-slate-500">
|
||||||
|
触发:{{ analysis.reasons.join(";") }} · {{ analysis.model }}{{ analysis.reasoning_effort ? ` / ${analysis.reasoning_effort}` : "" }}
|
||||||
|
· token {{ analysis.usage?.input_tokens.toLocaleString() }} / {{ analysis.usage?.output_tokens.toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="latest && latest.id !== analysis.id" class="text-xs text-slate-500">
|
||||||
最近一次分析 #{{ latest.id }}({{ formatTime(latest.started_at) }}):
|
最近一次分析 #{{ latest.id }}({{ formatTime(latest.started_at) }}):
|
||||||
<template v-if="latest.status === 'done'">
|
<template v-if="latest.status === 'done'">
|
||||||
{{ latest.decision?.summary }}
|
{{ latest.decision?.summary }}
|
||||||
@@ -115,55 +138,145 @@ const ladder = computed(() => sizing.value?.entry && entryRow.value
|
|||||||
查看
|
查看
|
||||||
</el-link>
|
</el-link>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="analysis.valid_until || analysis.close_deadline" class="flex flex-wrap gap-x-6">
|
|
||||||
<span v-if="analysis.plan_status === 'pending' && analysis.valid_until"><span class="label">限价单有效至</span>{{ formatTime(analysis.valid_until, "MM-DD HH:mm") }}(剩 {{ countdown(analysis.valid_until) }})</span>
|
|
||||||
<span v-if="analysis.first_fill_at"><span class="label">入场</span>{{ formatTime(analysis.first_fill_at, "MM-DD HH:mm") }}</span>
|
|
||||||
<span v-if="analysis.close_deadline"><span class="label">最晚平仓</span>{{ formatTime(analysis.close_deadline, "MM-DD HH:mm") }}(剩 {{ countdown(analysis.close_deadline) }})</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="analysis.result">
|
|
||||||
<span class="label">结果</span>{{ CLOSE_REASON_LABELS[analysis.close_reason ?? ""] ?? analysis.close_reason }},净盈亏 {{ analysis.result.net }} USDT(手续费 {{ analysis.result.commission }})
|
|
||||||
</div>
|
|
||||||
<div><span class="label">为什么</span>{{ decision.reasons }}</div>
|
|
||||||
|
|
||||||
|
<!-- 时间 -->
|
||||||
|
<div v-if="analysis.valid_until || analysis.close_deadline" class="grid grid-cols-3 gap-2">
|
||||||
|
<div class="tile">
|
||||||
|
<div class="label">
|
||||||
|
限价单有效至
|
||||||
|
</div>
|
||||||
|
<template v-if="analysis.plan_status === 'pending' && analysis.valid_until">
|
||||||
|
<div class="num text-slate-900">
|
||||||
|
{{ formatTime(analysis.valid_until, "MM-DD HH:mm") }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-[var(--el-color-primary)]">
|
||||||
|
剩 {{ countdown(analysis.valid_until) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="text-slate-400">
|
||||||
|
-
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile">
|
||||||
|
<div class="label">
|
||||||
|
入场
|
||||||
|
</div>
|
||||||
|
<div class="num text-slate-900">
|
||||||
|
{{ formatTime(analysis.first_fill_at, "MM-DD HH:mm") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile">
|
||||||
|
<div class="label">
|
||||||
|
最晚平仓
|
||||||
|
</div>
|
||||||
|
<template v-if="analysis.close_deadline">
|
||||||
|
<div class="num text-slate-900">
|
||||||
|
{{ formatTime(analysis.close_deadline, "MM-DD HH:mm") }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-amber-600">
|
||||||
|
剩 {{ countdown(analysis.close_deadline) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="text-slate-400">
|
||||||
|
-
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="analysis.result" class="border rounded-lg border-solid px-3 py-2"
|
||||||
|
:class="Number(analysis.result.net) >= 0 ? 'border-emerald-200 bg-emerald-50' : 'border-rose-200 bg-rose-50'"
|
||||||
|
>
|
||||||
|
<span class="label">结果</span>{{ CLOSE_REASON_LABELS[analysis.close_reason ?? ""] ?? analysis.close_reason }},净盈亏
|
||||||
|
<b class="num" :class="Number(analysis.result.net) >= 0 ? 'text-emerald-700' : 'text-rose-700'">{{ analysis.result.net }}</b> USDT(手续费 {{ analysis.result.commission }})
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="label mb-1">
|
||||||
|
为什么
|
||||||
|
</div>
|
||||||
|
<div class="text-slate-700 leading-relaxed">
|
||||||
|
{{ decision.reasons }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 价格与风险 -->
|
||||||
<template v-if="decision.action === 'plan' && decision.setup && sizing?.entry">
|
<template v-if="decision.action === 'plan' && decision.setup && sizing?.entry">
|
||||||
<div class="grid grid-cols-2 gap-x-6 gap-y-1">
|
<div class="flex flex-col gap-2 border border-slate-200 rounded-lg border-solid p-3">
|
||||||
<div>
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span class="label">{{ long ? "支撑区" : "压力区" }}</span>{{ sizing.setup.zone_low }} – {{ sizing.setup.zone_high }} · {{ decision.setup.basis }}
|
<span class="label">{{ long ? "支撑区" : "压力区" }}</span>
|
||||||
|
<span
|
||||||
|
class="num border rounded border-solid px-2 py-0.5"
|
||||||
|
:class="long ? 'border-emerald-200 bg-emerald-50 text-emerald-700' : 'border-rose-200 bg-rose-50 text-rose-700'"
|
||||||
|
>{{ sizing.setup.zone_low }} – {{ sizing.setup.zone_high }}</span>
|
||||||
|
<span class="text-xs text-slate-500">{{ decision.setup.basis }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="kv">
|
||||||
<span class="label">限价{{ long ? "做多" : "做空" }}</span>{{ sizing.entry.price }} × {{ sizing.entry.quantity }},已成交 {{ filled("entry", 0) }}{{ entryRow?.avg_price ? `,均价 ${entryRow.avg_price}` : "" }}
|
<span class="label">限价{{ long ? "做多" : "做空" }}</span>
|
||||||
(名义 {{ sizing.entry.notional }} USDT,{{ sizing.entry.leverage }} 倍交易资金)
|
<span><span class="num text-slate-900">{{ sizing.entry.price }} × {{ sizing.entry.quantity }}</span>
|
||||||
|
<span class="ml-2 text-xs text-slate-500">已成交 {{ filled("entry", 0) }}{{ entryRow?.avg_price ? `,均价 ${entryRow.avg_price}` : "" }}</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="kv">
|
||||||
<span class="label">止损</span>{{ currentStop ?? sizing.entry.stop }}(初始 {{ sizing.entry.stop }},距挂单价 {{ sizing.entry.stop_pct }}%;区间远端外 0.5×ATR(5m)、距挂单价至少 1×ATR(5m),止盈 1 后移到保本价)
|
<span class="label">名义价值</span>
|
||||||
|
<span><span class="num text-slate-900">{{ sizing.entry.notional }} USDT</span>
|
||||||
|
<span class="ml-2 text-xs text-slate-500">{{ sizing.entry.leverage }} 倍交易资金</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="kv">
|
||||||
<span class="label">止损亏损</span>{{ sizing.entry.risk_at_stop }} USDT(交易资金的 {{ sizing.entry.risk_pct }}%){{ sizing.entry.capped_by_leverage ? ",受 3 倍上限限制" : "" }}
|
<span class="label">止损</span>
|
||||||
|
<span><span class="num text-rose-600 font-semibold">{{ currentStop ?? sizing.entry.stop }}</span>
|
||||||
|
<span class="ml-2 text-xs text-slate-500">初始 {{ sizing.entry.stop }},距挂单价 {{ sizing.entry.stop_pct }}%;区间远端外 0.5×ATR(5m)、距挂单价至少 1×ATR(5m),止盈 1 后移到保本价</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="kv">
|
||||||
|
<span class="label">止损亏损</span>
|
||||||
|
<span><span class="num text-rose-600">{{ sizing.entry.risk_at_stop }} USDT</span>
|
||||||
|
<span class="ml-2 text-xs text-slate-500">交易资金的 {{ sizing.entry.risk_pct }}%{{ sizing.entry.capped_by_leverage ? ",受 3 倍上限限制" : "" }}</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-table v-if="ladder.length" :data="ladder" border size="small">
|
<div v-if="ladder.length">
|
||||||
<el-table-column label="分批止盈" min-width="90">
|
<div class="label mb-1">
|
||||||
|
分批止盈
|
||||||
|
</div>
|
||||||
|
<el-table :data="ladder" size="small" cell-class-name="num" class="border border-slate-200 rounded-lg border-solid">
|
||||||
|
<el-table-column label="档位" min-width="70">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
止盈 {{ row.leg + 1 }}
|
<span class="text-[var(--el-color-primary)]">止盈 {{ row.leg + 1 }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="move" label="距成交均价" min-width="90" />
|
<el-table-column prop="move" label="距成交均价" min-width="90" />
|
||||||
<el-table-column prop="ratio" label="平仓比例" min-width="80" />
|
<el-table-column prop="ratio" label="平仓比例" min-width="80" />
|
||||||
<el-table-column prop="price" label="价格" min-width="100" />
|
<el-table-column prop="price" label="价格" min-width="100" />
|
||||||
<el-table-column prop="filled" label="已成交" min-width="80" />
|
<el-table-column label="状态" min-width="120" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.filled !== '-'" type="success" size="small">
|
||||||
|
已成交 {{ row.filled }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-else-if="row.active" size="small">
|
||||||
|
挂单中
|
||||||
|
</el-tag>
|
||||||
|
<span v-else class="text-slate-400">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="decision.reevaluate.length">
|
<div v-if="decision.reevaluate.length" class="border border-amber-200 rounded-lg border-solid bg-amber-50 px-3 py-2">
|
||||||
<span class="label">重新判断</span>
|
<div class="mb-1 text-amber-800 font-semibold">
|
||||||
<div v-for="(c, i) in decision.reevaluate" :key="i" class="ml-2">
|
重新判断
|
||||||
|
</div>
|
||||||
|
<div v-for="(c, i) in decision.reevaluate" :key="i" class="text-amber-900/80">
|
||||||
· {{ condition(c) }}:{{ c.basis }}
|
· {{ condition(c) }}:{{ c.basis }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div><span class="label">风险</span>{{ decision.risks }}</div>
|
<div class="border border-rose-200 rounded-lg border-solid bg-rose-50 px-3 py-2">
|
||||||
|
<div class="mb-1 text-rose-700 font-semibold">
|
||||||
|
风险
|
||||||
|
</div>
|
||||||
|
<div class="text-rose-900/80">
|
||||||
|
{{ decision.risks }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.label {
|
.label {
|
||||||
@@ -171,4 +284,18 @@ const ladder = computed(() => sizing.value?.entry && entryRow.value
|
|||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.tile {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
.kv {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
.kv > .label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 64px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
+245
-119
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Effort, Provider } from "@/apis/settings"
|
import type { Effort, Provider } from "@/apis/settings"
|
||||||
import type { Analysis, OrderRow, SnapshotPosition, TradingEvent, TradingStatus, TradingSymbol } from "@/apis/trading"
|
import type { Analysis, OrderRow, SnapshotPosition, TradingEvent, TradingStatus, TradingSymbol } from "@/apis/trading"
|
||||||
|
import { Edit, Plus } from "@element-plus/icons-vue"
|
||||||
import { EFFORTS, listProviders } from "@/apis/settings"
|
import { EFFORTS, listProviders } from "@/apis/settings"
|
||||||
import { deleteSymbol, getStatus, getSymbolPlan, isOwnOrder, listAnalyses, listEvents, listSymbols, patchConfig, requestAnalysis, startTrading, stopTrading } from "@/apis/trading"
|
import { deleteSymbol, getStatus, getSymbolPlan, isOwnOrder, listAnalyses, listEvents, listSymbols, patchConfig, requestAnalysis, startTrading, stopTrading } from "@/apis/trading"
|
||||||
import { usePersisted } from "@/utils/storage"
|
import { usePersisted } from "@/utils/storage"
|
||||||
@@ -32,6 +33,11 @@ const snapshot = computed(() => status.value?.snapshot ?? null)
|
|||||||
const account = computed(() => snapshot.value?.account ?? null)
|
const account = computed(() => snapshot.value?.account ?? null)
|
||||||
const positions = computed(() => snapshot.value?.positions ?? [])
|
const positions = computed(() => snapshot.value?.positions ?? [])
|
||||||
const current = computed(() => symbols.value.find(s => s.symbol === selected.value))
|
const current = computed(() => symbols.value.find(s => s.symbol === selected.value))
|
||||||
|
const engineText = computed(() => snapshot.value ? `在线 · ${status.value!.running ? "正在交易" : "已停止"}` : "离线")
|
||||||
|
const capitalShort = computed(() => {
|
||||||
|
const s = status.value!
|
||||||
|
return s.effective_capital && s.capital_usdt && Number(s.effective_capital) < Number(s.capital_usdt)
|
||||||
|
})
|
||||||
const symbolOrders = computed(() => {
|
const symbolOrders = computed(() => {
|
||||||
const s = snapshot.value
|
const s = snapshot.value
|
||||||
if (!s)
|
if (!s)
|
||||||
@@ -79,12 +85,16 @@ function exitPrices(p: SnapshotPosition) {
|
|||||||
return { stop: nearest(stops), target: nearest(targets) }
|
return { stop: nearest(stops), target: nearest(targets) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function positionOf(symbol: string) {
|
||||||
|
return positions.value.find(p => p.symbol === symbol)
|
||||||
|
}
|
||||||
|
|
||||||
function symbolState(s: TradingSymbol) {
|
function symbolState(s: TradingSymbol) {
|
||||||
if (s.analyzing)
|
if (s.analyzing)
|
||||||
return { label: "分析中", type: "warning" } as const
|
return { label: "分析中", type: "warning" } as const
|
||||||
if (s.plan_status === "closing")
|
if (s.plan_status === "closing")
|
||||||
return { label: "平仓中", type: "danger" } as const
|
return { label: "平仓中", type: "danger" } as const
|
||||||
if (positions.value.some(p => p.symbol === s.symbol))
|
if (positionOf(s.symbol))
|
||||||
return { label: "持仓中", type: "success" } as const
|
return { label: "持仓中", type: "success" } as const
|
||||||
if (s.plan_status === "pending")
|
if (s.plan_status === "pending")
|
||||||
return { label: "挂单中", type: "primary" } as const
|
return { label: "挂单中", type: "primary" } as const
|
||||||
@@ -192,7 +202,7 @@ async function toggleRunning() {
|
|||||||
|
|
||||||
async function editCapital() {
|
async function editCapital() {
|
||||||
const { value } = await ElMessageBox.prompt(
|
const { value } = await ElMessageBox.prompt(
|
||||||
`仓位按交易资金计算:单笔风险短线 1%、长线 2%,所有仓位名义价值合计不超过 3 倍(逐仓 3 倍时保证金合计不超过交易资金)。不能超过账户权益 ${money(account.value?.margin_balance)} USDT;账户权益低于它时按权益计算。`,
|
`仓位按交易资金计算:单笔风险 1%(置信度低时 0.5%),所有仓位名义价值合计不超过 3 倍(逐仓 3 倍时保证金合计不超过交易资金)。不能超过账户权益 ${money(account.value?.margin_balance)} USDT;账户权益低于它时按权益计算。`,
|
||||||
"设置交易资金(USDT)",
|
"设置交易资金(USDT)",
|
||||||
{
|
{
|
||||||
inputValue: status.value!.capital_usdt ? num(status.value!.capital_usdt) : "",
|
inputValue: status.value!.capital_usdt ? num(status.value!.capital_usdt) : "",
|
||||||
@@ -237,68 +247,113 @@ onBeforeUnmount(() => clearInterval(timer))
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="status" class="h-full flex flex-col gap-3">
|
<div v-if="status" class="flex flex-col gap-4">
|
||||||
<!-- 顶部:引擎状态、启停、账户 -->
|
<!-- 顶部:引擎状态、启停、账户 -->
|
||||||
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 border border-[var(--el-border-color)] rounded border-solid px-4 py-3">
|
<el-card shadow="never" body-class="flex flex-wrap items-center gap-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<el-tag v-if="snapshot" type="success" effect="dark">
|
<div class="flex items-center gap-3 border border-slate-200 rounded-lg border-solid bg-slate-50 px-3 py-2">
|
||||||
引擎在线
|
<span class="h-3 w-3 rounded-full" :class="snapshot ? 'bg-emerald-500 pulse' : 'bg-slate-300'" />
|
||||||
</el-tag>
|
<div>
|
||||||
<el-tag v-else type="info" effect="dark">
|
<div class="text-xs text-slate-500">
|
||||||
引擎离线
|
交易引擎
|
||||||
</el-tag>
|
</div>
|
||||||
<el-tag :type="status.running ? 'success' : 'info'">
|
<div class="text-sm font-semibold" :class="snapshot ? 'text-emerald-700' : 'text-slate-500'">
|
||||||
{{ status.running ? "交易中" : "已停止" }}
|
{{ engineText }}
|
||||||
</el-tag>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<el-button
|
<el-button
|
||||||
:type="status.running ? 'danger' : 'primary'" :loading="toggling"
|
:type="status.running ? 'danger' : 'primary'" :plain="status.running" :loading="toggling" size="small"
|
||||||
:disabled="!status.running && (!status.configured || !status.capital_usdt)"
|
:disabled="!status.running && (!status.configured || !status.capital_usdt)"
|
||||||
@click="toggleRunning"
|
@click="toggleRunning"
|
||||||
>
|
>
|
||||||
{{ status.running ? "停止" : "开始行情交易" }}
|
{{ status.running ? "停止行情交易" : "开始行情交易" }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
|
||||||
<span>交易资金</span>
|
<div class="h-12 w-px bg-slate-200" />
|
||||||
<template v-if="status.capital_usdt">
|
|
||||||
{{ money(status.capital_usdt) }} USDT
|
<div class="metric">
|
||||||
<span v-if="status.effective_capital && Number(status.effective_capital) < Number(status.capital_usdt)" class="text-[var(--el-color-warning)]!">
|
<div class="metric-label">
|
||||||
(权益不足,按 {{ money(status.effective_capital) }})
|
交易资金
|
||||||
</span>
|
<el-icon class="cursor-pointer text-[var(--el-color-primary)]" title="设置交易资金" @click="editCapital">
|
||||||
</template>
|
<Edit />
|
||||||
<b v-else class="text-[var(--el-color-danger)]">未设置</b>
|
</el-icon>
|
||||||
<el-link type="primary" underline="never" class="ml-1" @click="editCapital">
|
|
||||||
设置
|
|
||||||
</el-link>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div v-if="status.capital_usdt" class="metric-value">
|
||||||
<span>账户权益</span>{{ money(account?.margin_balance) }} USDT
|
{{ money(status.capital_usdt) }}<small>USDT</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div v-else class="metric-value text-rose-600!">
|
||||||
<span>可用</span>{{ money(account?.available_balance) }} USDT
|
未设置
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div v-if="capitalShort" class="text-xs text-amber-600">
|
||||||
<span>未实现盈亏</span>
|
权益不足,按 {{ money(status.effective_capital!) }}
|
||||||
<b :class="pnlClass(account?.unrealized_pnl ?? '0')">{{ money(account?.unrealized_pnl) }}</b>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
|
||||||
<span>今日分析</span>{{ status.today.analyses }} / {{ status.today.limit }}
|
|
||||||
<span class="ml-1">token</span>{{ status.today.input_tokens.toLocaleString() }} / {{ status.today.output_tokens.toLocaleString() }}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<div class="metric-label">
|
||||||
|
账户权益
|
||||||
|
</div>
|
||||||
|
<div class="metric-value">
|
||||||
|
{{ money(account?.margin_balance) }}<small>USDT</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<div class="metric-label">
|
||||||
|
可用余额
|
||||||
|
</div>
|
||||||
|
<div class="metric-value">
|
||||||
|
{{ money(account?.available_balance) }}<small>USDT</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="metric"
|
||||||
|
:class="{ 'bg-emerald-50! border-emerald-200!': Number(account?.unrealized_pnl) > 0, 'bg-rose-50! border-rose-200!': Number(account?.unrealized_pnl) < 0 }"
|
||||||
|
>
|
||||||
|
<div class="metric-label">
|
||||||
|
未实现盈亏
|
||||||
|
</div>
|
||||||
|
<div class="metric-value" :class="pnlClass(account?.unrealized_pnl ?? '0')">
|
||||||
|
{{ money(account?.unrealized_pnl) }}<small>USDT</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-12 w-px bg-slate-200" />
|
||||||
|
|
||||||
|
<div class="w-56">
|
||||||
|
<div class="flex items-center justify-between text-xs text-slate-500">
|
||||||
|
今日分析
|
||||||
|
<span class="num text-sm text-slate-900 font-semibold">{{ status.today.analyses }} / {{ status.today.limit }}</span>
|
||||||
|
</div>
|
||||||
|
<el-progress :percentage="Math.min(100, status.today.analyses / status.today.limit * 100)" :show-text="false" :stroke-width="4" class="my-1" />
|
||||||
|
<div class="num text-xs text-slate-500">
|
||||||
|
token 入 {{ status.today.input_tokens.toLocaleString() }} / 出 {{ status.today.output_tokens.toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 text-xs text-slate-500">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="w-14">模型</span>
|
||||||
<el-select
|
<el-select
|
||||||
:model-value="status.provider_id" class="w-56" clearable :value-on-clear="null" placeholder="模型:跟随默认配置"
|
:model-value="status.provider_id" size="small" class="w-44" clearable :value-on-clear="null" placeholder="跟随默认配置"
|
||||||
@update:model-value="changeProvider"
|
@update:model-value="changeProvider"
|
||||||
>
|
>
|
||||||
<el-option v-for="p in providers" :key="p.id" :value="p.id" :label="`${p.name}(${p.model})`" />
|
<el-option v-for="p in providers" :key="p.id" :value="p.id" :label="`${p.name}(${p.model})`" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="w-14">思考强度</span>
|
||||||
<el-select
|
<el-select
|
||||||
:model-value="status.reasoning_effort" class="w-40" clearable :value-on-clear="null" placeholder="思考强度:跟随配置"
|
:model-value="status.reasoning_effort" size="small" class="w-44" clearable :value-on-clear="null" placeholder="跟随配置"
|
||||||
@update:model-value="changeEffort"
|
@update:model-value="changeEffort"
|
||||||
>
|
>
|
||||||
<el-option v-for="e in EFFORTS" :key="e" :value="e" :label="e" />
|
<el-option v-for="e in EFFORTS" :key="e" :value="e" :label="e" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<el-alert v-if="!status.configured" type="warning" :closable="false" show-icon>
|
<el-alert v-if="!status.configured" type="warning" :closable="false" show-icon>
|
||||||
还没有填写币安模拟盘 key,请先到「环境变量」页填写。
|
还没有填写币安模拟盘 key,请先到「环境变量」页填写。
|
||||||
@@ -311,12 +366,25 @@ onBeforeUnmount(() => clearInterval(timer))
|
|||||||
</el-alert>
|
</el-alert>
|
||||||
|
|
||||||
<!-- 持仓 -->
|
<!-- 持仓 -->
|
||||||
<el-table :data="positions" border size="small" empty-text="没有持仓">
|
<el-card shadow="never" body-class="p-0">
|
||||||
<el-table-column prop="symbol" label="币种" min-width="110" />
|
<template #header>
|
||||||
<el-table-column label="方向" width="70">
|
持仓({{ positions.length }})
|
||||||
|
</template>
|
||||||
|
<el-table :data="positions" empty-text="没有持仓" cell-class-name="num">
|
||||||
|
<el-table-column label="币种" min-width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="text-slate-900 font-semibold">
|
||||||
|
{{ row.symbol }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-slate-400 font-sans">
|
||||||
|
永续
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="方向" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.side === 'LONG' ? 'success' : 'danger'" size="small">
|
<el-tag :type="row.side === 'LONG' ? 'success' : 'danger'" size="small">
|
||||||
{{ row.side === "LONG" ? "多" : "空" }}
|
{{ row.side === "LONG" ? "↑ 多" : "↓ 空" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -332,12 +400,12 @@ onBeforeUnmount(() => clearInterval(timer))
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="保本价" min-width="100">
|
<el-table-column label="保本价" min-width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ Number(row.break_even_price).toFixed(2) }}
|
<span class="text-slate-500">{{ Number(row.break_even_price).toFixed(2) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="标记价" min-width="100">
|
<el-table-column label="标记价" min-width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ Number(row.mark_price).toFixed(2) }}
|
<span class="text-[var(--el-color-primary)]">{{ Number(row.mark_price).toFixed(2) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="未实现盈亏" min-width="100">
|
<el-table-column label="未实现盈亏" min-width="100">
|
||||||
@@ -347,130 +415,147 @@ onBeforeUnmount(() => clearInterval(timer))
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="强平价" min-width="100">
|
<el-table-column label="强平价" min-width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ Number(row.liquidation_price) ? Number(row.liquidation_price).toFixed(2) : "-" }}
|
<span class="text-amber-600">{{ Number(row.liquidation_price) ? Number(row.liquidation_price).toFixed(2) : "-" }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="杠杆" width="110">
|
<el-table-column label="杠杆" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<el-tag type="info" size="small">
|
||||||
{{ row.leverage }}x {{ row.margin_type === "ISOLATED" ? "逐仓" : "全仓" }}
|
{{ row.leverage }}x {{ row.margin_type === "ISOLATED" ? "逐仓" : "全仓" }}
|
||||||
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="止损 / 止盈" min-width="140">
|
<el-table-column label="止损" min-width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ exitPrices(row as SnapshotPosition).stop }} / {{ exitPrices(row as SnapshotPosition).target }}
|
<span class="text-rose-600">{{ exitPrices(row as SnapshotPosition).stop }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="平仓倒计时" width="100">
|
<el-table-column label="止盈" min-width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<span class="text-emerald-600">{{ exitPrices(row as SnapshotPosition).target }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="平仓倒计时" width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag type="warning" size="small">
|
||||||
{{ closeCountdown(row.symbol) }}
|
{{ closeCountdown(row.symbol) }}
|
||||||
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<div class="min-h-0 flex flex-1 gap-3">
|
<div class="grid grid-cols-[260px_minmax(0,1fr)_340px] items-start gap-4">
|
||||||
<!-- 侧边栏:交易币种 -->
|
<!-- 左:交易币种 -->
|
||||||
<div class="w-60 flex flex-col border border-[var(--el-border-color)] rounded border-solid">
|
<el-card shadow="never" body-class="flex flex-col gap-2">
|
||||||
<div class="flex items-center justify-between border-b border-b-[var(--el-border-color)] border-b-solid px-3 py-2">
|
<template #header>
|
||||||
<span class="font-bold">交易币种</span>
|
<div class="flex items-center justify-between">
|
||||||
<el-button size="small" type="primary" @click="addVisible = true">
|
交易币种
|
||||||
新增
|
<el-button link type="primary" :icon="Plus" @click="addVisible = true">
|
||||||
|
新增币种
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-scrollbar class="flex-1">
|
</template>
|
||||||
<div
|
<div
|
||||||
v-for="s in symbols" :key="s.symbol"
|
v-for="s in symbols" :key="s.symbol"
|
||||||
class="cursor-pointer border-b border-b-[var(--el-border-color-lighter)] border-b-solid px-3 py-2"
|
class="relative cursor-pointer overflow-hidden border rounded-lg border-solid py-2 pl-4 pr-3 transition-colors"
|
||||||
:class="s.symbol === selected ? 'bg-[var(--el-color-primary-light-9)]' : ''"
|
:class="s.symbol === selected ? 'border-[var(--el-color-primary)] bg-sky-50/60' : 'border-slate-200 hover:border-slate-300'"
|
||||||
@click="selected = s.symbol"
|
@click="selected = s.symbol"
|
||||||
>
|
>
|
||||||
|
<span class="absolute inset-y-0 left-0 w-1" :style="{ background: `var(--el-color-${symbolState(s).type})` }" />
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-bold">{{ s.symbol }}</span>
|
<span class="num text-slate-900 font-semibold">{{ s.symbol }}</span>
|
||||||
<el-tag :type="symbolState(s).type" size="small">
|
<el-tag :type="symbolState(s).type" size="small">
|
||||||
{{ symbolState(s).label }}
|
{{ symbolState(s).label }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 flex justify-end text-xs">
|
<div class="mt-1 flex items-center justify-between text-xs text-slate-500">
|
||||||
|
<span v-if="positionOf(s.symbol)" class="num">
|
||||||
|
{{ positionOf(s.symbol)!.leverage }}x {{ positionOf(s.symbol)!.side === "LONG" ? "多" : "空" }} · {{ num(positionOf(s.symbol)!.amount) }}
|
||||||
|
</span>
|
||||||
|
<span v-else />
|
||||||
<el-link type="danger" underline="never" class="text-xs!" @click.stop="remove(s)">
|
<el-link type="danger" underline="never" class="text-xs!" @click.stop="remove(s)">
|
||||||
删除
|
删除
|
||||||
</el-link>
|
</el-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-if="!symbols.length" description="还没有交易币种" :image-size="60" />
|
<el-empty v-if="!symbols.length" description="还没有交易币种" :image-size="60" />
|
||||||
</el-scrollbar>
|
</el-card>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 主区域:当前计划、挂单、时间线 -->
|
<!-- 中:当前计划 -->
|
||||||
<el-scrollbar class="flex-1">
|
|
||||||
<div class="flex flex-col gap-3 pr-2">
|
|
||||||
<PlanCard
|
<PlanCard
|
||||||
v-if="current" :symbol="current" :analysis="livePlan ?? latestAnalysis" :latest="latestAnalysis" :orders="planOrders"
|
v-if="current" :symbol="current" :analysis="livePlan ?? latestAnalysis" :latest="latestAnalysis" :orders="planOrders"
|
||||||
:requesting="requesting"
|
:requesting="requesting"
|
||||||
@analyze="analyze" @detail="openAnalysis"
|
@analyze="analyze" @detail="openAnalysis"
|
||||||
/>
|
/>
|
||||||
|
<el-card v-else shadow="never">
|
||||||
|
<el-empty description="先在左侧新增交易币种" :image-size="60" />
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<div v-if="current">
|
<!-- 右:挂单、时间线 -->
|
||||||
<div class="mb-2 font-bold">
|
<div class="flex flex-col gap-4">
|
||||||
当前挂单
|
<el-card v-if="current" shadow="never" body-class="flex flex-col gap-2">
|
||||||
|
<template #header>
|
||||||
|
当前挂单({{ symbolOrders.length }})
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-for="o in symbolOrders" :key="`${o.client_id}-${o.time}`"
|
||||||
|
class="border border-slate-200 rounded-lg border-solid bg-slate-50/60 px-3 py-2 text-xs"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm font-semibold" :class="o.side === 'BUY' ? 'text-emerald-700' : 'text-rose-700'">
|
||||||
|
{{ o.side === "BUY" ? "买" : "卖" }} · {{ o.type }}
|
||||||
|
</span>
|
||||||
|
<span class="num text-slate-400">{{ formatTime(new Date(o.time).toISOString(), "MM-DD HH:mm:ss") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="symbolOrders" border size="small" empty-text="没有挂单">
|
<div class="num mt-1 flex justify-between text-sm text-slate-900">
|
||||||
<el-table-column label="类型" min-width="150">
|
<span>{{ o.quantity === "全部持仓" ? o.quantity : num(o.quantity) }}</span>
|
||||||
<template #default="{ row }">
|
<span>@ {{ o.priceText }}</span>
|
||||||
{{ row.type }}
|
</div>
|
||||||
<el-tag v-if="row.conditional" size="small" class="ml-1">
|
<div class="mt-1 flex items-center gap-1 text-slate-500">
|
||||||
|
<el-tag v-if="o.conditional" size="small">
|
||||||
条件单
|
条件单
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
<el-tag v-if="o.reduce_only" size="small" type="info">
|
||||||
</el-table-column>
|
只减仓
|
||||||
<el-table-column label="方向" width="110">
|
</el-tag>
|
||||||
<template #default="{ row }">
|
<span class="ml-auto">来源:{{ isOwnOrder(o.client_id) ? "本程序" : "手动" }}</span>
|
||||||
{{ row.side === "BUY" ? "买" : "卖" }}{{ row.reduce_only ? "(只减仓)" : "" }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="priceText" label="价格" min-width="130" />
|
|
||||||
<el-table-column label="数量" min-width="100">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.quantity === "全部持仓" ? row.quantity : num(row.quantity) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="来源" width="80">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ isOwnOrder(row.client_id) ? "本程序" : "手动" }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="时间" width="130">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ formatTime(new Date(row.time).toISOString(), "MM-DD HH:mm:ss") }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="!symbolOrders.length" description="没有挂单" :image-size="48" />
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<div>
|
<el-card shadow="never">
|
||||||
<div class="mb-2 font-bold">
|
<template #header>
|
||||||
时间线
|
时间线
|
||||||
</div>
|
</template>
|
||||||
<el-timeline v-if="events.length" class="pl-1">
|
<div v-if="events.length" class="max-h-180 overflow-auto pr-1">
|
||||||
<el-timeline-item
|
<div v-for="e in events" :key="e.id" class="timeline-item relative pb-4 pl-5">
|
||||||
v-for="e in events" :key="e.id" :timestamp="formatTime(e.created_at)" :type="LEVEL_TYPES[e.level]"
|
<span class="absolute left-0 top-1 h-[11px] w-[11px] border-2 border-white rounded-full border-solid" :style="{ background: `var(--el-color-${LEVEL_TYPES[e.level]})` }" />
|
||||||
placement="top"
|
<div class="flex items-center gap-2 text-xs">
|
||||||
>
|
<span class="num text-slate-500">{{ formatTime(e.created_at) }}</span>
|
||||||
<el-tag size="small" type="info" class="mr-2">
|
<el-tag size="small" :type="LEVEL_TYPES[e.level]">
|
||||||
|
{{ e.level.toUpperCase() }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag size="small" type="info">
|
||||||
{{ e.symbol ?? "账户" }}
|
{{ e.symbol ?? "账户" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span :class="e.level === 'error' ? 'text-[var(--el-color-danger)]' : ''">{{ e.message }}</span>
|
</div>
|
||||||
<el-link v-if="e.analysis_id && e.kind !== 'analysis_started'" type="primary" underline="never" class="ml-2" @click="openAnalysis(e.analysis_id)">
|
<div class="mt-1 text-sm" :class="e.level === 'error' ? 'text-rose-600' : 'text-slate-700'">
|
||||||
|
{{ e.message }}
|
||||||
|
<el-link v-if="e.analysis_id && e.kind !== 'analysis_started'" type="primary" underline="never" class="ml-1" @click="openAnalysis(e.analysis_id)">
|
||||||
查看分析
|
查看分析
|
||||||
</el-link>
|
</el-link>
|
||||||
</el-timeline-item>
|
</div>
|
||||||
</el-timeline>
|
</div>
|
||||||
|
</div>
|
||||||
<el-empty v-else description="还没有事件" :image-size="60" />
|
<el-empty v-else description="还没有事件" :image-size="60" />
|
||||||
<el-button v-if="events.length && !eventsExhausted" link type="primary" @click="loadMoreEvents">
|
<el-button v-if="events.length && !eventsExhausted" link type="primary" @click="loadMoreEvents">
|
||||||
加载更早的事件
|
加载更早的事件
|
||||||
</el-button>
|
</el-button>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-scrollbar>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AddSymbolDialog v-model="addVisible" @added="onAdded" />
|
<AddSymbolDialog v-model="addVisible" @added="onAdded" />
|
||||||
<AnalysisDrawer v-model="drawerVisible" :analysis-id="drawerAnalysisId" />
|
<AnalysisDrawer v-model="drawerVisible" :analysis-id="drawerAnalysisId" />
|
||||||
@@ -478,12 +563,53 @@ onBeforeUnmount(() => clearInterval(timer))
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.stat {
|
.metric {
|
||||||
font-size: 14px;
|
min-width: 120px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--el-bg-color);
|
||||||
}
|
}
|
||||||
.stat span {
|
.metric-label {
|
||||||
margin-right: 6px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.metric-value {
|
||||||
|
margin-top: 2px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-family: var(--ap-font-mono);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.metric-value small {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
/* 竖线连到下一条事件 */
|
||||||
|
.timeline-item::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 5px;
|
||||||
|
width: 1px;
|
||||||
|
background: var(--el-border-color);
|
||||||
|
}
|
||||||
|
.timeline-item:last-child::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.pulse {
|
||||||
|
box-shadow: 0 0 0 0 rgb(16 185 129 / 50%);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
@keyframes pulse {
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 6px rgb(16 185 129 / 0%);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
+125
-1
@@ -1,3 +1,82 @@
|
|||||||
|
/* 浅色主题(Stitch「Daylight」稿):色值只在这里定义,页面里用 Element 变量或 UnoCSS 色板类 */
|
||||||
|
:root {
|
||||||
|
--ap-font-sans:
|
||||||
|
"Inter Variable", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
--ap-font-mono:
|
||||||
|
"JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Consolas, "PingFang SC", "Microsoft YaHei",
|
||||||
|
monospace;
|
||||||
|
|
||||||
|
--el-font-family: var(--ap-font-sans);
|
||||||
|
--el-border-radius-base: 4px;
|
||||||
|
|
||||||
|
--el-color-primary: #006194;
|
||||||
|
--el-color-success: #16a34a;
|
||||||
|
--el-color-warning: #d97706;
|
||||||
|
--el-color-danger: #dc2626;
|
||||||
|
--el-color-error: #dc2626;
|
||||||
|
--el-color-info: #64748b;
|
||||||
|
|
||||||
|
--el-text-color-primary: #0f172a;
|
||||||
|
--el-text-color-regular: #334155;
|
||||||
|
--el-text-color-secondary: #64748b;
|
||||||
|
--el-text-color-placeholder: #94a3b8;
|
||||||
|
--el-border-color: #e2e8f0;
|
||||||
|
--el-border-color-light: #e2e8f0;
|
||||||
|
--el-border-color-lighter: #f1f5f9;
|
||||||
|
--el-border-color-extra-light: #f8fafc;
|
||||||
|
--el-border-color-dark: #cbd5e1;
|
||||||
|
--el-border-color-darker: #94a3b8;
|
||||||
|
--el-fill-color: #f1f5f9;
|
||||||
|
--el-fill-color-light: #f8fafc;
|
||||||
|
--el-fill-color-lighter: #f8fafc;
|
||||||
|
--el-fill-color-extra-light: #fcfdfe;
|
||||||
|
--el-fill-color-dark: #e2e8f0;
|
||||||
|
--el-fill-color-darker: #cbd5e1;
|
||||||
|
--el-bg-color-page: #f8fafc;
|
||||||
|
--el-box-shadow-lighter: 0 1px 3px 0 rgb(15 23 42 / 3%), 0 1px 2px -1px rgb(15 23 42 / 2%);
|
||||||
|
--el-box-shadow-light: 0 4px 6px -1px rgb(15 23 42 / 5%), 0 2px 4px -2px rgb(15 23 42 / 3%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element 的深浅档按与白色、黑色的混合比例生成,与它的 SCSS 算法一致 */
|
||||||
|
:root {
|
||||||
|
--el-color-primary-light-3: color-mix(in srgb, var(--el-color-primary), white 30%);
|
||||||
|
--el-color-primary-light-5: color-mix(in srgb, var(--el-color-primary), white 50%);
|
||||||
|
--el-color-primary-light-7: color-mix(in srgb, var(--el-color-primary), white 70%);
|
||||||
|
--el-color-primary-light-8: color-mix(in srgb, var(--el-color-primary), white 80%);
|
||||||
|
--el-color-primary-light-9: color-mix(in srgb, var(--el-color-primary), white 90%);
|
||||||
|
--el-color-primary-dark-2: color-mix(in srgb, var(--el-color-primary), black 20%);
|
||||||
|
--el-color-success-light-3: color-mix(in srgb, var(--el-color-success), white 30%);
|
||||||
|
--el-color-success-light-5: color-mix(in srgb, var(--el-color-success), white 50%);
|
||||||
|
--el-color-success-light-7: color-mix(in srgb, var(--el-color-success), white 70%);
|
||||||
|
--el-color-success-light-8: color-mix(in srgb, var(--el-color-success), white 80%);
|
||||||
|
--el-color-success-light-9: color-mix(in srgb, var(--el-color-success), white 90%);
|
||||||
|
--el-color-success-dark-2: color-mix(in srgb, var(--el-color-success), black 20%);
|
||||||
|
--el-color-warning-light-3: color-mix(in srgb, var(--el-color-warning), white 30%);
|
||||||
|
--el-color-warning-light-5: color-mix(in srgb, var(--el-color-warning), white 50%);
|
||||||
|
--el-color-warning-light-7: color-mix(in srgb, var(--el-color-warning), white 70%);
|
||||||
|
--el-color-warning-light-8: color-mix(in srgb, var(--el-color-warning), white 80%);
|
||||||
|
--el-color-warning-light-9: color-mix(in srgb, var(--el-color-warning), white 90%);
|
||||||
|
--el-color-warning-dark-2: color-mix(in srgb, var(--el-color-warning), black 20%);
|
||||||
|
--el-color-danger-light-3: color-mix(in srgb, var(--el-color-danger), white 30%);
|
||||||
|
--el-color-danger-light-5: color-mix(in srgb, var(--el-color-danger), white 50%);
|
||||||
|
--el-color-danger-light-7: color-mix(in srgb, var(--el-color-danger), white 70%);
|
||||||
|
--el-color-danger-light-8: color-mix(in srgb, var(--el-color-danger), white 80%);
|
||||||
|
--el-color-danger-light-9: color-mix(in srgb, var(--el-color-danger), white 90%);
|
||||||
|
--el-color-danger-dark-2: color-mix(in srgb, var(--el-color-danger), black 20%);
|
||||||
|
--el-color-error-light-3: color-mix(in srgb, var(--el-color-error), white 30%);
|
||||||
|
--el-color-error-light-5: color-mix(in srgb, var(--el-color-error), white 50%);
|
||||||
|
--el-color-error-light-7: color-mix(in srgb, var(--el-color-error), white 70%);
|
||||||
|
--el-color-error-light-8: color-mix(in srgb, var(--el-color-error), white 80%);
|
||||||
|
--el-color-error-light-9: color-mix(in srgb, var(--el-color-error), white 90%);
|
||||||
|
--el-color-error-dark-2: color-mix(in srgb, var(--el-color-error), black 20%);
|
||||||
|
--el-color-info-light-3: color-mix(in srgb, var(--el-color-info), white 30%);
|
||||||
|
--el-color-info-light-5: color-mix(in srgb, var(--el-color-info), white 50%);
|
||||||
|
--el-color-info-light-7: color-mix(in srgb, var(--el-color-info), white 70%);
|
||||||
|
--el-color-info-light-8: color-mix(in srgb, var(--el-color-info), white 80%);
|
||||||
|
--el-color-info-light-9: color-mix(in srgb, var(--el-color-info), white 90%);
|
||||||
|
--el-color-info-dark-2: color-mix(in srgb, var(--el-color-info), black 20%);
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
#app {
|
#app {
|
||||||
@@ -6,7 +85,48 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
color: var(--el-text-color-regular);
|
||||||
|
font-family: var(--ap-font-sans);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
background: var(--el-bg-color-page);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--el-border-color-dark);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--el-border-color-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 面板:白底、细边框、8px 圆角,头部是带分隔线的标题条 */
|
||||||
|
.el-card {
|
||||||
|
--el-card-border-radius: 8px;
|
||||||
|
--el-card-padding: 12px;
|
||||||
|
}
|
||||||
|
.el-card.is-never-shadow {
|
||||||
|
box-shadow: var(--el-box-shadow-lighter);
|
||||||
|
}
|
||||||
|
.el-card__header {
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
--el-table-header-bg-color: var(--el-fill-color-light);
|
||||||
|
--el-table-header-text-color: var(--el-text-color-secondary);
|
||||||
|
--el-table-row-hover-bg-color: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
.el-table th.el-table__cell {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Markdown 渲染(Chat 回复、skill 预览、CONTEXT.md 预览) */
|
/* Markdown 渲染(Chat 回复、skill 预览、CONTEXT.md 预览) */
|
||||||
@@ -75,3 +195,7 @@ body {
|
|||||||
padding: 0.3em 0.6em;
|
padding: 0.3em 0.6em;
|
||||||
border: 1px solid var(--el-border-color);
|
border: 1px solid var(--el-border-color);
|
||||||
}
|
}
|
||||||
|
.markdown-body th {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
Vendored
+13
-7
@@ -12,36 +12,40 @@ export {}
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||||
ElAside: typeof import('element-plus/es')['ElAside']
|
|
||||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
|
ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
|
||||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||||
|
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||||
|
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
|
||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||||
|
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
|
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
|
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||||
|
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||||
ElForm: typeof import('element-plus/es')['ElForm']
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
ElLink: typeof import('element-plus/es')['ElLink']
|
ElLink: typeof import('element-plus/es')['ElLink']
|
||||||
ElMain: typeof import('element-plus/es')['ElMain']
|
|
||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElOptionGroup: typeof import('element-plus/es')['ElOptionGroup']
|
ElOptionGroup: typeof import('element-plus/es')['ElOptionGroup']
|
||||||
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
|
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
|
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
@@ -51,11 +55,13 @@ declare module 'vue' {
|
|||||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
|
ElTree: typeof import('element-plus/es')['ElTree']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
SkillTree: typeof import('./../src/components/SkillTree.vue')['default']
|
||||||
|
SkillVersionDrawer: typeof import('./../src/components/SkillVersionDrawer.vue')['default']
|
||||||
}
|
}
|
||||||
export interface GlobalDirectives {
|
export interface GlobalDirectives {
|
||||||
vInfiniteScroll: typeof import('element-plus/es')['ElInfiniteScroll']
|
|
||||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+9
-1
@@ -13,6 +13,13 @@ export default defineConfig({
|
|||||||
important: "#app"
|
important: "#app"
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
// 字体栈定义在 styles/index.css,Element Plus 与 UnoCSS 共用
|
||||||
|
theme: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: "var(--ap-font-sans)",
|
||||||
|
mono: "var(--ap-font-mono)"
|
||||||
|
}
|
||||||
|
},
|
||||||
// 自定义规则
|
// 自定义规则
|
||||||
rules: [],
|
rules: [],
|
||||||
// 自定义快捷方式
|
// 自定义快捷方式
|
||||||
@@ -20,6 +27,7 @@ export default defineConfig({
|
|||||||
"wh-full": "w-full h-full",
|
"wh-full": "w-full h-full",
|
||||||
"flex-center": "flex justify-center items-center",
|
"flex-center": "flex justify-center items-center",
|
||||||
"flex-x-center": "flex justify-center",
|
"flex-x-center": "flex justify-center",
|
||||||
"flex-y-center": "flex items-center"
|
"flex-y-center": "flex items-center",
|
||||||
|
"num": "font-mono tabular-nums"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Reference in new issue
Block a user