Restyle the web UI as a dark trading terminal
- Single palette (web/src/styles/palette.ts) taken from the Stitch design, exposed as ap-* UnoCSS colors and --ap-* CSS variables and used for Element Plus dark theme overrides, KLineChart and the news iframe; zero radius, JetBrains Mono / Space Grotesk bundled locally - New sidebar and header layout; trading page, plan card and analysis drawer follow the design; other pages use the same panels - Status API reports today's closed trades, wins and net PnL, shown as today's PnL and win rate on the trading page
This commit is contained in:
1 parent
665c12fc31
commit
d5fa78f1ad
31 files changed
+1016
-322
No files matched your search
@@ -32,11 +32,15 @@ SNAPSHOT_FRESH_SECONDS = 30
|
|||||||
LIVE_PLAN_STATUSES = ("pending", "open", "closing")
|
LIVE_PLAN_STATUSES = ("pending", "open", "closing")
|
||||||
|
|
||||||
|
|
||||||
class TodayUsage(BaseModel):
|
class TodayStats(BaseModel):
|
||||||
analyses: int
|
analyses: int
|
||||||
limit: int
|
limit: int
|
||||||
input_tokens: int
|
input_tokens: int
|
||||||
output_tokens: int
|
output_tokens: int
|
||||||
|
# 今日结束(平仓)的计划:笔数、净盈亏为正的笔数、净盈亏合计(含手续费)
|
||||||
|
trades: int
|
||||||
|
wins: int
|
||||||
|
net_pnl: Decimal
|
||||||
|
|
||||||
|
|
||||||
class StatusOut(BaseModel):
|
class StatusOut(BaseModel):
|
||||||
@@ -50,7 +54,7 @@ class StatusOut(BaseModel):
|
|||||||
reasoning_effort: str | None
|
reasoning_effort: str | None
|
||||||
# trader 进程的心跳快照;进程离线时为空
|
# trader 进程的心跳快照;进程离线时为空
|
||||||
snapshot: dict | None
|
snapshot: dict | None
|
||||||
today: TodayUsage
|
today: TodayStats
|
||||||
|
|
||||||
|
|
||||||
class ConfigIn(BaseModel):
|
class ConfigIn(BaseModel):
|
||||||
@@ -156,6 +160,10 @@ async def get_status(session: Annotated[AsyncSession, Depends(get_session)]) ->
|
|||||||
func.coalesce(func.sum(TradingAnalysis.usage["output_tokens"].as_integer()), 0),
|
func.coalesce(func.sum(TradingAnalysis.usage["output_tokens"].as_integer()), 0),
|
||||||
).where(TradingAnalysis.started_at >= today)
|
).where(TradingAnalysis.started_at >= today)
|
||||||
)).one()
|
)).one()
|
||||||
|
# 计划 closed 时 executor.finish 一定写了 result
|
||||||
|
nets = [Decimal(r["net"]) for r in (await session.scalars(
|
||||||
|
select(TradingAnalysis.result).where(TradingAnalysis.plan_status == "closed", TradingAnalysis.closed_at >= today)
|
||||||
|
)).all()]
|
||||||
snapshot = await read_snapshot()
|
snapshot = await read_snapshot()
|
||||||
equity = snapshot and snapshot["account"] and Decimal(snapshot["account"]["margin_balance"])
|
equity = snapshot and snapshot["account"] and Decimal(snapshot["account"]["margin_balance"])
|
||||||
return StatusOut(
|
return StatusOut(
|
||||||
@@ -163,7 +171,8 @@ async def get_status(session: Annotated[AsyncSession, Depends(get_session)]) ->
|
|||||||
running_changed_at=account.running_changed_at, capital_usdt=account.capital_usdt,
|
running_changed_at=account.running_changed_at, capital_usdt=account.capital_usdt,
|
||||||
effective_capital=min(account.capital_usdt, equity) if account.capital_usdt and equity else None,
|
effective_capital=min(account.capital_usdt, equity) if account.capital_usdt and equity else None,
|
||||||
provider_id=account.provider_id, reasoning_effort=account.reasoning_effort, snapshot=snapshot,
|
provider_id=account.provider_id, reasoning_effort=account.reasoning_effort, snapshot=snapshot,
|
||||||
today=TodayUsage(analyses=count, limit=DAILY_ANALYSIS_LIMIT, input_tokens=input_tokens, output_tokens=output_tokens),
|
today=TodayStats(analyses=count, limit=DAILY_ANALYSIS_LIMIT, input_tokens=input_tokens, output_tokens=output_tokens,
|
||||||
|
trades=len(nets), wins=sum(n > 0 for n in nets), net_pnl=sum(nets, Decimal(0))),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN" class="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||||
|
"@fontsource-variable/space-grotesk": "^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/jetbrains-mono':
|
||||||
|
specifier: ^5.3.0
|
||||||
|
version: 5.3.0
|
||||||
|
'@fontsource-variable/space-grotesk':
|
||||||
|
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-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==}
|
||||||
|
|
||||||
|
'@fontsource-variable/[email protected]':
|
||||||
|
resolution: {integrity: sha512-2IxmvfB08i9vnGB3Ym/AXvhRE+8XOjWMXIyDum03c+tPwH0FUoMNQfGpU8NXPxjbws0Vvss3AH0Zqt4oJBBAdw==}
|
||||||
|
|
||||||
'@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
|
||||||
|
|||||||
+10
-1
@@ -64,7 +64,16 @@ export interface TradingStatus {
|
|||||||
reasoning_effort: Effort | null
|
reasoning_effort: Effort | null
|
||||||
/** trader 进程离线时为空 */
|
/** trader 进程离线时为空 */
|
||||||
snapshot: Snapshot | null
|
snapshot: Snapshot | null
|
||||||
today: { analyses: number, limit: number, input_tokens: number, output_tokens: number }
|
today: {
|
||||||
|
analyses: number
|
||||||
|
limit: number
|
||||||
|
input_tokens: number
|
||||||
|
output_tokens: number
|
||||||
|
/** 今日结束的计划笔数、其中净盈亏为正的笔数、净盈亏合计(含手续费) */
|
||||||
|
trades: number
|
||||||
|
wins: number
|
||||||
|
net_pnl: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TradingSymbol {
|
export interface TradingSymbol {
|
||||||
|
|||||||
@@ -126,8 +126,8 @@ function onCollapse(item: TreeItem) {
|
|||||||
>
|
>
|
||||||
<template #default="{ data }">
|
<template #default="{ data }">
|
||||||
<div
|
<div
|
||||||
class="group w-full flex items-center gap-1 overflow-hidden rounded pr-1 text-sm"
|
class="group w-full flex items-center gap-1 overflow-hidden pr-1 text-sm"
|
||||||
:class="{ 'opacity-50': !data.data.enabled, 'bg-yellow-100': highlightIds?.includes(data.id) }"
|
:class="{ 'opacity-50': !data.data.enabled, 'bg-ap-brand/15': highlightIds?.includes(data.id) }"
|
||||||
>
|
>
|
||||||
<span>{{ data.data.kind === "folder" ? "📁" : "📄" }}</span>
|
<span>{{ data.data.kind === "folder" ? "📁" : "📄" }}</span>
|
||||||
<span class="truncate">{{ data.data.name }}</span>
|
<span class="truncate">{{ data.data.name }}</span>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ async function publish(version: SkillVersion) {
|
|||||||
<el-scrollbar class="w-60 shrink-0">
|
<el-scrollbar class="w-60 shrink-0">
|
||||||
<div
|
<div
|
||||||
v-for="v in versions" :key="v.id"
|
v-for="v in versions" :key="v.id"
|
||||||
class="mb-1 cursor-pointer border rounded border-solid px-2 py-1 text-sm"
|
class="mb-1 cursor-pointer border border-solid px-2 py-1 text-sm"
|
||||||
:class="v.id === selectedId ? 'border-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)]' : 'border-[var(--el-border-color-lighter)]'"
|
:class="v.id === selectedId ? 'border-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)]' : 'border-[var(--el-border-color-lighter)]'"
|
||||||
@click="selectedId = v.id"
|
@click="selectedId = v.id"
|
||||||
>
|
>
|
||||||
@@ -81,11 +81,11 @@ async function publish(version: SkillVersion) {
|
|||||||
发布此版本
|
发布此版本
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-scrollbar class="min-h-0 flex-1 border rounded border-solid border-[var(--el-border-color-lighter)]">
|
<el-scrollbar class="min-h-0 flex-1 border border-ap-line border-solid bg-ap-sunken">
|
||||||
<div class="p-2 text-xs font-mono">
|
<div class="p-2 text-xs font-mono">
|
||||||
<div
|
<div
|
||||||
v-for="(line, i) in diff" :key="i" class="whitespace-pre-wrap"
|
v-for="(line, i) in diff" :key="i" class="whitespace-pre-wrap"
|
||||||
:class="{ 'bg-green-100': line.type === 'add', 'bg-red-100 line-through': line.type === 'del' }"
|
:class="{ 'diff-add': line.type === 'add', 'diff-del': line.type === 'del' }"
|
||||||
>
|
>
|
||||||
{{ line.type === "add" ? "+ " : line.type === "del" ? "- " : " " }}{{ line.text }}
|
{{ line.type === "add" ? "+ " : line.type === "del" ? "- " : " " }}{{ line.text }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+61
-17
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { DataLine } from "@element-plus/icons-vue"
|
||||||
import { useUserStore } from "@/pinia/user"
|
import { useUserStore } from "@/pinia/user"
|
||||||
import { menuRoutes } from "@/router"
|
import { menuRoutes } from "@/router"
|
||||||
|
|
||||||
@@ -13,27 +14,70 @@ async function logout() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-container class="h-full">
|
<div class="h-full flex bg-ap-bg">
|
||||||
<el-aside width="200px" class="border-r border-r-solid border-[var(--el-border-color)]">
|
<aside class="w-[200px] flex shrink-0 flex-col border-r border-r-ap-line border-r-solid bg-ap-panel">
|
||||||
<div class="h-14 flex-center text-lg font-bold">
|
<div class="h-14 flex shrink-0 flex-col justify-center border-b border-b-ap-line border-b-solid bg-ap-panel-high px-3">
|
||||||
AlphaPulse
|
<div class="flex items-center gap-2 font-head text-base text-ap-brand font-bold tracking-wider">
|
||||||
|
<el-icon :size="18">
|
||||||
|
<DataLine />
|
||||||
|
</el-icon>
|
||||||
|
ALPHAPULSE
|
||||||
</div>
|
</div>
|
||||||
<el-menu :default-active="route.path" router class="border-r-0!">
|
<div class="mt-0.5 text-[10px] text-ap-muted tracking-[0.2em]">
|
||||||
<el-menu-item v-for="item in menuRoutes" :key="item.path" :index="item.path">
|
TRADING TERMINAL
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="flex flex-col py-1">
|
||||||
|
<router-link v-for="item in menuRoutes" :key="item.path" :to="item.path" class="nav-item" active-class="active">
|
||||||
|
<el-icon :size="16">
|
||||||
|
<component :is="item.meta.icon" />
|
||||||
|
</el-icon>
|
||||||
{{ item.meta.title }}
|
{{ item.meta.title }}
|
||||||
</el-menu-item>
|
</router-link>
|
||||||
</el-menu>
|
</nav>
|
||||||
</el-aside>
|
</aside>
|
||||||
<el-container>
|
<div class="min-w-0 flex flex-1 flex-col">
|
||||||
<el-header class="flex-y-center justify-end gap-3 border-b border-b-solid border-[var(--el-border-color)]">
|
<header class="h-14 flex shrink-0 items-center justify-between border-b border-b-ap-line border-b-solid bg-ap-panel px-4">
|
||||||
<span>{{ userStore.username }}</span>
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-head text-base text-ap-brand font-bold">AlphaPulse</span>
|
||||||
|
<span class="text-ap-line">/</span>
|
||||||
|
<span class="font-bold tracking-wide">{{ route.meta.title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="h-6 w-6 flex-center border border-ap-line border-solid bg-ap-panel-highest text-[11px] text-ap-brand font-bold">
|
||||||
|
{{ userStore.username!.slice(0, 2).toUpperCase() }}
|
||||||
|
</span>
|
||||||
|
<span class="font-semibold">{{ userStore.username }}</span>
|
||||||
<el-button link @click="logout">
|
<el-button link @click="logout">
|
||||||
退出
|
退出
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-header>
|
</div>
|
||||||
<el-main>
|
</header>
|
||||||
|
<main class="min-h-0 flex-1 overflow-auto p-2">
|
||||||
<router-view />
|
<router-view />
|
||||||
</el-main>
|
</main>
|
||||||
</el-container>
|
</div>
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
color: var(--ap-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.nav-item:hover {
|
||||||
|
background: var(--ap-panel-highest);
|
||||||
|
color: var(--ap-text);
|
||||||
|
}
|
||||||
|
.nav-item.active {
|
||||||
|
border-left-color: var(--ap-brand);
|
||||||
|
background: var(--ap-panel-high);
|
||||||
|
color: var(--ap-brand);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { createPinia } from "pinia"
|
import { createPinia } from "pinia"
|
||||||
import App from "@/App.vue"
|
import App from "@/App.vue"
|
||||||
import { router } from "@/router"
|
import { router } from "@/router"
|
||||||
|
import "@fontsource-variable/jetbrains-mono"
|
||||||
|
import "@fontsource-variable/space-grotesk"
|
||||||
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 "@/styles/index.css"
|
import "@/styles/index.css"
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ load()
|
|||||||
<div class="min-h-0 flex flex-1 gap-3">
|
<div class="min-h-0 flex flex-1 gap-3">
|
||||||
<textarea
|
<textarea
|
||||||
v-model="content" placeholder="目标、已确认的结论、待办…(Markdown)"
|
v-model="content" placeholder="目标、已确认的结论、待办…(Markdown)"
|
||||||
class="box-border flex-1 resize-none border rounded border-solid border-[var(--el-border-color)] p-3 text-sm leading-6 font-mono outline-none focus:border-[var(--el-color-primary)]"
|
class="box-border flex-1 resize-none border border-ap-line border-solid bg-ap-sunken p-3 text-sm text-ap-text leading-6 font-mono outline-none focus:border-ap-brand"
|
||||||
/>
|
/>
|
||||||
<el-scrollbar class="flex-1 border rounded border-solid border-[var(--el-border-color-lighter)]">
|
<el-scrollbar class="flex-1 border border-ap-line border-solid bg-ap-panel-low">
|
||||||
<div class="markdown-body p-3" v-html="preview" />
|
<div class="markdown-body p-3" v-html="preview" />
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ function prettyResult(result: string) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div v-for="item in items" :key="item.key" :class="item.kind === 'user' ? 'self-end max-w-3/4' : 'self-stretch'">
|
<div v-for="item in items" :key="item.key" :class="item.kind === 'user' ? 'self-end max-w-3/4' : 'self-stretch'">
|
||||||
<div v-if="item.kind === 'user'" class="whitespace-pre-wrap rounded-lg bg-[var(--el-color-primary-light-9)] px-3 py-2 text-sm">
|
<div v-if="item.kind === 'user'" class="whitespace-pre-wrap border-l-2 border-l-ap-brand border-l-solid bg-ap-panel-high px-3 py-2 font-sans text-sm">
|
||||||
{{ item.text }}
|
{{ item.text }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-col gap-2">
|
<div v-else class="flex flex-col gap-2">
|
||||||
@@ -71,7 +71,7 @@ function prettyResult(result: string) {
|
|||||||
<span class="truncate text-[var(--el-text-color-secondary)] font-mono">{{ JSON.stringify(call.arguments) }}</span>
|
<span class="truncate text-[var(--el-text-color-secondary)] font-mono">{{ JSON.stringify(call.arguments) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<pre class="m-0 max-h-80 overflow-auto whitespace-pre-wrap break-all rounded bg-[var(--el-fill-color-light)] p-2 text-xs">{{ call.result === null ? "等待结果…" : prettyResult(call.result) }}</pre>
|
<pre class="m-0 max-h-80 overflow-auto whitespace-pre-wrap break-all border border-ap-line border-solid bg-ap-panel-low p-2 text-xs">{{ call.result === null ? "等待结果…" : prettyResult(call.result) }}</pre>
|
||||||
</el-collapse-item>
|
</el-collapse-item>
|
||||||
</el-collapse>
|
</el-collapse>
|
||||||
<div v-if="item.stopped" class="text-xs text-[var(--el-text-color-secondary)]">
|
<div v-if="item.stopped" class="text-xs text-[var(--el-text-color-secondary)]">
|
||||||
@@ -84,6 +84,9 @@ function prettyResult(result: string) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.tool-cards {
|
||||||
|
border: 1px solid var(--ap-line);
|
||||||
|
}
|
||||||
.tool-cards :deep(.el-collapse-item__header) {
|
.tool-cards :deep(.el-collapse-item__header) {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
|
|||||||
@@ -218,14 +218,16 @@ loadSkills()
|
|||||||
<el-tabs v-model="activeTab" class="chat-tabs min-h-0 flex flex-1 flex-col">
|
<el-tabs v-model="activeTab" class="chat-tabs min-h-0 flex flex-1 flex-col">
|
||||||
<el-tab-pane label="Chat" name="chat" class="h-full">
|
<el-tab-pane label="Chat" name="chat" class="h-full">
|
||||||
<div class="h-full flex gap-3">
|
<div class="h-full flex gap-3">
|
||||||
<el-card class="w-72 shrink-0" body-class="h-full box-border flex flex-col gap-2" shadow="never">
|
<el-card class="w-72 flex shrink-0 flex-col" body-class="min-h-0 flex-1 box-border flex flex-col gap-2" shadow="never">
|
||||||
<div class="flex items-center justify-between text-sm font-bold">
|
<template #header>
|
||||||
|
<div class="w-full flex items-center justify-between">
|
||||||
Skills
|
Skills
|
||||||
<el-button link size="small" @click="router.push('/skills')">
|
<el-button link type="primary" size="small" @click="router.push('/skills')">
|
||||||
去管理
|
去管理
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-[var(--el-text-color-secondary)]">
|
</template>
|
||||||
|
<div class="text-xs text-ap-muted">
|
||||||
点击查看草稿与已发布版本的差异并发布;黄色为本次 Chat 刚改过的
|
点击查看草稿与已发布版本的差异并发布;黄色为本次 Chat 刚改过的
|
||||||
</div>
|
</div>
|
||||||
<div class="min-h-0 flex-1">
|
<div class="min-h-0 flex-1">
|
||||||
@@ -242,7 +244,7 @@ loadSkills()
|
|||||||
<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>
|
||||||
<el-tooltip content="下一轮发给模型的 system prompt(含已发布 skills、CONTEXT.md)与历史消息的估算大小" placement="bottom">
|
<el-tooltip content="下一轮发给模型的 system prompt(含已发布 skills、CONTEXT.md)与历史消息的估算大小" placement="bottom">
|
||||||
<span class="text-xs" :class="tokenRatio > 0.8 ? 'text-red-500 font-bold' : 'text-[var(--el-text-color-secondary)]'">
|
<span class="text-xs" :class="tokenRatio > 0.8 ? 'text-ap-down font-bold' : 'text-[var(--el-text-color-secondary)]'">
|
||||||
上下文约 {{ estimatedTokens.toLocaleString() }}{{ provider ? ` / ${provider.context_window.toLocaleString()}` : "" }} token
|
上下文约 {{ estimatedTokens.toLocaleString() }}{{ provider ? ` / ${provider.context_window.toLocaleString()}` : "" }} token
|
||||||
</span>
|
</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ loadLatest()
|
|||||||
{{ formatTime(source.last_success_at) }}
|
{{ formatTime(source.last_success_at) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="连续失败">
|
<el-descriptions-item label="连续失败">
|
||||||
<span :class="{ 'text-red-500 font-bold': source.consecutive_failures > 0 }">{{ source.consecutive_failures }}</span>
|
<span :class="{ 'text-ap-down font-bold': source.consecutive_failures > 0 }">{{ source.consecutive_failures }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="下次运行">
|
<el-descriptions-item label="下次运行">
|
||||||
{{ formatTime(source.paused_until && source.paused_until > (source.process?.next_run_at ?? "") ? source.paused_until : source.process?.next_run_at) }}
|
{{ formatTime(source.paused_until && source.paused_until > (source.process?.next_run_at ?? "") ? source.paused_until : source.process?.next_run_at) }}
|
||||||
@@ -157,7 +157,7 @@ loadLatest()
|
|||||||
</div>
|
</div>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item v-if="source.last_error" label="最近错误" :span="4">
|
<el-descriptions-item v-if="source.last_error" label="最近错误" :span="4">
|
||||||
<span class="whitespace-pre-wrap text-xs text-red-500">{{ source.last_error }}</span>
|
<span class="whitespace-pre-wrap text-xs text-ap-down">{{ source.last_error }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ loadLatest()
|
|||||||
</template>
|
</template>
|
||||||
<div v-for="item in latest" :key="item.id" class="mb-2 text-sm">
|
<div v-for="item in latest" :key="item.id" class="mb-2 text-sm">
|
||||||
<span class="mr-2 text-xs text-[var(--el-text-color-secondary)]">{{ formatTime(item.published_at, "MM-DD HH:mm") }}</span>
|
<span class="mr-2 text-xs text-[var(--el-text-color-secondary)]">{{ formatTime(item.published_at, "MM-DD HH:mm") }}</span>
|
||||||
<el-link :href="item.url" target="_blank">
|
<el-link :href="item.url" target="_blank" class="font-sans">
|
||||||
{{ item.title || item.excerpt.slice(0, 60) }}
|
{{ item.title || item.excerpt.slice(0, 60) }}
|
||||||
</el-link>
|
</el-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -189,10 +189,10 @@ loadLatest()
|
|||||||
<el-switch v-model="autoScroll" active-text="自动滚动" size="small" />
|
<el-switch v-model="autoScroll" active-text="自动滚动" size="small" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="logBox" class="min-h-0 flex-1 overflow-auto rounded bg-[#1e1e1e] p-2 text-xs text-[#d4d4d4] font-mono leading-5">
|
<div ref="logBox" class="min-h-0 flex-1 overflow-auto border border-ap-line border-solid bg-ap-sunken p-2 text-xs text-ap-text font-mono leading-5">
|
||||||
<div v-for="log in visibleLogs" :key="log.id" class="whitespace-pre-wrap break-all">
|
<div v-for="log in visibleLogs" :key="log.id" class="whitespace-pre-wrap break-all">
|
||||||
<span class="text-[#808080]">{{ formatTime(log.ts) }}</span>
|
<span class="text-ap-faint">{{ formatTime(log.ts) }}</span>
|
||||||
<span :class="{ 'text-yellow-400': log.level === 'WARNING', 'text-red-400': log.level === 'ERROR' || log.level === 'CRITICAL' }" class="mx-2">{{ log.level }}</span>{{ log.message }}
|
<span :class="{ 'text-ap-warn': log.level === 'WARNING', 'text-ap-down': log.level === 'ERROR' || log.level === 'CRITICAL' }" class="mx-2">{{ log.level }}</span>{{ log.message }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|||||||
@@ -73,14 +73,14 @@ refresh().then(() => Promise.all(info.value!.sources.map(s => loadHistory(s.code
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="info" class="h-full flex flex-col gap-3">
|
<div v-if="info" class="h-full flex flex-col gap-3">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3 border border-ap-line border-solid bg-ap-panel px-3 py-2">
|
||||||
<el-tag :type="info.manager_alive ? 'success' : 'danger'">
|
<el-tag :type="info.manager_alive ? 'success' : 'danger'">
|
||||||
{{ info.manager_alive ? "管理进程运行中" : "管理进程未运行" }}
|
<i class="mr-1.5 inline-block h-1.5 w-1.5 bg-current" :class="{ 'animate-pulse': info.manager_alive }" />{{ info.manager_alive ? "管理进程运行中" : "管理进程未运行" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-tag :type="connected ? 'success' : 'danger'">
|
<el-tag :type="connected ? 'success' : 'danger'">
|
||||||
{{ connected ? "日志推送已连接" : "日志推送断开" }}
|
<i class="mr-1.5 inline-block h-1.5 w-1.5 bg-current" />{{ connected ? "日志推送已连接" : "日志推送断开" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span class="text-sm text-[var(--el-text-color-secondary)]">数据库 {{ formatBytes(info.db_size_bytes) }}</span>
|
<span class="text-xs text-ap-muted">数据库 <b class="text-ap-text">{{ formatBytes(info.db_size_bytes) }}</b></span>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<el-button type="danger" :loading="restarting.size > 0" @click="restart('all')">
|
<el-button type="danger" :loading="restarting.size > 0" @click="restart('all')">
|
||||||
重启全部采集
|
重启全部采集
|
||||||
@@ -89,7 +89,7 @@ refresh().then(() => Promise.all(info.value!.sources.map(s => loadHistory(s.code
|
|||||||
<el-tabs v-model="activeTab" type="border-card" class="crawler-tabs min-h-0 flex flex-1 flex-col">
|
<el-tabs v-model="activeTab" type="border-card" class="crawler-tabs min-h-0 flex flex-1 flex-col">
|
||||||
<el-tab-pane v-for="source in info.sources" :key="source.code" :name="source.code" class="h-full">
|
<el-tab-pane v-for="source in info.sources" :key="source.code" :name="source.code" class="h-full">
|
||||||
<template #label>
|
<template #label>
|
||||||
<span :class="{ 'text-red-500 font-bold': source.consecutive_failures >= FAILURE_ALERT }">
|
<span :class="{ 'text-ap-down font-bold': source.consecutive_failures >= FAILURE_ALERT }">
|
||||||
{{ source.name }}
|
{{ source.name }}
|
||||||
</span>
|
</span>
|
||||||
<el-badge v-if="source.consecutive_failures >= FAILURE_ALERT" :value="source.consecutive_failures" class="ml-1" />
|
<el-badge v-if="source.consecutive_failures >= FAILURE_ALERT" :value="source.consecutive_failures" class="ml-1" />
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { DataLine } from "@element-plus/icons-vue"
|
||||||
import { useUserStore } from "@/pinia/user"
|
import { useUserStore } from "@/pinia/user"
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -20,14 +21,20 @@ async function submit() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full flex-center">
|
<div class="login-bg h-full flex-center">
|
||||||
<el-card class="w-90">
|
<div class="w-90 border border-ap-line border-solid bg-ap-panel">
|
||||||
<template #header>
|
<div class="h-14 flex flex-col justify-center border-b border-b-ap-line border-b-solid bg-ap-panel-high px-4">
|
||||||
<div class="text-center text-lg font-bold">
|
<div class="flex items-center gap-2 font-head text-lg text-ap-brand font-bold tracking-wider">
|
||||||
AlphaPulse
|
<el-icon :size="20">
|
||||||
|
<DataLine />
|
||||||
|
</el-icon>
|
||||||
|
ALPHAPULSE
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div class="text-[10px] text-ap-muted tracking-[0.2em]">
|
||||||
<el-form :model="form" @submit.prevent="submit">
|
新闻 · 行情 · AI 分析 · 模拟盘
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-form :model="form" class="p-5" @submit.prevent="submit">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-input v-model="form.username" placeholder="用户名" autocomplete="username" />
|
<el-input v-model="form.username" placeholder="用户名" autocomplete="username" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -38,6 +45,16 @@ async function submit() {
|
|||||||
登录
|
登录
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-bg {
|
||||||
|
background-color: var(--ap-bg);
|
||||||
|
background-image:
|
||||||
|
linear-gradient(color-mix(in srgb, var(--ap-line) 25%, transparent) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, color-mix(in srgb, var(--ap-line) 25%, transparent) 1px, transparent 1px);
|
||||||
|
background-size: 32px 32px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import type { DeepPartial, Period, Styles } from "klinecharts"
|
import type { DeepPartial, Period, Styles } from "klinecharts"
|
||||||
|
import { PALETTE } from "@/styles/palette"
|
||||||
|
|
||||||
export const UP = "#0ecb81"
|
export const UP = PALETTE.candleUp
|
||||||
export const DOWN = "#f6465d"
|
export const DOWN = PALETTE.candleDown
|
||||||
const GRID = "#2b3139"
|
const GRID = PALETTE.panelHighest
|
||||||
const TEXT = "#848e9c"
|
const AXIS = PALETTE.line
|
||||||
|
const TEXT = PALETTE.faint
|
||||||
|
// canvas 读不到 CSS 变量,字体直接写;未设置时 KLineChart 默认 Helvetica Neue,缺字体时会回退成衬线体
|
||||||
|
const FONT = `"JetBrains Mono Variable", ui-monospace, "PingFang SC", "Microsoft YaHei", monospace`
|
||||||
|
|
||||||
/** “分时”用 1m 数据画面积图,与币安一致 */
|
/** “分时”用 1m 数据画面积图,与币安一致 */
|
||||||
export const TIME_LINE = "time"
|
export const TIME_LINE = "time"
|
||||||
@@ -59,7 +63,7 @@ export function toPeriod(interval: string): Period {
|
|||||||
return { type, span }
|
return { type, span }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 在内置 dark 主题上覆盖币安配色 */
|
/** 在内置 dark 主题上覆盖:币安涨跌色 + 站点配色与字体 */
|
||||||
export const BINANCE_STYLES: DeepPartial<Styles> = {
|
export const BINANCE_STYLES: DeepPartial<Styles> = {
|
||||||
grid: {
|
grid: {
|
||||||
horizontal: { color: GRID },
|
horizontal: { color: GRID },
|
||||||
@@ -78,13 +82,22 @@ export const BINANCE_STYLES: DeepPartial<Styles> = {
|
|||||||
noChangeWickColor: TEXT
|
noChangeWickColor: TEXT
|
||||||
},
|
},
|
||||||
priceMark: {
|
priceMark: {
|
||||||
last: { upColor: UP, downColor: DOWN, noChangeColor: TEXT }
|
high: { textFamily: FONT },
|
||||||
}
|
low: { textFamily: FONT },
|
||||||
|
last: { upColor: UP, downColor: DOWN, noChangeColor: TEXT, text: { family: FONT } }
|
||||||
|
},
|
||||||
|
tooltip: { title: { family: FONT }, legend: { family: FONT } }
|
||||||
},
|
},
|
||||||
indicator: {
|
indicator: {
|
||||||
ohlc: { upColor: UP, downColor: DOWN, noChangeColor: TEXT }
|
ohlc: { upColor: UP, downColor: DOWN, noChangeColor: TEXT },
|
||||||
|
lastValueMark: { text: { family: FONT } },
|
||||||
|
tooltip: { title: { family: FONT }, legend: { family: FONT } }
|
||||||
},
|
},
|
||||||
xAxis: { axisLine: { color: GRID }, tickText: { color: TEXT } },
|
xAxis: { axisLine: { color: AXIS }, tickText: { color: TEXT, family: FONT } },
|
||||||
yAxis: { axisLine: { color: GRID }, tickText: { color: TEXT } },
|
yAxis: { axisLine: { color: AXIS }, tickText: { color: TEXT, family: FONT } },
|
||||||
separator: { color: GRID }
|
crosshair: {
|
||||||
|
horizontal: { text: { family: FONT } },
|
||||||
|
vertical: { text: { family: FONT } }
|
||||||
|
},
|
||||||
|
separator: { color: AXIS }
|
||||||
}
|
}
|
||||||
@@ -189,9 +189,9 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="market h-full flex flex-col overflow-hidden rounded bg-[#161a1e] text-[#eaecef]">
|
<div class="market h-full flex flex-col overflow-hidden border border-ap-line border-solid bg-ap-panel text-ap-text">
|
||||||
<!-- 顶栏:交易对与 24h 行情 -->
|
<!-- 顶栏:交易对与 24h 行情 -->
|
||||||
<div class="flex flex-wrap items-center gap-6 border-b border-b-solid border-[#2b3139] px-4 py-2">
|
<div class="flex flex-wrap items-center gap-6 border-b border-b-solid border-ap-line px-4 py-2">
|
||||||
<el-select
|
<el-select
|
||||||
:model-value="`${selection.market}:${selection.symbol}`" filterable class="symbol-select w-52"
|
:model-value="`${selection.market}:${selection.symbol}`" filterable class="symbol-select w-52"
|
||||||
@change="selectSymbol"
|
@change="selectSymbol"
|
||||||
@@ -202,7 +202,7 @@ onBeforeUnmount(() => {
|
|||||||
:value="`${s.market}:${s.symbol}`" :label="`${s.base}/${s.quote}${s.market === 'um' ? ' 永续' : ''}`"
|
:value="`${s.market}:${s.symbol}`" :label="`${s.base}/${s.quote}${s.market === 'um' ? ' 永续' : ''}`"
|
||||||
>
|
>
|
||||||
<span>{{ s.base }}/{{ s.quote }}</span>
|
<span>{{ s.base }}/{{ s.quote }}</span>
|
||||||
<span v-if="s.status !== 'ready'" class="float-right text-xs text-[#f0b90b]">{{ s.status === "error" ? "出错" : "补齐中" }}</span>
|
<span v-if="s.status !== 'ready'" class="float-right text-xs text-ap-brand">{{ s.status === "error" ? "出错" : "补齐中" }}</span>
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-option-group>
|
</el-option-group>
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -232,8 +232,8 @@ onBeforeUnmount(() => {
|
|||||||
<div>{{ formatBig(ticker.quoteVolume) }}</div>
|
<div>{{ formatBig(ticker.quoteVolume) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<span class="text-xs" :class="connected ? 'text-[#0ecb81]' : 'text-[#f6465d]'">● {{ connected ? "实时" : "断开" }}</span>
|
<span class="text-xs" :class="connected ? 'text-ap-up' : 'text-ap-down'">● {{ connected ? "实时" : "断开" }}</span>
|
||||||
<el-button size="small" type="warning" @click="addVisible = true">
|
<el-button size="small" type="primary" @click="addVisible = true">
|
||||||
新增交易对
|
新增交易对
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="small" @click="managerVisible = true">
|
<el-button size="small" @click="managerVisible = true">
|
||||||
@@ -242,7 +242,7 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 周期与指标工具栏 -->
|
<!-- 周期与指标工具栏 -->
|
||||||
<div class="flex items-center gap-1 border-b border-b-solid border-[#2b3139] px-3 py-1 text-sm">
|
<div class="flex items-center gap-1 border-b border-b-solid border-ap-line px-3 py-1 text-sm">
|
||||||
<span
|
<span
|
||||||
v-for="i in favorites.filter(f => intervals.includes(f))" :key="i"
|
v-for="i in favorites.filter(f => intervals.includes(f))" :key="i"
|
||||||
class="tool" :class="{ active: interval === i }" @click="interval = i"
|
class="tool" :class="{ active: interval === i }" @click="interval = i"
|
||||||
@@ -318,18 +318,18 @@ onBeforeUnmount(() => {
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<span v-if="backfillProgress !== null" class="text-xs text-[#f0b90b]">
|
<span v-if="backfillProgress !== null" class="text-xs text-ap-brand">
|
||||||
{{ INTERVAL_LABELS[dataInterval] }} 历史数据补齐中 {{ backfillProgress }}%,最新部分暂未完整
|
{{ INTERVAL_LABELS[dataInterval] }} 历史数据补齐中 {{ backfillProgress }}%,最新部分暂未完整
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="min-h-0 flex flex-1">
|
<div class="min-h-0 flex flex-1">
|
||||||
<!-- 画线工具栏 -->
|
<!-- 画线工具栏 -->
|
||||||
<div class="flex flex-col gap-1 border-r border-r-solid border-[#2b3139] p-1 text-xs">
|
<div class="flex flex-col gap-1 border-r border-r-solid border-ap-line p-1 text-xs">
|
||||||
<span v-for="tool in DRAW_TOOLS" :key="tool.name" class="tool text-center" :title="tool.label" @click="chart?.createOverlay(tool.name)">
|
<span v-for="tool in DRAW_TOOLS" :key="tool.name" class="tool text-center" :title="tool.label" @click="chart?.createOverlay(tool.name)">
|
||||||
{{ tool.label }}
|
{{ tool.label }}
|
||||||
</span>
|
</span>
|
||||||
<span class="tool text-center text-[#f6465d]" title="清除全部画线" @click="chart?.removeOverlay()">清除</span>
|
<span class="tool text-center text-ap-down" title="清除全部画线" @click="chart?.removeOverlay()">清除</span>
|
||||||
</div>
|
</div>
|
||||||
<div ref="chartEl" class="min-w-0 flex-1" />
|
<div ref="chartEl" class="min-w-0 flex-1" />
|
||||||
</div>
|
</div>
|
||||||
@@ -344,28 +344,27 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.stat > div:first-child {
|
.stat > div:first-child {
|
||||||
color: #848e9c;
|
color: var(--ap-muted);
|
||||||
}
|
}
|
||||||
.tool {
|
.tool {
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 4px;
|
color: var(--ap-muted);
|
||||||
color: #848e9c;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.tool:hover {
|
.tool:hover {
|
||||||
color: #eaecef;
|
color: var(--ap-text);
|
||||||
background: #2b3139;
|
background: var(--ap-panel-highest);
|
||||||
}
|
}
|
||||||
.tool.active {
|
.tool.active {
|
||||||
color: #f0b90b;
|
color: var(--ap-brand);
|
||||||
}
|
}
|
||||||
.symbol-select :deep(.el-select__wrapper) {
|
.symbol-select :deep(.el-select__wrapper) {
|
||||||
background: #2b3139;
|
background: var(--ap-panel-high);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.symbol-select :deep(.el-select__selected-item) {
|
.symbol-select :deep(.el-select__selected-item) {
|
||||||
color: #eaecef;
|
color: var(--ap-text);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import type { NewsDetail, Revision } from "@/apis/news"
|
import type { NewsDetail, Revision } from "@/apis/news"
|
||||||
import { getNews, listRevisions } from "@/apis/news"
|
import { getNews, listRevisions } from "@/apis/news"
|
||||||
import { useSourceNames } from "@/composables/useSourceNames"
|
import { useSourceNames } from "@/composables/useSourceNames"
|
||||||
|
import { PALETTE } from "@/styles/palette"
|
||||||
import { diffLines, formatTime, htmlToLines } from "@/utils/text"
|
import { diffLines, formatTime, htmlToLines } from "@/utils/text"
|
||||||
|
|
||||||
const newsId = defineModel<number | null>({ required: true })
|
const newsId = defineModel<number | null>({ required: true })
|
||||||
@@ -27,7 +28,7 @@ watch(newsId, async (id) => {
|
|||||||
// 第三方正文放进无脚本权限的 sandbox iframe,避免其中的脚本访问本站
|
// 第三方正文放进无脚本权限的 sandbox iframe,避免其中的脚本访问本站
|
||||||
const srcdoc = computed(() => detail.value && `<!doctype html><html><head><meta charset="utf-8">
|
const srcdoc = computed(() => detail.value && `<!doctype html><html><head><meta charset="utf-8">
|
||||||
<meta name="referrer" content="no-referrer"><base target="_blank">
|
<meta name="referrer" content="no-referrer"><base target="_blank">
|
||||||
<style>body{font:15px/1.8 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif;color:#303133;margin:0 4px}img{max-width:100%;height:auto}</style>
|
<style>:root{color-scheme:dark;background:${PALETTE.panel}}body{font:15px/1.8 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif;color:${PALETTE.text};margin:0 4px}a{color:${PALETTE.brand}}img{max-width:100%;height:auto}</style>
|
||||||
</head><body>${detail.value.content_html}</body></html>`)
|
</head><body>${detail.value.content_html}</body></html>`)
|
||||||
|
|
||||||
const diff = computed(() => {
|
const diff = computed(() => {
|
||||||
@@ -44,11 +45,11 @@ const diff = computed(() => {
|
|||||||
<template>
|
<template>
|
||||||
<el-drawer :model-value="newsId != null" size="50%" :with-header="false" @close="newsId = null">
|
<el-drawer :model-value="newsId != null" size="50%" :with-header="false" @close="newsId = null">
|
||||||
<div v-if="detail" class="h-full flex flex-col gap-3">
|
<div v-if="detail" class="h-full flex flex-col gap-3">
|
||||||
<div class="text-lg font-bold">
|
<div class="font-sans text-lg font-bold">
|
||||||
{{ detail.title || detail.excerpt.slice(0, 60) }}
|
{{ detail.title || detail.excerpt.slice(0, 60) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2 text-sm text-[var(--el-text-color-secondary)]">
|
<div class="flex flex-wrap items-center gap-2 text-sm text-[var(--el-text-color-secondary)]">
|
||||||
<el-tag size="small">
|
<el-tag size="small" type="info">
|
||||||
{{ sourceNames[detail.source_code] ?? detail.source_code }}
|
{{ sourceNames[detail.source_code] ?? detail.source_code }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span>{{ formatTime(detail.published_at, "YYYY-MM-DD HH:mm:ss") }}</span>
|
<span>{{ formatTime(detail.published_at, "YYYY-MM-DD HH:mm:ss") }}</span>
|
||||||
@@ -76,7 +77,7 @@ const diff = computed(() => {
|
|||||||
<div class="text-sm leading-7">
|
<div class="text-sm leading-7">
|
||||||
<div
|
<div
|
||||||
v-for="(line, i) in diff" :key="i"
|
v-for="(line, i) in diff" :key="i"
|
||||||
:class="{ 'bg-green-100': line.type === 'add', 'bg-red-100 line-through': line.type === 'del' }"
|
:class="{ 'diff-add': line.type === 'add', 'diff-del': line.type === 'del' }"
|
||||||
class="px-2"
|
class="px-2"
|
||||||
>
|
>
|
||||||
{{ line.type === "add" ? "+ " : line.type === "del" ? "- " : " " }}{{ line.text }}
|
{{ line.type === "add" ? "+ " : line.type === "del" ? "- " : " " }}{{ line.text }}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ load()
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full flex flex-col gap-3">
|
<div class="h-full flex flex-col gap-3">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3 border border-ap-line border-solid bg-ap-panel px-3 py-2">
|
||||||
<el-checkbox-group v-model="filters.source" @change="search">
|
<el-checkbox-group v-model="filters.source" @change="search">
|
||||||
<el-checkbox-button v-for="(name, code) in sourceNames" :key="code" :value="code">
|
<el-checkbox-button v-for="(name, code) in sourceNames" :key="code" :value="code">
|
||||||
{{ name }}
|
{{ name }}
|
||||||
@@ -85,8 +85,9 @@ load()
|
|||||||
<el-button type="primary" @click="search">
|
<el-button type="primary" @click="search">
|
||||||
查询
|
查询
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<div class="flex-1" />
|
||||||
<el-tag :type="connected ? 'success' : 'danger'" size="small">
|
<el-tag :type="connected ? 'success' : 'danger'" size="small">
|
||||||
{{ connected ? "实时推送已连接" : "实时推送断开" }}
|
<i class="mr-1.5 inline-block h-1.5 w-1.5 bg-current" />{{ connected ? "实时推送已连接" : "实时推送断开" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -95,14 +96,14 @@ load()
|
|||||||
class="cursor-pointer" @click="search"
|
class="cursor-pointer" @click="search"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div ref="listBox" v-loading="loading" class="min-h-0 flex-1 overflow-auto">
|
<div ref="listBox" v-loading="loading" class="min-h-0 flex-1 overflow-auto border border-ap-line border-solid bg-ap-panel">
|
||||||
<div
|
<div
|
||||||
v-for="item in items" :key="item.id"
|
v-for="item in items" :key="item.id"
|
||||||
class="cursor-pointer border-b border-b-solid border-[var(--el-border-color-lighter)] px-2 py-3 hover:bg-[var(--el-fill-color-light)]"
|
class="cursor-pointer border-b border-b-ap-line/50 border-b-solid px-3 py-2.5 hover:bg-ap-panel-high"
|
||||||
@click="openedId = item.id"
|
@click="openedId = item.id"
|
||||||
>
|
>
|
||||||
<div class="mb-1 flex items-center gap-2 text-xs text-[var(--el-text-color-secondary)]">
|
<div class="mb-1 flex items-center gap-2 text-xs text-[var(--el-text-color-secondary)]">
|
||||||
<el-tag size="small">
|
<el-tag size="small" type="info">
|
||||||
{{ sourceNames[item.source_code] ?? item.source_code }}
|
{{ sourceNames[item.source_code] ?? item.source_code }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span>{{ formatTime(item.published_at) }}</span>
|
<span>{{ formatTime(item.published_at) }}</span>
|
||||||
@@ -113,10 +114,10 @@ load()
|
|||||||
付费
|
付费
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.title" class="font-bold">
|
<div v-if="item.title" class="mb-0.5 font-sans text-[14px] font-bold">
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="line-clamp-3 text-sm text-[var(--el-text-color-regular)]">
|
<div class="line-clamp-3 font-sans text-[13px] text-ap-muted leading-relaxed">
|
||||||
{{ item.summary || item.excerpt }}
|
{{ item.summary || item.excerpt }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -131,18 +131,20 @@ loadExchange()
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="flex items-center justify-between">
|
<el-card shadow="never" body-class="p-0!">
|
||||||
|
<template #header>
|
||||||
|
<div class="w-full flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-lg font-bold">模型配置</span>
|
模型配置
|
||||||
<span class="ml-3 text-xs text-[var(--el-text-color-secondary)]">通过 Sub2API 等中转接入;API key 加密存储,页面只显示末 4 位</span>
|
<span class="ml-3 text-[11px] text-ap-muted font-normal font-mono">通过 Sub2API 等中转接入;API key 加密存储,页面只显示末 4 位</span>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" @click="openCreate">
|
<el-button size="small" type="primary" @click="openCreate">
|
||||||
新增配置
|
新增配置
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
<el-table :data="providers" border>
|
<el-table :data="providers">
|
||||||
<el-table-column label="名称" min-width="120">
|
<el-table-column label="名称" min-width="110">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.name }}
|
{{ row.name }}
|
||||||
<el-tag v-if="row.is_default" size="small" type="success" class="ml-1">
|
<el-tag v-if="row.is_default" size="small" type="success" class="ml-1">
|
||||||
@@ -150,29 +152,29 @@ loadExchange()
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="协议" min-width="200">
|
<el-table-column label="协议" min-width="230" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ PROTOCOL_LABELS[row.protocol as keyof typeof PROTOCOL_LABELS] }}
|
{{ PROTOCOL_LABELS[row.protocol as keyof typeof PROTOCOL_LABELS] }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="base_url" label="Base URL" min-width="200" />
|
<el-table-column prop="base_url" label="Base URL" min-width="220" show-overflow-tooltip />
|
||||||
<el-table-column prop="model" label="模型" min-width="150" />
|
<el-table-column prop="model" label="模型" min-width="160" show-overflow-tooltip />
|
||||||
<el-table-column label="思考强度" width="90">
|
<el-table-column label="思考强度" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ (row as Provider).reasoning_effort ?? "-" }}
|
{{ (row as Provider).reasoning_effort ?? "-" }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="API Key" width="100">
|
<el-table-column label="API Key" width="90">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
****{{ row.api_key_hint }}
|
****{{ row.api_key_hint }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="更新时间" width="130">
|
<el-table-column label="更新时间" width="110">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatTime(row.updated_at, "MM-DD HH:mm") }}
|
{{ formatTime(row.updated_at, "MM-DD HH:mm") }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="250" fixed="right">
|
<el-table-column label="操作" width="220" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" :loading="testing === row.id" @click="test(row as Provider)">
|
<el-button link type="primary" :loading="testing === row.id" @click="test(row as Provider)">
|
||||||
测试连接
|
测试连接
|
||||||
@@ -189,6 +191,7 @@ loadExchange()
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<el-dialog v-model="dialogVisible" :title="editingId === null ? '新增模型配置' : '编辑模型配置'" width="560px">
|
<el-dialog v-model="dialogVisible" :title="editingId === null ? '新增模型配置' : '编辑模型配置'" width="560px">
|
||||||
<el-form :model="form" label-width="100px">
|
<el-form :model="form" label-width="100px">
|
||||||
@@ -245,24 +248,27 @@ loadExchange()
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<el-card shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="w-full flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-lg font-bold">币安模拟盘</span>
|
币安模拟盘
|
||||||
<span class="ml-3 text-xs text-[var(--el-text-color-secondary)]">只连接币安模拟盘(demo-fapi.binance.com);在 demo.binance.com 的 API 管理创建 key,并开启合约交易权限</span>
|
<span class="ml-3 text-[11px] text-ap-muted font-normal font-mono">只连接币安模拟盘(demo-fapi.binance.com);在 demo.binance.com 的 API 管理创建 key,并开启合约交易权限</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex shrink-0">
|
||||||
<el-button :disabled="!exchange?.configured" :loading="exchangeTesting" @click="runExchangeTest">
|
<el-button size="small" :disabled="!exchange?.configured" :loading="exchangeTesting" @click="runExchangeTest">
|
||||||
测试连接
|
测试连接
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="primary" :disabled="exchange?.running" @click="openExchangeEdit">
|
<el-button size="small" type="primary" :disabled="exchange?.running" @click="openExchangeEdit">
|
||||||
{{ exchange?.configured ? "修改 key" : "填写 key" }}
|
{{ exchange?.configured ? "修改 key" : "填写 key" }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-if="exchange?.configured" type="danger" plain :disabled="exchange.running" @click="removeExchange">
|
<el-button v-if="exchange?.configured" size="small" type="danger" plain :disabled="exchange.running" @click="removeExchange">
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-descriptions v-if="exchange" :column="3" border>
|
</template>
|
||||||
|
<el-descriptions v-if="exchange" :column="3" border size="small">
|
||||||
<el-descriptions-item label="API Key">
|
<el-descriptions-item label="API Key">
|
||||||
{{ exchange.configured ? `****${exchange.api_key_hint}` : "未配置" }}
|
{{ exchange.configured ? `****${exchange.api_key_hint}` : "未配置" }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -276,6 +282,7 @@ loadExchange()
|
|||||||
{{ exchange.updated_at ? formatTime(exchange.updated_at, "MM-DD HH:mm") : "-" }}
|
{{ exchange.updated_at ? formatTime(exchange.updated_at, "MM-DD HH:mm") : "-" }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<el-dialog v-model="exchangeDialogVisible" title="币安模拟盘 API key" width="560px">
|
<el-dialog v-model="exchangeDialogVisible" title="币安模拟盘 API key" width="560px">
|
||||||
<el-form :model="exchangeForm" label-width="100px">
|
<el-form :model="exchangeForm" label-width="100px">
|
||||||
|
|||||||
@@ -145,9 +145,9 @@ loadTree()
|
|||||||
<div class="min-h-0 flex flex-1 gap-3">
|
<div class="min-h-0 flex flex-1 gap-3">
|
||||||
<textarea
|
<textarea
|
||||||
v-model="content" placeholder="用 Markdown 写下投资经验、判断流程或输出规范"
|
v-model="content" placeholder="用 Markdown 写下投资经验、判断流程或输出规范"
|
||||||
class="box-border flex-1 resize-none border rounded border-solid border-[var(--el-border-color)] p-3 text-sm leading-6 font-mono outline-none focus:border-[var(--el-color-primary)]"
|
class="box-border flex-1 resize-none border border-ap-line border-solid bg-ap-sunken p-3 text-sm text-ap-text leading-6 font-mono outline-none focus:border-ap-brand"
|
||||||
/>
|
/>
|
||||||
<el-scrollbar class="flex-1 border rounded border-solid border-[var(--el-border-color-lighter)]">
|
<el-scrollbar class="flex-1 border border-ap-line border-solid bg-ap-panel-low">
|
||||||
<!-- markdown-it 已关闭 html,内容中的标签按文本显示 -->
|
<!-- markdown-it 已关闭 html,内容中的标签按文本显示 -->
|
||||||
<div class="markdown-body p-3" v-html="preview" />
|
<div class="markdown-body p-3" v-html="preview" />
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { AnalysisDetail } from "@/apis/trading"
|
import type { AnalysisDetail } from "@/apis/trading"
|
||||||
|
import { Cpu } from "@element-plus/icons-vue"
|
||||||
import { getAnalysis } from "@/apis/trading"
|
import { getAnalysis } from "@/apis/trading"
|
||||||
import { renderMarkdown } from "@/utils/markdown"
|
import { renderMarkdown } from "@/utils/markdown"
|
||||||
import { formatTime } from "@/utils/text"
|
import { formatDuration, formatTime } from "@/utils/text"
|
||||||
|
|
||||||
const props = defineProps<{ analysisId: number | null }>()
|
const props = defineProps<{ analysisId: number | null }>()
|
||||||
const visible = defineModel<boolean>({ required: true })
|
const visible = defineModel<boolean>({ required: true })
|
||||||
@@ -36,17 +37,59 @@ function pretty(raw: string | undefined) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-drawer v-model="visible" :title="detail ? `分析 #${detail.id} · ${detail.symbol}` : '分析'" size="55%">
|
<el-drawer v-model="visible" size="55%">
|
||||||
|
<template #header>
|
||||||
|
<div class="min-w-0 flex items-center gap-2">
|
||||||
|
<el-icon class="text-ap-brand" :size="18">
|
||||||
|
<Cpu />
|
||||||
|
</el-icon>
|
||||||
|
<div class="min-w-0 flex flex-col">
|
||||||
|
<span class="font-head text-sm text-ap-text font-bold">{{ detail ? `分析 #${detail.id} · ${detail.symbol}` : "分析" }}</span>
|
||||||
|
<span v-if="detail" class="truncate text-[10px] text-ap-muted">
|
||||||
|
{{ formatTime(detail.started_at) }} – {{ formatTime(detail.finished_at) }} · {{ detail.reasons.join(";") }}{{ detail.dry_run ? " · 试运行" : "" }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<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 border border-ap-line border-solid bg-ap-sunken p-3 text-xs">
|
||||||
{{ formatTime(detail.started_at) }} – {{ formatTime(detail.finished_at) }} · {{ detail.reasons.join(";") }}
|
<div>
|
||||||
· {{ detail.model }}{{ detail.reasoning_effort ? ` / ${detail.reasoning_effort}` : "" }}
|
<div class="text-[10px] text-ap-muted">
|
||||||
· {{ detail.usage?.input_tokens.toLocaleString() }} / {{ detail.usage?.output_tokens.toLocaleString() }} token
|
分析 ID
|
||||||
{{ detail.dry_run ? "· 试运行" : "" }}
|
</div>
|
||||||
|
<b>#{{ detail.id }}</b>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-[10px] text-ap-muted">
|
||||||
|
耗时
|
||||||
|
</div>
|
||||||
|
<b class="text-ap-brand">{{ detail.finished_at ? formatDuration(detail.started_at, detail.finished_at) : "-" }}</b>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-[10px] text-ap-muted">
|
||||||
|
token 输入 / 输出
|
||||||
|
</div>
|
||||||
|
<b>{{ detail.usage?.input_tokens.toLocaleString() ?? "-" }} / {{ detail.usage?.output_tokens.toLocaleString() ?? "-" }}</b>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-[10px] text-ap-muted">
|
||||||
|
模型 / 思考强度
|
||||||
|
</div>
|
||||||
|
<b class="text-ap-up">{{ detail.model ?? "-" }}{{ detail.reasoning_effort ? ` / ${detail.reasoning_effort}` : "" }}</b>
|
||||||
|
</div>
|
||||||
</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" class="bg-ap-panel-low!">
|
||||||
|
<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" class="bg-ap-panel-low!" body-class="p-0!">
|
||||||
|
<template #header>
|
||||||
|
输入数据与工具调用 ({{ toolResults.size }})
|
||||||
|
</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 ? '发给模型的数据(账户、持仓、预取行情)' : '程序提示'">
|
||||||
@@ -55,13 +98,17 @@ function pretty(raw: string | undefined) {
|
|||||||
<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="text-xs">
|
||||||
|
<b class="text-ap-up">{{ call.name }}</b>
|
||||||
|
<span class="ml-2 text-ap-muted">{{ JSON.stringify(call.arguments) }}</span>
|
||||||
|
</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 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>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<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 { Clock, Cpu, Refresh, TopRight, Warning } 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 { formatDuration, formatTime } from "@/utils/text"
|
||||||
|
|
||||||
/** analysis:执行中的计划,没有时为最近一次分析;latest:最近一次分析(与 analysis 不同时另行提示) */
|
/** analysis:执行中的计划,没有时为最近一次分析;latest:最近一次分析(与 analysis 不同时另行提示) */
|
||||||
const props = defineProps<{ symbol: TradingSymbol, analysis: Analysis | null, latest: Analysis | null, orders: OrderRow[], requesting: boolean }>()
|
const props = defineProps<{ symbol: TradingSymbol, analysis: Analysis | null, latest: Analysis | null, orders: OrderRow[], requesting: boolean }>()
|
||||||
@@ -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 ? formatDuration(props.analysis.started_at, props.analysis.finished_at) : "")
|
||||||
|
|
||||||
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)
|
||||||
@@ -55,55 +57,64 @@ const ladder = computed(() => sizing.value?.entry && entryRow.value
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="border border-[var(--el-border-color)] rounded border-solid p-3">
|
<div class="border border-ap-line border-solid bg-ap-panel-low p-4">
|
||||||
<div class="mb-2 flex items-center gap-2">
|
<div class="mb-3 flex items-center gap-2 border-b border-b-ap-line border-b-solid pb-2.5">
|
||||||
<span class="font-bold">{{ symbol.symbol }}</span>
|
<el-icon class="text-ap-brand" :size="18">
|
||||||
<span class="text-xs text-[var(--el-text-color-secondary)]">短线做波动(区间内挂限价单,最长持仓 4 小时)</span>
|
<Cpu />
|
||||||
|
</el-icon>
|
||||||
|
<span class="font-head text-base font-bold">{{ symbol.symbol }}</span>
|
||||||
|
<span class="text-xs text-ap-muted">短线做波动(区间内挂限价单,最长持仓 4 小时)</span>
|
||||||
<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-link v-if="analysis && analysis.status !== 'running'" type="primary" @click="emit('detail', analysis.id)">
|
||||||
查看完整分析
|
查看完整分析
|
||||||
</el-button>
|
<el-icon class="ml-0.5">
|
||||||
<el-button size="small" type="primary" :loading="busy" @click="emit('analyze')">
|
<TopRight />
|
||||||
|
</el-icon>
|
||||||
|
</el-link>
|
||||||
|
<el-button type="primary" :loading="busy" @click="emit('analyze')">
|
||||||
{{ busy ? "分析中" : "立即分析" }}
|
{{ busy ? "分析中" : "立即分析" }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!analysis" class="text-sm text-[var(--el-text-color-secondary)]">
|
<div v-if="!analysis" class="state-strip">
|
||||||
还没有分析。引擎停止时点「立即分析」为试运行:计划只记录,不下单。
|
还没有分析。引擎停止时点「立即分析」为试运行:计划只记录,不下单。
|
||||||
</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="state-strip">
|
||||||
模型分析中,已用 {{ elapsed }} 秒(通常 1–3 分钟)…
|
<i class="mr-2.5 inline-block h-1.5 w-1.5 animate-ping bg-ap-brand" />模型分析中,已用 {{ 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">
|
<div v-else-if="decision" class="flex flex-col gap-3">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<el-tag v-if="analysis.dry_run" type="info" size="small">
|
<el-tag v-if="analysis.dry_run" type="info">
|
||||||
试运行
|
试运行
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-tag v-if="analysis.plan_status" type="success" effect="dark" size="small">
|
<el-tag v-if="analysis.plan_status" type="success">
|
||||||
{{ PLAN_STATUS_LABELS[analysis.plan_status] }}
|
{{ PLAN_STATUS_LABELS[analysis.plan_status] }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-tag size="small">
|
<el-tag type="primary">
|
||||||
{{ ACTION_LABELS[decision.action] }}
|
{{ ACTION_LABELS[decision.action] }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-tag v-if="decision.direction" :type="decision.direction === 'long' ? 'success' : 'danger'" size="small">
|
<el-tag v-if="decision.direction" :type="decision.direction === 'long' ? 'success' : 'danger'">
|
||||||
{{ decision.direction === "long" ? "做多" : "做空" }}
|
{{ decision.direction === "long" ? "做多" : "做空" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-tag v-if="decision.confidence" type="warning" size="small">
|
<el-tag v-if="decision.confidence" type="warning">
|
||||||
置信度{{ CONFIDENCE_LABELS[decision.confidence] }}
|
置信度{{ CONFIDENCE_LABELS[decision.confidence] }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span class="text-xs text-[var(--el-text-color-secondary)]">
|
<span class="text-[11px] text-ap-muted">
|
||||||
{{ formatTime(analysis.started_at) }} · {{ analysis.reasons.join(";") }} · {{ analysis.model }}{{ analysis.reasoning_effort ? ` / ${analysis.reasoning_effort}` : "" }}
|
{{ formatTime(analysis.started_at) }} · {{ analysis.reasons.join(";") }} · {{ analysis.model }}{{ analysis.reasoning_effort ? ` / ${analysis.reasoning_effort}` : "" }}
|
||||||
|
{{ duration ? `· 耗时 ${duration}` : "" }}
|
||||||
· {{ analysis.usage?.input_tokens.toLocaleString() }} / {{ analysis.usage?.output_tokens.toLocaleString() }} token
|
· {{ analysis.usage?.input_tokens.toLocaleString() }} / {{ analysis.usage?.output_tokens.toLocaleString() }} token
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-bold">
|
|
||||||
|
<div class="border-l-4 border-l-ap-brand border-l-solid bg-ap-panel px-3 py-2.5 font-sans text-[15px] font-bold">
|
||||||
{{ decision.summary }}
|
{{ decision.summary }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="latest && latest.id !== analysis.id" class="text-xs text-[var(--el-text-color-secondary)]">
|
|
||||||
|
<div v-if="latest && latest.id !== analysis.id" class="text-xs text-ap-muted">
|
||||||
最近一次分析 #{{ 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 }}
|
||||||
@@ -111,56 +122,106 @@ const ladder = computed(() => sizing.value?.entry && entryRow.value
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
{{ latest.status === "running" ? "分析中" : `失败:${latest.error}` }}
|
{{ latest.status === "running" ? "分析中" : `失败:${latest.error}` }}
|
||||||
</template>
|
</template>
|
||||||
<el-link type="primary" underline="never" class="ml-1 text-xs!" @click="emit('detail', latest.id)">
|
<el-link type="primary" class="ml-1 text-xs!" @click="emit('detail', latest.id)">
|
||||||
查看
|
查看
|
||||||
</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>
|
<div v-if="analysis.valid_until || analysis.first_fill_at || analysis.close_deadline" class="flex flex-wrap items-center gap-x-5 gap-y-1 border border-ap-line border-solid bg-ap-sunken px-3 py-1.5 text-xs">
|
||||||
<span v-if="analysis.first_fill_at"><span class="label">入场</span>{{ formatTime(analysis.first_fill_at, "MM-DD HH:mm") }}</span>
|
<el-icon class="text-ap-up" :size="15">
|
||||||
<span v-if="analysis.close_deadline"><span class="label">最晚平仓</span>{{ formatTime(analysis.close_deadline, "MM-DD HH:mm") }}(剩 {{ countdown(analysis.close_deadline) }})</span>
|
<Clock />
|
||||||
|
</el-icon>
|
||||||
|
<span v-if="analysis.plan_status === 'pending' && analysis.valid_until">
|
||||||
|
限价单有效至 {{ formatTime(analysis.valid_until, "MM-DD HH:mm") }}<b class="ml-1 text-ap-brand">(剩 {{ countdown(analysis.valid_until) }})</b>
|
||||||
|
</span>
|
||||||
|
<span v-if="analysis.first_fill_at">入场 {{ formatTime(analysis.first_fill_at, "MM-DD HH:mm") }}</span>
|
||||||
|
<span v-if="analysis.close_deadline">
|
||||||
|
最晚平仓 {{ formatTime(analysis.close_deadline, "MM-DD HH:mm") }}<b class="ml-1 text-ap-brand">(剩 {{ countdown(analysis.close_deadline) }})</b>
|
||||||
|
</span>
|
||||||
</div>
|
</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 v-if="analysis.result" class="text-sm">
|
||||||
|
<span class="label">结果</span>{{ CLOSE_REASON_LABELS[analysis.close_reason ?? ""] ?? analysis.close_reason }},净盈亏
|
||||||
|
<b :class="Number(analysis.result.net) >= 0 ? 'text-ap-up' : 'text-ap-down'">{{ analysis.result.net }} USDT</b>(手续费 {{ analysis.result.commission }})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="font-sans text-[13px] leading-relaxed">
|
||||||
|
<span class="label">为什么</span>{{ decision.reasons }}
|
||||||
</div>
|
</div>
|
||||||
<div><span class="label">为什么</span>{{ decision.reasons }}</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="grid grid-cols-2 border border-ap-line border-solid bg-ap-panel-high text-xs">
|
||||||
<div>
|
<div class="flex flex-col gap-1.5 p-3">
|
||||||
<span class="label">{{ long ? "支撑区" : "压力区" }}</span>{{ sizing.setup.zone_low }} – {{ sizing.setup.zone_high }} · {{ decision.setup.basis }}
|
<div class="param">
|
||||||
|
<span>{{ long ? "支撑区" : "压力区" }}</span>
|
||||||
|
<b :class="long ? 'text-ap-up' : 'text-ap-down'">{{ sizing.setup.zone_low }} – {{ sizing.setup.zone_high }}</b>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="font-sans text-[11px] text-ap-muted">
|
||||||
<span class="label">限价{{ long ? "做多" : "做空" }}</span>{{ sizing.entry.price }} × {{ sizing.entry.quantity }},已成交 {{ filled("entry", 0) }}{{ entryRow?.avg_price ? `,均价 ${entryRow.avg_price}` : "" }}
|
{{ decision.setup.basis }}
|
||||||
(名义 {{ sizing.entry.notional }} USDT,{{ sizing.entry.leverage }} 倍交易资金)
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="param">
|
||||||
<span class="label">止损</span>{{ currentStop ?? sizing.entry.stop }}(初始 {{ sizing.entry.stop }},距挂单价 {{ sizing.entry.stop_pct }}%;区间远端外 0.5×ATR(5m)、距挂单价至少 1×ATR(5m),止盈 1 后移到保本价)
|
<span>限价{{ long ? "做多" : "做空" }}</span>
|
||||||
|
<b>{{ sizing.entry.price }} × {{ sizing.entry.quantity }}</b>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="text-[11px] text-ap-muted">
|
||||||
<span class="label">止损亏损</span>{{ sizing.entry.risk_at_stop }} USDT(交易资金的 {{ sizing.entry.risk_pct }}%){{ sizing.entry.capped_by_leverage ? ",受 3 倍上限限制" : "" }}
|
已成交 {{ filled("entry", 0) }}{{ entryRow?.avg_price ? `,均价 ${entryRow.avg_price}` : "" }};名义 {{ sizing.entry.notional }} USDT,{{ sizing.entry.leverage }} 倍交易资金
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-table v-if="ladder.length" :data="ladder" border size="small">
|
<div class="flex flex-col gap-1.5 border-l border-l-ap-line border-l-solid p-3">
|
||||||
|
<div class="param">
|
||||||
|
<span>止损</span>
|
||||||
|
<b class="text-ap-down">{{ currentStop ?? sizing.entry.stop }}</b>
|
||||||
|
</div>
|
||||||
|
<div class="param">
|
||||||
|
<span>初始止损 / 距挂单价</span>
|
||||||
|
<span>{{ sizing.entry.stop }} / {{ sizing.entry.stop_pct }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="param">
|
||||||
|
<span>止损亏损</span>
|
||||||
|
<b class="text-ap-down">{{ sizing.entry.risk_at_stop }} USDT(交易资金的 {{ sizing.entry.risk_pct }}%)</b>
|
||||||
|
</div>
|
||||||
|
<div class="text-[11px] text-ap-muted">
|
||||||
|
区间远端外 0.5×ATR(5m)、距挂单价至少 1×ATR(5m);止盈 1 后移到保本价{{ sizing.entry.capped_by_leverage ? ";受 3 倍上限限制" : "" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="ladder.length">
|
||||||
|
<div class="panel-title mb-1.5">
|
||||||
|
分批止盈
|
||||||
|
</div>
|
||||||
|
<el-table :data="ladder" border size="small">
|
||||||
<el-table-column label="分批止盈" min-width="90">
|
<el-table-column label="分批止盈" min-width="90">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
止盈 {{ row.leg + 1 }}
|
<b class="text-ap-up">止盈 {{ row.leg + 1 }}</b>
|
||||||
</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" align="right" />
|
||||||
<el-table-column prop="ratio" label="平仓比例" min-width="80" />
|
<el-table-column prop="ratio" label="平仓比例" min-width="80" align="right" />
|
||||||
<el-table-column prop="price" label="价格" min-width="100" />
|
<el-table-column label="价格" min-width="100" align="right">
|
||||||
<el-table-column prop="filled" label="已成交" min-width="80" />
|
<template #default="{ row }">
|
||||||
|
<b>{{ row.price }}</b>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="filled" label="已成交" min-width="80" align="center" />
|
||||||
</el-table>
|
</el-table>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="decision.reevaluate.length">
|
<div class="flex flex-col gap-1.5 border border-ap-line border-solid bg-ap-sunken p-2.5 font-sans text-xs">
|
||||||
<span class="label">重新判断</span>
|
<div v-for="(c, i) in decision.reevaluate" :key="i" class="flex items-start gap-1.5 text-ap-brand">
|
||||||
<div v-for="(c, i) in decision.reevaluate" :key="i" class="ml-2">
|
<el-icon class="mt-0.5 shrink-0">
|
||||||
· {{ condition(c) }}:{{ c.basis }}
|
<Refresh />
|
||||||
|
</el-icon>
|
||||||
|
<span><b>重新判断:</b>{{ condition(c) }}:{{ c.basis }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-1.5 text-ap-down" :class="{ 'border-t border-t-ap-line border-t-solid pt-1.5': decision.reevaluate.length }">
|
||||||
|
<el-icon class="mt-0.5 shrink-0">
|
||||||
|
<Warning />
|
||||||
|
</el-icon>
|
||||||
|
<span><b>风险:</b>{{ decision.risks }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div><span class="label">风险</span>{{ decision.risks }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -168,7 +229,28 @@ const ladder = computed(() => sizing.value?.entry && entryRow.value
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.label {
|
.label {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--ap-muted);
|
||||||
|
font-family: var(--ap-font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.state-strip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--ap-line);
|
||||||
|
background: var(--ap-panel-highest);
|
||||||
|
color: var(--ap-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.param {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, var(--ap-line) 60%, transparent);
|
||||||
|
}
|
||||||
|
.param > span:first-child {
|
||||||
|
color: var(--ap-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
+195
-114
@@ -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 { DataAnalysis } 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"
|
||||||
@@ -11,7 +12,7 @@ import PlanCard from "./components/PlanCard.vue"
|
|||||||
|
|
||||||
const STATUS_REFRESH_MS = 3000
|
const STATUS_REFRESH_MS = 3000
|
||||||
const EVENTS_PAGE = 50
|
const EVENTS_PAGE = 50
|
||||||
const LEVEL_TYPES = { info: "primary", warn: "warning", error: "danger" } as const
|
const LEVEL_DOTS = { info: "bg-ap-faint", warn: "bg-ap-warn", error: "bg-ap-down" } as const
|
||||||
|
|
||||||
const status = ref<TradingStatus>()
|
const status = ref<TradingStatus>()
|
||||||
const symbols = ref<TradingSymbol[]>([])
|
const symbols = ref<TradingSymbol[]>([])
|
||||||
@@ -52,17 +53,33 @@ const symbolOrders = computed(() => {
|
|||||||
].sort((a, b) => b.time - a.time)
|
].sort((a, b) => b.time - a.time)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 价格、数量:千分位,保留原有小数 */
|
||||||
function num(value: string) {
|
function num(value: string) {
|
||||||
return String(Number(value))
|
return Number(value).toLocaleString("en-US", { maximumFractionDigits: 8 })
|
||||||
}
|
}
|
||||||
|
|
||||||
function money(value: string | undefined) {
|
function money(value: string | undefined) {
|
||||||
return value === undefined ? "-" : Number(value).toFixed(2)
|
return value === undefined ? "-" : Number(value).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function signedMoney(value: string | undefined) {
|
||||||
|
return value !== undefined && Number(value) > 0 ? `+${money(value)}` : money(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pnlClass(value: string) {
|
function pnlClass(value: string) {
|
||||||
const n = Number(value)
|
const n = Number(value)
|
||||||
return n > 0 ? "text-[var(--el-color-success)]" : n < 0 ? "text-[var(--el-color-danger)]" : ""
|
return n > 0 ? "text-ap-up" : n < 0 ? "text-ap-down" : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const winRate = computed(() => {
|
||||||
|
const { trades, wins } = status.value!.today
|
||||||
|
return trades ? `${(wins / trades * 100).toFixed(1)}%` : "-"
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 交易币种列表第二行:该币的持仓 */
|
||||||
|
function positionLine(symbol: string) {
|
||||||
|
const p = positions.value.find(x => x.symbol === symbol)
|
||||||
|
return p ? { text: `${p.leverage}x ${p.side === "LONG" ? "多" : "空"} · ${num(p.amount)}`, long: p.side === "LONG" } : null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 持仓当前生效的止损、止盈触发价:取交易所上平仓方向的挂单 */
|
/** 持仓当前生效的止损、止盈触发价:取交易所上平仓方向的挂单 */
|
||||||
@@ -192,10 +209,10 @@ 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 ? String(Number(status.value!.capital_usdt)) : "",
|
||||||
inputPattern: /^(?:[1-9]\d*|0)(?:\.\d{1,2})?$/,
|
inputPattern: /^(?:[1-9]\d*|0)(?:\.\d{1,2})?$/,
|
||||||
inputErrorMessage: "请输入正数,最多两位小数",
|
inputErrorMessage: "请输入正数,最多两位小数",
|
||||||
confirmButtonText: "保存"
|
confirmButtonText: "保存"
|
||||||
@@ -237,68 +254,71 @@ onBeforeUnmount(() => clearInterval(timer))
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="status" class="h-full flex flex-col gap-3">
|
<div v-if="status" class="h-full flex flex-col gap-2">
|
||||||
<!-- 顶部:引擎状态、启停、账户 -->
|
<!-- 状态栏:引擎、启停、账户与今日统计、模型 -->
|
||||||
<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">
|
<div class="flex flex-col gap-2 border border-ap-line border-solid bg-ap-panel px-3 py-2.5">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<el-tag v-if="snapshot" type="success" effect="dark">
|
<el-tag :type="snapshot ? 'success' : 'info'">
|
||||||
引擎在线
|
<i class="mr-1.5 inline-block h-1.5 w-1.5 bg-current" :class="{ 'animate-pulse': snapshot }" />{{ snapshot ? "引擎在线" : "引擎离线" }}
|
||||||
</el-tag>
|
|
||||||
<el-tag v-else type="info" effect="dark">
|
|
||||||
引擎离线
|
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-tag :type="status.running ? 'success' : 'info'">
|
<el-tag :type="status.running ? 'success' : 'info'">
|
||||||
{{ status.running ? "交易中" : "已停止" }}
|
<i class="mr-1.5 inline-block h-1.5 w-1.5 bg-current" />{{ status.running ? "交易中" : "已停止" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-button
|
<el-button
|
||||||
:type="status.running ? 'danger' : 'primary'" :loading="toggling"
|
size="small" :type="status.running ? 'danger' : 'primary'" :loading="toggling"
|
||||||
: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 class="stat">
|
|
||||||
<span>交易资金</span>
|
|
||||||
<template v-if="status.capital_usdt">
|
|
||||||
{{ money(status.capital_usdt) }} USDT
|
|
||||||
<span v-if="status.effective_capital && Number(status.effective_capital) < Number(status.capital_usdt)" class="text-[var(--el-color-warning)]!">
|
|
||||||
(权益不足,按 {{ money(status.effective_capital) }})
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<b v-else class="text-[var(--el-color-danger)]">未设置</b>
|
|
||||||
<el-link type="primary" underline="never" class="ml-1" @click="editCapital">
|
|
||||||
设置
|
|
||||||
</el-link>
|
|
||||||
</div>
|
|
||||||
<div class="stat">
|
|
||||||
<span>账户权益</span>{{ money(account?.margin_balance) }} USDT
|
|
||||||
</div>
|
|
||||||
<div class="stat">
|
|
||||||
<span>可用</span>{{ money(account?.available_balance) }} USDT
|
|
||||||
</div>
|
|
||||||
<div class="stat">
|
|
||||||
<span>未实现盈亏</span>
|
|
||||||
<b :class="pnlClass(account?.unrealized_pnl ?? '0')">{{ money(account?.unrealized_pnl) }}</b>
|
|
||||||
</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 class="flex-1" />
|
<div class="flex-1" />
|
||||||
|
<span class="metric mr-2">
|
||||||
|
<span class="key">今日分析</span>{{ status.today.analyses }} / {{ status.today.limit }}
|
||||||
|
<span class="key ml-1.5">token</span>{{ status.today.input_tokens.toLocaleString() }} / {{ status.today.output_tokens.toLocaleString() }}
|
||||||
|
</span>
|
||||||
<el-select
|
<el-select
|
||||||
:model-value="status.provider_id" class="w-56" clearable :value-on-clear="null" placeholder="模型:跟随默认配置"
|
:model-value="status.provider_id" class="w-64" clearable :value-on-clear="null" placeholder="跟随默认配置"
|
||||||
@update:model-value="changeProvider"
|
@update:model-value="changeProvider"
|
||||||
>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<span class="text-[11px] text-ap-muted">模型:</span>
|
||||||
|
</template>
|
||||||
<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>
|
||||||
<el-select
|
<el-select
|
||||||
:model-value="status.reasoning_effort" class="w-40" clearable :value-on-clear="null" placeholder="思考强度:跟随配置"
|
:model-value="status.reasoning_effort" class="w-36" clearable :value-on-clear="null" placeholder="跟随配置"
|
||||||
@update:model-value="changeEffort"
|
@update:model-value="changeEffort"
|
||||||
>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<span class="text-[11px] text-ap-muted">思考:</span>
|
||||||
|
</template>
|
||||||
<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 class="flex flex-wrap items-center gap-y-1">
|
||||||
|
<span class="metric">
|
||||||
|
<span class="key">交易资金</span>
|
||||||
|
<template v-if="status.capital_usdt">
|
||||||
|
<b>{{ money(status.capital_usdt) }}</b><span class="unit">USDT</span>
|
||||||
|
<span v-if="status.effective_capital && Number(status.effective_capital) < Number(status.capital_usdt)" class="ml-1 text-ap-warn">
|
||||||
|
(权益不足,按 {{ money(status.effective_capital) }})
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<b v-else class="text-ap-down">未设置</b>
|
||||||
|
<el-link type="primary" class="ml-1.5 text-[11px]!" @click="editCapital">设置</el-link>
|
||||||
|
</span>
|
||||||
|
<span class="metric"><span class="key">账户权益</span><b>{{ money(account?.margin_balance) }}</b><span class="unit">USDT</span></span>
|
||||||
|
<span class="metric"><span class="key">可用</span><b>{{ money(account?.available_balance) }}</b><span class="unit">USDT</span></span>
|
||||||
|
<span class="metric">
|
||||||
|
<span class="key">未实现盈亏</span><b :class="pnlClass(account?.unrealized_pnl ?? '0')">{{ signedMoney(account?.unrealized_pnl) }}</b>
|
||||||
|
</span>
|
||||||
|
<span class="metric">
|
||||||
|
<span class="key">今日收益</span><b :class="pnlClass(status.today.net_pnl)">{{ signedMoney(status.today.net_pnl) }}</b><span class="unit">USDT</span>
|
||||||
|
</span>
|
||||||
|
<span class="metric"><span class="key">今日交易</span><b>{{ status.today.trades }}</b><span class="unit">笔</span></span>
|
||||||
|
<span class="metric"><span class="key">胜率</span><b class="text-ap-brand">{{ winRate }}</b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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,8 +331,20 @@ 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>
|
||||||
|
<i class="inline-block h-1.5 w-1.5" :class="positions.length ? 'bg-ap-up' : 'bg-ap-faint'" />
|
||||||
|
<span>持仓 ({{ positions.length }})</span>
|
||||||
|
</template>
|
||||||
|
<el-table :data="positions" size="small" empty-text="没有持仓">
|
||||||
|
<el-table-column label="币种" min-width="130">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="text-ap-brand font-bold">{{ row.symbol }}</span>
|
||||||
|
<el-tag size="small" type="info" class="ml-1.5">
|
||||||
|
永续
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="方向" width="70">
|
<el-table-column label="方向" width="70">
|
||||||
<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">
|
||||||
@@ -320,156 +352,174 @@ onBeforeUnmount(() => clearInterval(timer))
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="数量" min-width="90">
|
<el-table-column label="数量" min-width="90" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ num(row.amount) }}
|
{{ num(row.amount) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="开仓价" min-width="100">
|
<el-table-column label="开仓价" min-width="100" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ num(row.entry_price) }}
|
{{ num(row.entry_price) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="保本价" min-width="100">
|
<el-table-column label="保本价" min-width="100" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ Number(row.break_even_price).toFixed(2) }}
|
<span class="text-ap-muted">{{ money(row.break_even_price) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="标记价" min-width="100">
|
<el-table-column label="标记价" min-width="100" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ Number(row.mark_price).toFixed(2) }}
|
<b>{{ money(row.mark_price) }}</b>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="未实现盈亏" min-width="100">
|
<el-table-column label="未实现盈亏" min-width="100" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span :class="pnlClass(row.unrealized_pnl)">{{ money(row.unrealized_pnl) }}</span>
|
<b :class="pnlClass(row.unrealized_pnl)">{{ signedMoney(row.unrealized_pnl) }}</b>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="强平价" min-width="100">
|
<el-table-column label="强平价" min-width="100" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ Number(row.liquidation_price) ? Number(row.liquidation_price).toFixed(2) : "-" }}
|
{{ Number(row.liquidation_price) ? money(row.liquidation_price) : "-" }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="杠杆" width="110">
|
<el-table-column label="杠杆" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small" type="info">
|
||||||
{{ 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="160" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ exitPrices(row as SnapshotPosition).stop }} / {{ exitPrices(row as SnapshotPosition).target }}
|
<span class="text-ap-down">{{ exitPrices(row as SnapshotPosition).stop }}</span>
|
||||||
|
/
|
||||||
|
<span class="text-ap-up">{{ exitPrices(row as SnapshotPosition).target }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="平仓倒计时" width="100">
|
<el-table-column label="平仓倒计时" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ closeCountdown(row.symbol) }}
|
<b class="text-ap-brand">{{ closeCountdown(row.symbol) }}</b>
|
||||||
</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="min-h-0 flex flex-1 gap-2">
|
||||||
<!-- 侧边栏:交易币种 -->
|
<!-- 交易币种 -->
|
||||||
<div class="w-60 flex flex-col border border-[var(--el-border-color)] rounded border-solid">
|
<el-card shadow="never" class="w-60 flex shrink-0 flex-col" body-class="p-0! min-h-0 flex-1">
|
||||||
<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="w-full flex items-center justify-between">
|
||||||
|
<span>交易币种 ({{ symbols.length }})</span>
|
||||||
<el-button size="small" type="primary" @click="addVisible = true">
|
<el-button size="small" type="primary" @click="addVisible = true">
|
||||||
新增
|
新增
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-scrollbar class="flex-1">
|
</template>
|
||||||
|
<el-scrollbar>
|
||||||
<div
|
<div
|
||||||
v-for="s in symbols" :key="s.symbol"
|
v-for="s in symbols" :key="s.symbol" class="symbol-item" :class="{ active: s.symbol === selected }"
|
||||||
class="cursor-pointer border-b border-b-[var(--el-border-color-lighter)] border-b-solid px-3 py-2"
|
|
||||||
:class="s.symbol === selected ? 'bg-[var(--el-color-primary-light-9)]' : ''"
|
|
||||||
@click="selected = s.symbol"
|
@click="selected = s.symbol"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-bold">{{ s.symbol }}</span>
|
<span class="text-sm font-bold">{{ 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-[11px]">
|
||||||
<el-link type="danger" underline="never" class="text-xs!" @click.stop="remove(s)">
|
<span v-if="positionLine(s.symbol)" class="font-semibold" :class="positionLine(s.symbol)!.long ? 'text-ap-up' : 'text-ap-down'">
|
||||||
|
{{ positionLine(s.symbol)!.text }}
|
||||||
|
</span>
|
||||||
|
<span v-else />
|
||||||
|
<el-link type="danger" class="text-[11px]!" @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-scrollbar>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
<!-- 主区域:当前计划、挂单、时间线 -->
|
<!-- 当前币种:计划、挂单、时间线 -->
|
||||||
<el-scrollbar class="flex-1">
|
<el-card shadow="never" class="min-w-0 flex flex-1 flex-col" body-class="p-0! min-h-0 flex-1">
|
||||||
<div class="flex flex-col gap-3 pr-2">
|
<template #header>
|
||||||
|
<el-icon class="text-ap-brand">
|
||||||
|
<DataAnalysis />
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ current ? `${current.symbol} · 交易计划` : "交易计划" }}</span>
|
||||||
|
</template>
|
||||||
|
<el-scrollbar>
|
||||||
|
<div class="flex flex-col gap-3 p-3">
|
||||||
<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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="current">
|
<el-card v-if="current" shadow="never" class="sub-card" body-class="p-0!">
|
||||||
<div class="mb-2 font-bold">
|
<template #header>
|
||||||
当前挂单
|
当前挂单 ({{ symbolOrders.length }})
|
||||||
</div>
|
</template>
|
||||||
<el-table :data="symbolOrders" border size="small" empty-text="没有挂单">
|
<el-table :data="symbolOrders" size="small" empty-text="没有挂单">
|
||||||
<el-table-column label="类型" min-width="150">
|
<el-table-column label="类型" min-width="150">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.type }}
|
{{ row.type }}
|
||||||
<el-tag v-if="row.conditional" size="small" class="ml-1">
|
<el-tag v-if="row.conditional" size="small" type="info" class="ml-1">
|
||||||
条件单
|
条件单
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="方向" width="110">
|
<el-table-column label="方向" width="110">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.side === "BUY" ? "买" : "卖" }}{{ row.reduce_only ? "(只减仓)" : "" }}
|
<b :class="row.side === 'BUY' ? 'text-ap-up' : 'text-ap-down'">{{ row.side === "BUY" ? "买" : "卖" }}</b>{{ row.reduce_only ? "(只减仓)" : "" }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="priceText" label="价格" min-width="130" />
|
<el-table-column prop="priceText" label="价格 / 触发" min-width="130" align="right" />
|
||||||
<el-table-column label="数量" min-width="100">
|
<el-table-column label="数量" min-width="100" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.quantity === "全部持仓" ? row.quantity : num(row.quantity) }}
|
{{ row.quantity === "全部持仓" ? row.quantity : num(row.quantity) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="来源" width="80">
|
<el-table-column label="来源" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ isOwnOrder(row.client_id) ? "本程序" : "手动" }}
|
<span class="text-ap-muted">{{ isOwnOrder(row.client_id) ? "本程序" : "手动" }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="时间" width="130">
|
<el-table-column label="时间" width="130" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatTime(new Date(row.time).toISOString(), "MM-DD HH:mm:ss") }}
|
<span class="text-ap-muted">{{ formatTime(new Date(row.time).toISOString()) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
<div>
|
<el-card shadow="never" class="sub-card">
|
||||||
<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="ml-1.5 border-l border-l-ap-line border-l-solid">
|
||||||
<el-timeline-item
|
<div v-for="e in events" :key="e.id" class="relative pb-3 pl-5 last:pb-1">
|
||||||
v-for="e in events" :key="e.id" :timestamp="formatTime(e.created_at)" :type="LEVEL_TYPES[e.level]"
|
<i class="absolute left-[-6px] top-1 h-2.5 w-2.5 border-2 border-ap-panel-low border-solid" :class="LEVEL_DOTS[e.level]" />
|
||||||
placement="top"
|
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||||
>
|
<span class="text-ap-brand font-bold">{{ formatTime(e.created_at) }}</span>
|
||||||
<el-tag size="small" type="info" class="mr-2">
|
<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>
|
<span :class="e.level === 'error' ? 'text-ap-down' : ''">{{ e.message }}</span>
|
||||||
<el-link v-if="e.analysis_id && e.kind !== 'analysis_started'" type="primary" underline="never" class="ml-2" @click="openAnalysis(e.analysis_id)">
|
<el-link v-if="e.analysis_id && e.kind !== 'analysis_started'" type="primary" @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" class="mt-1" @click="loadMoreEvents">
|
||||||
加载更早的事件
|
加载更早的事件
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddSymbolDialog v-model="addVisible" @added="onAdded" />
|
<AddSymbolDialog v-model="addVisible" @added="onAdded" />
|
||||||
@@ -478,12 +528,43 @@ onBeforeUnmount(() => clearInterval(timer))
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.stat {
|
.metric {
|
||||||
font-size: 14px;
|
display: inline-flex;
|
||||||
}
|
align-items: baseline;
|
||||||
.stat span {
|
gap: 5px;
|
||||||
margin-right: 6px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.metric + .metric::before {
|
||||||
|
content: "|";
|
||||||
|
margin: 0 10px 0 5px;
|
||||||
|
color: var(--ap-line);
|
||||||
|
}
|
||||||
|
.metric .key {
|
||||||
|
color: var(--ap-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.metric .unit {
|
||||||
|
color: var(--ap-muted);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.symbol-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, var(--ap-line) 50%, transparent);
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.symbol-item:hover {
|
||||||
|
background: var(--ap-panel-highest);
|
||||||
|
}
|
||||||
|
.symbol-item.active {
|
||||||
|
border-left-color: var(--ap-brand);
|
||||||
|
background: var(--ap-panel-high);
|
||||||
|
}
|
||||||
|
.sub-card {
|
||||||
|
--el-card-bg-color: var(--ap-panel-low);
|
||||||
|
}
|
||||||
|
.sub-card :deep(.el-table) {
|
||||||
|
--el-table-bg-color: var(--ap-panel-low);
|
||||||
|
--el-table-tr-bg-color: var(--ap-panel-low);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
|
import { ChatDotRound, Collection, DataAnalysis, Document, Monitor, Setting, TrendCharts } from "@element-plus/icons-vue"
|
||||||
import { createRouter, createWebHistory } from "vue-router"
|
import { createRouter, createWebHistory } from "vue-router"
|
||||||
import Layout from "@/layouts/index.vue"
|
import Layout from "@/layouts/index.vue"
|
||||||
import { useUserStore } from "@/pinia/user"
|
import { useUserStore } from "@/pinia/user"
|
||||||
|
|
||||||
export const menuRoutes = [
|
export const menuRoutes = [
|
||||||
{ path: "/news", name: "news", component: () => import("@/pages/news/index.vue"), meta: { title: "新闻流" } },
|
{ path: "/news", name: "news", component: () => import("@/pages/news/index.vue"), meta: { title: "新闻流", icon: Document } },
|
||||||
{ path: "/crawler", name: "crawler", component: () => import("@/pages/crawler/index.vue"), meta: { title: "采集控制台" } },
|
{ path: "/crawler", name: "crawler", component: () => import("@/pages/crawler/index.vue"), meta: { title: "采集控制台", icon: Monitor } },
|
||||||
{ path: "/market", name: "market", component: () => import("@/pages/market/index.vue"), meta: { title: "行情" } },
|
{ path: "/market", name: "market", component: () => import("@/pages/market/index.vue"), meta: { title: "行情", icon: TrendCharts } },
|
||||||
{ path: "/trading", name: "trading", component: () => import("@/pages/trading/index.vue"), meta: { title: "模拟盘" } },
|
{ path: "/trading", name: "trading", component: () => import("@/pages/trading/index.vue"), meta: { title: "模拟盘", icon: DataAnalysis } },
|
||||||
{ path: "/chat", name: "chat", component: () => import("@/pages/chat/index.vue"), meta: { title: "Chat" } },
|
{ path: "/chat", name: "chat", component: () => import("@/pages/chat/index.vue"), meta: { title: "Chat", icon: ChatDotRound } },
|
||||||
{ path: "/skills", name: "skills", component: () => import("@/pages/skills/index.vue"), meta: { title: "Skills" } },
|
{ path: "/skills", name: "skills", component: () => import("@/pages/skills/index.vue"), meta: { title: "Skills", icon: Collection } },
|
||||||
{ path: "/settings", name: "settings", component: () => import("@/pages/settings/index.vue"), meta: { title: "环境变量" } }
|
{ path: "/settings", name: "settings", component: () => import("@/pages/settings/index.vue"), meta: { title: "环境变量", icon: Setting } }
|
||||||
]
|
]
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
|
|||||||
+311
-16
@@ -1,3 +1,5 @@
|
|||||||
|
/* 全站深色交易终端主题。颜色来自 palette.ts(由 UnoCSS preflight 输出为 --ap-* 变量) */
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
#app {
|
#app {
|
||||||
@@ -6,14 +8,260 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
background: var(--ap-bg);
|
||||||
|
color: var(--ap-text);
|
||||||
|
font-family: var(--ap-font-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Markdown 渲染(Chat 回复、skill 预览、CONTEXT.md 预览) */
|
/* 浏览器默认不让表单控件继承字体 */
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--ap-panel-highest) transparent;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--ap-panel-highest);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Element Plus 变量 ---------- */
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--el-bg-color: var(--ap-panel);
|
||||||
|
--el-bg-color-page: var(--ap-bg);
|
||||||
|
--el-bg-color-overlay: var(--ap-panel);
|
||||||
|
--el-text-color-primary: var(--ap-text);
|
||||||
|
--el-text-color-regular: var(--ap-text);
|
||||||
|
--el-text-color-secondary: var(--ap-muted);
|
||||||
|
--el-text-color-placeholder: var(--ap-faint);
|
||||||
|
--el-text-color-disabled: color-mix(in srgb, var(--ap-faint) 60%, var(--ap-bg));
|
||||||
|
--el-border-color: var(--ap-line);
|
||||||
|
--el-border-color-dark: var(--ap-faint);
|
||||||
|
--el-border-color-darker: var(--ap-faint);
|
||||||
|
--el-border-color-light: color-mix(in srgb, var(--ap-line) 80%, var(--ap-panel));
|
||||||
|
--el-border-color-lighter: color-mix(in srgb, var(--ap-line) 60%, var(--ap-panel));
|
||||||
|
--el-border-color-extra-light: color-mix(in srgb, var(--ap-line) 40%, var(--ap-panel));
|
||||||
|
--el-border-color-hover: var(--ap-faint);
|
||||||
|
--el-fill-color: var(--ap-panel-high);
|
||||||
|
--el-fill-color-light: var(--ap-panel-high);
|
||||||
|
--el-fill-color-lighter: var(--ap-panel-low);
|
||||||
|
--el-fill-color-extra-light: var(--ap-panel-low);
|
||||||
|
--el-fill-color-dark: var(--ap-panel-highest);
|
||||||
|
--el-fill-color-darker: var(--ap-panel-highest);
|
||||||
|
/* 输入框、下拉框、复选按钮等的底色 */
|
||||||
|
--el-fill-color-blank: var(--ap-sunken);
|
||||||
|
--el-mask-color: color-mix(in srgb, var(--ap-bg) 80%, transparent);
|
||||||
|
|
||||||
|
--el-color-primary: var(--ap-brand);
|
||||||
|
--el-color-success: var(--ap-up);
|
||||||
|
--el-color-warning: var(--ap-warn);
|
||||||
|
--el-color-danger: var(--ap-down);
|
||||||
|
--el-color-error: var(--ap-down);
|
||||||
|
--el-color-info: var(--ap-faint);
|
||||||
|
/* Element 在实底语义色上一律用 --el-color-white 作文字色(按钮、分页、复选按钮、实心标签等);
|
||||||
|
我们的黄、浅绿、浅红都需要深色字 */
|
||||||
|
--el-color-white: var(--ap-bg);
|
||||||
|
|
||||||
|
--el-border-radius-base: 0;
|
||||||
|
--el-border-radius-small: 0;
|
||||||
|
--el-border-radius-round: 0;
|
||||||
|
--el-font-family: var(--ap-font-mono);
|
||||||
|
--el-font-size-base: 13px;
|
||||||
|
--el-font-size-small: 12px;
|
||||||
|
--el-font-size-extra-small: 11px;
|
||||||
|
/* 稿中弹层不用柔和阴影,只用硬描边 */
|
||||||
|
--el-box-shadow: 2px 2px 0 #000;
|
||||||
|
--el-box-shadow-light: 2px 2px 0 #000;
|
||||||
|
--el-box-shadow-lighter: none;
|
||||||
|
--el-box-shadow-dark: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 语义色的浅色/深色档:浅色档向面板色混合,标签自然成为「浅底 + 描边」;深色档在暗色主题里是更亮的悬停色 */
|
||||||
|
html.dark {
|
||||||
|
--el-color-primary-light-3: color-mix(in srgb, var(--el-color-primary) 70%, var(--ap-panel));
|
||||||
|
--el-color-primary-light-5: color-mix(in srgb, var(--el-color-primary) 50%, var(--ap-panel));
|
||||||
|
--el-color-primary-light-7: color-mix(in srgb, var(--el-color-primary) 30%, var(--ap-panel));
|
||||||
|
--el-color-primary-light-8: color-mix(in srgb, var(--el-color-primary) 22%, var(--ap-panel));
|
||||||
|
--el-color-primary-light-9: color-mix(in srgb, var(--el-color-primary) 12%, var(--ap-panel));
|
||||||
|
--el-color-primary-dark-2: color-mix(in srgb, var(--el-color-primary) 80%, #fff);
|
||||||
|
--el-color-success-light-3: color-mix(in srgb, var(--el-color-success) 70%, var(--ap-panel));
|
||||||
|
--el-color-success-light-5: color-mix(in srgb, var(--el-color-success) 50%, var(--ap-panel));
|
||||||
|
--el-color-success-light-7: color-mix(in srgb, var(--el-color-success) 30%, var(--ap-panel));
|
||||||
|
--el-color-success-light-8: color-mix(in srgb, var(--el-color-success) 22%, var(--ap-panel));
|
||||||
|
--el-color-success-light-9: color-mix(in srgb, var(--el-color-success) 12%, var(--ap-panel));
|
||||||
|
--el-color-success-dark-2: color-mix(in srgb, var(--el-color-success) 80%, #fff);
|
||||||
|
--el-color-warning-light-3: color-mix(in srgb, var(--el-color-warning) 70%, var(--ap-panel));
|
||||||
|
--el-color-warning-light-5: color-mix(in srgb, var(--el-color-warning) 50%, var(--ap-panel));
|
||||||
|
--el-color-warning-light-7: color-mix(in srgb, var(--el-color-warning) 30%, var(--ap-panel));
|
||||||
|
--el-color-warning-light-8: color-mix(in srgb, var(--el-color-warning) 22%, var(--ap-panel));
|
||||||
|
--el-color-warning-light-9: color-mix(in srgb, var(--el-color-warning) 12%, var(--ap-panel));
|
||||||
|
--el-color-warning-dark-2: color-mix(in srgb, var(--el-color-warning) 80%, #fff);
|
||||||
|
--el-color-danger-light-3: color-mix(in srgb, var(--el-color-danger) 70%, var(--ap-panel));
|
||||||
|
--el-color-danger-light-5: color-mix(in srgb, var(--el-color-danger) 50%, var(--ap-panel));
|
||||||
|
--el-color-danger-light-7: color-mix(in srgb, var(--el-color-danger) 30%, var(--ap-panel));
|
||||||
|
--el-color-danger-light-8: color-mix(in srgb, var(--el-color-danger) 22%, var(--ap-panel));
|
||||||
|
--el-color-danger-light-9: color-mix(in srgb, var(--el-color-danger) 12%, var(--ap-panel));
|
||||||
|
--el-color-danger-dark-2: color-mix(in srgb, var(--el-color-danger) 80%, #fff);
|
||||||
|
--el-color-error-light-3: var(--el-color-danger-light-3);
|
||||||
|
--el-color-error-light-5: var(--el-color-danger-light-5);
|
||||||
|
--el-color-error-light-7: var(--el-color-danger-light-7);
|
||||||
|
--el-color-error-light-8: var(--el-color-danger-light-8);
|
||||||
|
--el-color-error-light-9: var(--el-color-danger-light-9);
|
||||||
|
--el-color-error-dark-2: var(--el-color-danger-dark-2);
|
||||||
|
--el-color-info-light-3: color-mix(in srgb, var(--el-color-info) 70%, var(--ap-panel));
|
||||||
|
--el-color-info-light-5: color-mix(in srgb, var(--el-color-info) 50%, var(--ap-panel));
|
||||||
|
--el-color-info-light-7: color-mix(in srgb, var(--el-color-info) 30%, var(--ap-panel));
|
||||||
|
--el-color-info-light-8: color-mix(in srgb, var(--el-color-info) 22%, var(--ap-panel));
|
||||||
|
--el-color-info-light-9: color-mix(in srgb, var(--el-color-info) 12%, var(--ap-panel));
|
||||||
|
--el-color-info-dark-2: color-mix(in srgb, var(--el-color-info) 80%, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 组件 ---------- */
|
||||||
|
|
||||||
|
/* 面板:1px 边框,32px 标题条 */
|
||||||
|
html.dark .el-card {
|
||||||
|
--el-card-bg-color: var(--ap-panel);
|
||||||
|
--el-card-border-color: var(--ap-line);
|
||||||
|
--el-card-border-radius: 0;
|
||||||
|
--el-card-padding: 12px;
|
||||||
|
}
|
||||||
|
.el-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 4px 12px !important;
|
||||||
|
border-bottom-color: var(--ap-line) !important;
|
||||||
|
background: var(--ap-panel-high);
|
||||||
|
font-family: var(--ap-font-head);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格:紧凑行高,表头下沉色,数字等宽 */
|
||||||
|
html.dark .el-table {
|
||||||
|
--el-table-bg-color: var(--ap-panel);
|
||||||
|
--el-table-tr-bg-color: var(--ap-panel);
|
||||||
|
--el-table-expanded-cell-bg-color: var(--ap-panel);
|
||||||
|
--el-table-header-bg-color: var(--ap-sunken);
|
||||||
|
--el-table-header-text-color: var(--ap-muted);
|
||||||
|
--el-table-row-hover-bg-color: var(--ap-panel-high);
|
||||||
|
--el-table-border-color: color-mix(in srgb, var(--ap-line) 60%, var(--ap-panel));
|
||||||
|
--el-table-text-color: var(--ap-text);
|
||||||
|
}
|
||||||
|
.el-table th.el-table__cell {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主按钮:黄底深色字,悬停变亮 */
|
||||||
|
html.dark .el-button--primary:not(.is-plain, .is-text, .is-link) {
|
||||||
|
--el-button-hover-bg-color: var(--el-color-primary-dark-2);
|
||||||
|
--el-button-hover-border-color: var(--el-color-primary-dark-2);
|
||||||
|
}
|
||||||
|
/* 危险、成功、警告按钮:浅底描边(稿中的「停止」),悬停填实 */
|
||||||
|
html.dark .el-button--danger {
|
||||||
|
--ap-button-color: var(--el-color-danger);
|
||||||
|
}
|
||||||
|
html.dark .el-button--success {
|
||||||
|
--ap-button-color: var(--el-color-success);
|
||||||
|
}
|
||||||
|
html.dark .el-button--warning {
|
||||||
|
--ap-button-color: var(--el-color-warning);
|
||||||
|
}
|
||||||
|
html.dark :is(.el-button--danger, .el-button--success, .el-button--warning):not(.is-text, .is-link) {
|
||||||
|
--el-button-text-color: var(--ap-button-color);
|
||||||
|
--el-button-bg-color: color-mix(in srgb, var(--ap-button-color) 14%, transparent);
|
||||||
|
--el-button-border-color: color-mix(in srgb, var(--ap-button-color) 45%, transparent);
|
||||||
|
--el-button-hover-text-color: var(--ap-bg);
|
||||||
|
--el-button-hover-bg-color: var(--ap-button-color);
|
||||||
|
--el-button-hover-border-color: var(--ap-button-color);
|
||||||
|
--el-button-active-text-color: var(--ap-bg);
|
||||||
|
--el-button-active-bg-color: var(--ap-button-color);
|
||||||
|
--el-button-active-border-color: var(--ap-button-color);
|
||||||
|
}
|
||||||
|
/* 暗色主题里 light-5 是变暗,链接悬停要变亮 */
|
||||||
|
html.dark .el-button.is-link {
|
||||||
|
--el-button-hover-link-text-color: color-mix(in srgb, var(--el-button-text-color) 75%, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-tag {
|
||||||
|
--el-tag-border-radius: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 树放在面板里,不要下沉底色 */
|
||||||
|
html.dark .el-tree {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-descriptions__body {
|
||||||
|
background-color: var(--ap-panel);
|
||||||
|
}
|
||||||
|
html.dark .el-descriptions {
|
||||||
|
--el-descriptions-item-bordered-label-background: var(--ap-panel-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-switch__core,
|
||||||
|
html.dark .el-switch__core .el-switch__action {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
html.dark .el-switch__core .el-switch__action {
|
||||||
|
background-color: var(--ap-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-progress-bar__outer,
|
||||||
|
.el-progress-bar__inner {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-drawer {
|
||||||
|
border-left: 1px solid var(--ap-line);
|
||||||
|
}
|
||||||
|
.el-drawer__header {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
padding: 0 16px !important;
|
||||||
|
height: 48px;
|
||||||
|
border-bottom: 1px solid var(--ap-line);
|
||||||
|
background: var(--ap-panel-high);
|
||||||
|
}
|
||||||
|
.el-drawer__title,
|
||||||
|
.el-dialog__title,
|
||||||
|
.el-message-box__title {
|
||||||
|
font-family: var(--ap-font-head);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
html.dark .el-dialog,
|
||||||
|
html.dark .el-message-box {
|
||||||
|
border: 1px solid var(--ap-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 正文 ---------- */
|
||||||
|
|
||||||
|
/* Markdown 渲染(分析报告、Chat 回复、skill 预览、CONTEXT.md 预览):长段中文用无衬线字体 */
|
||||||
.markdown-body {
|
.markdown-body {
|
||||||
|
font-family: var(--ap-font-sans);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.7;
|
line-height: 1.75;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
color: var(--ap-text);
|
||||||
}
|
}
|
||||||
.markdown-body > :first-child {
|
.markdown-body > :first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
@@ -22,17 +270,21 @@ body {
|
|||||||
.markdown-body h2,
|
.markdown-body h2,
|
||||||
.markdown-body h3,
|
.markdown-body h3,
|
||||||
.markdown-body h4 {
|
.markdown-body h4 {
|
||||||
margin: 1em 0 0.5em;
|
margin: 1.1em 0 0.5em;
|
||||||
font-weight: 600;
|
font-family: var(--ap-font-head);
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.markdown-body h1 {
|
.markdown-body h1 {
|
||||||
font-size: 1.5em;
|
font-size: 1.45em;
|
||||||
}
|
}
|
||||||
.markdown-body h2 {
|
.markdown-body h2 {
|
||||||
font-size: 1.3em;
|
padding-bottom: 0.25em;
|
||||||
|
border-bottom: 1px solid var(--ap-line);
|
||||||
|
font-size: 1.25em;
|
||||||
}
|
}
|
||||||
.markdown-body h3 {
|
.markdown-body h3 {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
|
color: var(--ap-brand);
|
||||||
}
|
}
|
||||||
.markdown-body p,
|
.markdown-body p,
|
||||||
.markdown-body ul,
|
.markdown-body ul,
|
||||||
@@ -46,17 +298,23 @@ body {
|
|||||||
.markdown-body ol {
|
.markdown-body ol {
|
||||||
padding-left: 1.5em;
|
padding-left: 1.5em;
|
||||||
}
|
}
|
||||||
|
.markdown-body strong {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.markdown-body a {
|
||||||
|
color: var(--ap-brand);
|
||||||
|
}
|
||||||
.markdown-body code {
|
.markdown-body code {
|
||||||
padding: 0.1em 0.3em;
|
padding: 0.1em 0.3em;
|
||||||
border-radius: 3px;
|
background: var(--ap-panel-high);
|
||||||
background: var(--el-fill-color);
|
font-family: var(--ap-font-mono);
|
||||||
font-size: 0.9em;
|
font-size: 0.88em;
|
||||||
}
|
}
|
||||||
.markdown-body pre {
|
.markdown-body pre {
|
||||||
padding: 0.6em 0.8em;
|
padding: 0.6em 0.8em;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border-radius: 4px;
|
border: 1px solid var(--ap-line);
|
||||||
background: var(--el-fill-color-light);
|
background: var(--ap-sunken);
|
||||||
}
|
}
|
||||||
.markdown-body pre code {
|
.markdown-body pre code {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -64,14 +322,51 @@ body {
|
|||||||
}
|
}
|
||||||
.markdown-body blockquote {
|
.markdown-body blockquote {
|
||||||
padding-left: 0.8em;
|
padding-left: 0.8em;
|
||||||
border-left: 3px solid var(--el-border-color);
|
border-left: 3px solid var(--ap-brand);
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--ap-muted);
|
||||||
}
|
}
|
||||||
.markdown-body table {
|
.markdown-body table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
font-family: var(--ap-font-mono);
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.markdown-body th,
|
.markdown-body th,
|
||||||
.markdown-body td {
|
.markdown-body td {
|
||||||
padding: 0.3em 0.6em;
|
padding: 0.35em 0.7em;
|
||||||
border: 1px solid var(--el-border-color);
|
border: 1px solid var(--ap-line);
|
||||||
|
}
|
||||||
|
.markdown-body th {
|
||||||
|
background: var(--ap-panel-high);
|
||||||
|
color: var(--ap-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.markdown-body hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--ap-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 逐行差异(新闻修订、skill 版本) */
|
||||||
|
.diff-add {
|
||||||
|
background: color-mix(in srgb, var(--ap-up) 14%, transparent);
|
||||||
|
}
|
||||||
|
.diff-del {
|
||||||
|
background: color-mix(in srgb, var(--ap-down) 14%, transparent);
|
||||||
|
color: var(--ap-muted);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 折叠面板:标题和内容留出内边距,标题过长时截断 */
|
||||||
|
.el-collapse-item__header {
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 10px 0 12px;
|
||||||
|
}
|
||||||
|
.el-collapse-item__title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.el-collapse-item__content {
|
||||||
|
padding: 0 12px 12px;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 全站配色,取自 Stitch 稿 code.html 的 Tailwind token(screen.png 取样核实)。
|
||||||
|
* uno.config.ts 用它生成 ap-* 类名和 --ap-* CSS 变量;KLineChart 和新闻 iframe 读不到 CSS 变量,直接引用这里的值。
|
||||||
|
*/
|
||||||
|
export const PALETTE = {
|
||||||
|
bg: "#0a141f",
|
||||||
|
sunken: "#050f19",
|
||||||
|
panelLow: "#131c27",
|
||||||
|
panel: "#17202b",
|
||||||
|
panelHigh: "#212b36",
|
||||||
|
panelHighest: "#2c3641",
|
||||||
|
line: "#3c4a40",
|
||||||
|
faint: "#859489",
|
||||||
|
text: "#d9e3f3",
|
||||||
|
muted: "#bbcabd",
|
||||||
|
brand: "#ffd162",
|
||||||
|
up: "#45e89b",
|
||||||
|
down: "#ffb4ab",
|
||||||
|
warn: "#ff8f1f",
|
||||||
|
// K 线柱体需要更饱和的涨跌色
|
||||||
|
candleUp: "#0ecb81",
|
||||||
|
candleDown: "#f6465d"
|
||||||
|
} as const
|
||||||
@@ -4,6 +4,12 @@ export function formatTime(value: string | null | undefined, pattern = "MM-DD HH
|
|||||||
return value ? dayjs(value).format(pattern) : "-"
|
return value ? dayjs(value).format(pattern) : "-"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 两个时间点之间的耗时,如「1m 52s」 */
|
||||||
|
export function formatDuration(from: string, to: string) {
|
||||||
|
const seconds = Math.round((new Date(to).getTime() - new Date(from).getTime()) / 1000)
|
||||||
|
return seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`
|
||||||
|
}
|
||||||
|
|
||||||
/** HTML 转纯文本行;DOMParser 解析的文档不会执行脚本 */
|
/** HTML 转纯文本行;DOMParser 解析的文档不会执行脚本 */
|
||||||
export function htmlToLines(html: string): string[] {
|
export function htmlToLines(html: string): string[] {
|
||||||
const marked = html.replace(/<br\s*\/?>|<\/(?:p|div|li|h\d)>/gi, "\n")
|
const marked = html.replace(/<br\s*\/?>|<\/(?:p|div|li|h\d)>/gi, "\n")
|
||||||
|
|||||||
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']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vendored
+2
@@ -1,7 +1,9 @@
|
|||||||
|
import type { Component } from "vue"
|
||||||
import "vue-router"
|
import "vue-router"
|
||||||
|
|
||||||
declare module "vue-router" {
|
declare module "vue-router" {
|
||||||
interface RouteMeta {
|
interface RouteMeta {
|
||||||
title?: string
|
title?: string
|
||||||
|
icon?: Component
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-1
@@ -1,4 +1,10 @@
|
|||||||
import { defineConfig, presetAttributify, presetWind3 } from "unocss"
|
import { defineConfig, presetAttributify, presetWind3 } from "unocss"
|
||||||
|
import { PALETTE } from "./src/styles/palette"
|
||||||
|
|
||||||
|
// camelCase → kebab-case:类名写 bg-ap-panel-high,CSS 变量为 --ap-panel-high
|
||||||
|
const kebab = (name: string) => name.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)
|
||||||
|
const apColors = Object.fromEntries(Object.entries(PALETTE).map(([k, v]) => [kebab(k), v]))
|
||||||
|
const CJK = `"PingFang SC", "Microsoft YaHei", "Noto Sans SC"`
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
// 预设
|
// 预设
|
||||||
@@ -13,6 +19,24 @@ export default defineConfig({
|
|||||||
important: "#app"
|
important: "#app"
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
theme: {
|
||||||
|
colors: { ap: apColors },
|
||||||
|
fontFamily: {
|
||||||
|
// 界面、表格、数字
|
||||||
|
mono: `"JetBrains Mono Variable", ui-monospace, ${CJK}, monospace`,
|
||||||
|
// 标题
|
||||||
|
head: `"Space Grotesk Variable", ${CJK}, sans-serif`,
|
||||||
|
// 正文类:新闻、Markdown、Chat 回复
|
||||||
|
sans: `-apple-system, BlinkMacSystemFont, "Segoe UI", ${CJK}, sans-serif`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
preflights: [{
|
||||||
|
getCSS: ({ theme }) => {
|
||||||
|
const fonts = (theme as { fontFamily: Record<string, string> }).fontFamily
|
||||||
|
return `:root{${Object.entries(apColors).map(([k, v]) => `--ap-${k}:${v};`).join("")}`
|
||||||
|
+ `--ap-font-mono:${fonts.mono};--ap-font-head:${fonts.head};--ap-font-sans:${fonts.sans};}`
|
||||||
|
}
|
||||||
|
}],
|
||||||
// 自定义规则
|
// 自定义规则
|
||||||
rules: [],
|
rules: [],
|
||||||
// 自定义快捷方式
|
// 自定义快捷方式
|
||||||
@@ -20,6 +44,8 @@ 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",
|
||||||
|
// 面板标题条里的标题
|
||||||
|
"panel-title": "font-head text-[12px] font-bold tracking-wider text-ap-text"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Reference in new issue
Block a user