first commit

This commit is contained in:
dekun
2026-05-28 21:43:23 +08:00
commit 1d5c97904f
33 changed files with 5250 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>加密货币前置匹配系统</title>
</head>
<body class="bg-dark-bg text-dark-text">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+2773
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
{
"name": "crypto-pre-trade-frontend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"axios": "^1.7.9"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"vite": "^6.0.5"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+37
View File
@@ -0,0 +1,37 @@
<template>
<div class="min-h-screen flex flex-col">
<!-- 顶部导航 -->
<header class="border-b border-dark-border bg-dark-card px-6 py-3 flex items-center justify-between">
<h1 class="text-lg font-semibold tracking-wide">加密货币前置匹配系统</h1>
<nav class="flex gap-1">
<router-link
to="/"
class="px-4 py-2 text-sm rounded-md transition-colors"
:class="$route.path === '/' ? 'bg-dark-accent text-white' : 'text-dark-muted hover:text-dark-text'"
>
日常使用
</router-link>
<router-link
to="/config"
class="px-4 py-2 text-sm rounded-md transition-colors"
:class="$route.path === '/config' ? 'bg-dark-accent text-white' : 'text-dark-muted hover:text-dark-text'"
>
系统配置
</router-link>
</nav>
</header>
<!-- 主内容 -->
<main class="flex-1 p-6">
<router-view />
</main>
<!-- 底部 -->
<footer class="border-t border-dark-border px-6 py-2 text-xs text-dark-muted text-center">
纯本地部署 · 无登录 · 仅做前置策略匹配 · 箱体/点位/突破条件需人工确认
</footer>
</div>
</template>
<script setup>
</script>
+35
View File
@@ -0,0 +1,35 @@
import axios from 'axios'
const api = axios.create({
baseURL: '/api',
timeout: 10000,
})
// ── 大盘阶段 ──
export const getRegimes = () => api.get('/regimes')
export const createRegime = (data) => api.post('/regimes', data)
export const updateRegime = (id, data) => api.put(`/regimes/${id}`, data)
export const deleteRegime = (id) => api.delete(`/regimes/${id}`)
// ── 账户 ──
export const getAccounts = () => api.get('/accounts')
export const createAccount = (data) => api.post('/accounts', data)
export const updateAccount = (id, data) => api.put(`/accounts/${id}`, data)
export const deleteAccount = (id) => api.delete(`/accounts/${id}`)
// ── 策略 ──
export const getStrategies = () => api.get('/strategies')
export const createStrategy = (data) => api.post('/strategies', data)
export const updateStrategy = (id, data) => api.put(`/strategies/${id}`, data)
export const deleteStrategy = (id) => api.delete(`/strategies/${id}`)
// ── 匹配配置 ──
export const getMatches = () => api.get('/matches')
export const createMatch = (data) => api.post('/matches', data)
export const updateMatch = (id, data) => api.put(`/matches/${id}`, data)
export const deleteMatch = (id) => api.delete(`/matches/${id}`)
// ── 前置匹配 ──
export const doMatch = (params) => api.get('/match', { params })
export default api
+6
View File
@@ -0,0 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './style.css'
createApp(App).use(router).mount('#app')
+13
View File
@@ -0,0 +1,13 @@
import { createRouter, createWebHistory } from 'vue-router'
import DailyMatch from '../views/DailyMatch.vue'
import ConfigCenter from '../views/ConfigCenter.vue'
const routes = [
{ path: '/', name: 'DailyMatch', component: DailyMatch },
{ path: '/config', name: 'ConfigCenter', component: ConfigCenter },
]
export default createRouter({
history: createWebHistory(),
routes,
})
+51
View File
@@ -0,0 +1,51 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
}
/* 滚动条样式 */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: #141414; }
::-webkit-scrollbar-thumb { background: #404040; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #525252; }
/* 通用卡片 */
.card {
@apply bg-dark-card border border-dark-border rounded-lg p-4;
}
/* 通用按钮 */
.btn {
@apply px-4 py-2 rounded-md text-sm font-medium transition-colors;
}
.btn-primary {
@apply btn bg-dark-accent text-white hover:bg-blue-600;
}
.btn-danger {
@apply btn bg-dark-danger text-white hover:bg-red-600;
}
.btn-ghost {
@apply btn bg-dark-hover text-dark-text hover:bg-dark-border;
}
/* 表单元素 */
.input {
@apply w-full bg-dark-bg border border-dark-border rounded-md px-3 py-2 text-sm
text-dark-text focus:outline-none focus:border-dark-accent;
}
.select {
@apply input appearance-none cursor-pointer;
}
.textarea {
@apply input resize-y min-h-[80px];
}
/* 标签页 */
.tab-active {
@apply border-b-2 border-dark-accent text-dark-accent;
}
+367
View File
@@ -0,0 +1,367 @@
<template>
<div>
<!-- 标签页切换 -->
<div class="flex gap-0 border-b border-dark-border mb-6">
<button
v-for="tab in tabs"
:key="tab.key"
@click="activeTab = tab.key"
class="px-5 py-3 text-sm font-medium transition-colors"
:class="activeTab === tab.key ? 'tab-active' : 'text-dark-muted hover:text-dark-text'"
>
{{ tab.label }}
</button>
</div>
<!-- 大盘管理 -->
<section v-if="activeTab === 'regimes'">
<div class="flex justify-between items-center mb-4">
<h2 class="text-base font-semibold">大盘阶段管理</h2>
<button @click="openRegimeForm()" class="btn-primary">新增阶段</button>
</div>
<table class="w-full text-sm">
<thead>
<tr class="border-b border-dark-border text-dark-muted text-left">
<th class="py-2 px-3">ID</th>
<th class="py-2 px-3">名称</th>
<th class="py-2 px-3">交易类型</th>
<th class="py-2 px-3">允许方向</th>
<th class="py-2 px-3">备注</th>
<th class="py-2 px-3 w-32">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="r in regimes" :key="r.id" class="border-b border-dark-border/50 hover:bg-dark-hover">
<td class="py-2 px-3">{{ r.id }}</td>
<td class="py-2 px-3 font-medium">{{ r.name }}</td>
<td class="py-2 px-3">{{ r.trade_type }}</td>
<td class="py-2 px-3">{{ r.allow_direction }}</td>
<td class="py-2 px-3 text-dark-muted">{{ r.remark }}</td>
<td class="py-2 px-3">
<button @click="openRegimeForm(r)" class="text-dark-accent text-xs mr-2 hover:underline">编辑</button>
<button @click="handleDeleteRegime(r.id)" class="text-dark-danger text-xs hover:underline">删除</button>
</td>
</tr>
</tbody>
</table>
</section>
<!-- 账户管理 -->
<section v-if="activeTab === 'accounts'">
<div class="flex justify-between items-center mb-4">
<h2 class="text-base font-semibold">账户管理</h2>
<button @click="openAccountForm()" class="btn-primary">新增账户</button>
</div>
<table class="w-full text-sm">
<thead>
<tr class="border-b border-dark-border text-dark-muted text-left">
<th class="py-2 px-3">ID</th>
<th class="py-2 px-3">账户名</th>
<th class="py-2 px-3">本金(U)</th>
<th class="py-2 px-3">交易周期</th>
<th class="py-2 px-3">风险比</th>
<th class="py-2 px-3">状态</th>
<th class="py-2 px-3 w-32">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="a in accounts" :key="a.id" class="border-b border-dark-border/50 hover:bg-dark-hover">
<td class="py-2 px-3">{{ a.id }}</td>
<td class="py-2 px-3 font-medium">{{ a.account_name }}</td>
<td class="py-2 px-3">{{ a.total_capital }}</td>
<td class="py-2 px-3">{{ a.trade_cycle }}</td>
<td class="py-2 px-3">{{ a.risk_ratio }}</td>
<td class="py-2 px-3">
<span :class="a.enable ? 'text-dark-success' : 'text-dark-danger'">
{{ a.enable ? '启用' : '禁用' }}
</span>
</td>
<td class="py-2 px-3">
<button @click="openAccountForm(a)" class="text-dark-accent text-xs mr-2 hover:underline">编辑</button>
<button @click="handleDeleteAccount(a.id)" class="text-dark-danger text-xs hover:underline">删除</button>
</td>
</tr>
</tbody>
</table>
</section>
<!-- 策略管理 -->
<section v-if="activeTab === 'strategies'">
<div class="flex justify-between items-center mb-4">
<h2 class="text-base font-semibold">策略管理</h2>
<button @click="openStrategyForm()" class="btn-primary">新增策略</button>
</div>
<div class="grid gap-4">
<div v-for="s in strategies" :key="s.id" class="card">
<div class="flex justify-between items-start mb-2">
<div>
<h3 class="font-medium">{{ s.strategy_name }}</h3>
<p class="text-xs text-dark-muted mt-1">
周期: {{ s.fit_cycle }} · 趋势: {{ s.fit_trend_strength }} · 类型: {{ s.trade_type }}
</p>
</div>
<div>
<button @click="openStrategyForm(s)" class="text-dark-accent text-xs mr-2 hover:underline">编辑</button>
<button @click="handleDeleteStrategy(s.id)" class="text-dark-danger text-xs hover:underline">删除</button>
</div>
</div>
<pre class="text-xs text-dark-muted whitespace-pre-wrap bg-dark-bg rounded p-3">{{ s.strategy_rule }}</pre>
</div>
</div>
</section>
<!-- 匹配配置 -->
<section v-if="activeTab === 'matches'">
<div class="flex justify-between items-center mb-4">
<h2 class="text-base font-semibold">匹配配置</h2>
<button @click="openMatchForm()" class="btn-primary">新增匹配</button>
</div>
<table class="w-full text-sm">
<thead>
<tr class="border-b border-dark-border text-dark-muted text-left">
<th class="py-2 px-3">ID</th>
<th class="py-2 px-3">大盘阶段</th>
<th class="py-2 px-3">周期</th>
<th class="py-2 px-3">强弱</th>
<th class="py-2 px-3">账户</th>
<th class="py-2 px-3">策略</th>
<th class="py-2 px-3">强制方向</th>
<th class="py-2 px-3 w-32">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="m in matchList" :key="m.id" class="border-b border-dark-border/50 hover:bg-dark-hover">
<td class="py-2 px-3">{{ m.id }}</td>
<td class="py-2 px-3">{{ m.regime_name }}</td>
<td class="py-2 px-3">{{ m.market_cycle }}</td>
<td class="py-2 px-3">{{ m.trend_strength }}</td>
<td class="py-2 px-3">{{ m.account_name }}</td>
<td class="py-2 px-3">{{ m.strategy_name }}</td>
<td class="py-2 px-3 text-dark-muted">{{ m.force_direction || '跟随大盘' }}</td>
<td class="py-2 px-3">
<button @click="openMatchForm(m)" class="text-dark-accent text-xs mr-2 hover:underline">编辑</button>
<button @click="handleDeleteMatch(m.id)" class="text-dark-danger text-xs hover:underline">删除</button>
</td>
</tr>
</tbody>
</table>
</section>
<!-- 通用弹窗 -->
<div v-if="showModal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click.self="showModal = false">
<div class="card w-full max-w-lg max-h-[80vh] overflow-y-auto">
<h3 class="text-base font-semibold mb-4">{{ modalTitle }}</h3>
<form @submit.prevent="handleSubmit" class="flex flex-col gap-3">
<!-- 大盘阶段表单 -->
<template v-if="formType === 'regime'">
<input v-model="formData.name" class="input" placeholder="阶段名称" required />
<select v-model="formData.trade_type" class="select" required>
<option value="顺势">顺势</option>
<option value="反转">反转</option>
<option value="观望">观望</option>
</select>
<select v-model="formData.allow_direction" class="select" required>
<option value="做多">做多</option>
<option value="做空">做空</option>
<option value="禁止">禁止</option>
<option value="多空均可">多空均可</option>
</select>
<input v-model="formData.remark" class="input" placeholder="备注" />
</template>
<!-- 账户表单 -->
<template v-if="formType === 'account'">
<input v-model="formData.account_name" class="input" placeholder="账户名称" required />
<input v-model.number="formData.total_capital" type="number" class="input" placeholder="本金(U)" required />
<input v-model="formData.trade_cycle" class="input" placeholder="交易周期" required />
<input v-model="formData.risk_ratio" class="input" placeholder="风险比" required />
<select v-model.number="formData.enable" class="select">
<option :value="1">启用</option>
<option :value="0">禁用</option>
</select>
<input v-model="formData.remark" class="input" placeholder="备注" />
</template>
<!-- 策略表单 -->
<template v-if="formType === 'strategy'">
<input v-model="formData.strategy_name" class="input" placeholder="策略名称" required />
<input v-model="formData.fit_cycle" class="input" placeholder="适用周期" required />
<select v-model="formData.fit_trend_strength" class="select" required>
<option value="强"></option>
<option value="弱"></option>
<option value="全部">全部</option>
</select>
<select v-model="formData.trade_type" class="select" required>
<option value="顺势">顺势</option>
<option value="反转">反转</option>
<option value="全部">全部</option>
</select>
<textarea v-model="formData.strategy_rule" class="textarea" placeholder="策略规则文本" required></textarea>
<input v-model="formData.remark" class="input" placeholder="备注" />
</template>
<!-- 匹配表单 -->
<template v-if="formType === 'match'">
<select v-model.number="formData.market_regime_id" class="select" required>
<option :value="null" disabled>选择大盘阶段</option>
<option v-for="r in regimes" :key="r.id" :value="r.id">{{ r.name }}</option>
</select>
<select v-model="formData.market_cycle" class="select" required>
<option value="日线">日线</option>
<option value="4H">4H</option>
<option value="1H">1H</option>
</select>
<select v-model="formData.trend_strength" class="select" required>
<option value="强"></option>
<option value="弱"></option>
<option value="震荡">震荡</option>
</select>
<select v-model.number="formData.account_id" class="select" required>
<option :value="null" disabled>选择账户</option>
<option v-for="a in accounts" :key="a.id" :value="a.id">{{ a.account_name }}</option>
</select>
<select v-model.number="formData.strategy_id" class="select" required>
<option :value="null" disabled>选择策略</option>
<option v-for="s in strategies" :key="s.id" :value="s.id">{{ s.strategy_name }}</option>
</select>
<select v-model="formData.force_direction" class="select">
<option value="">跟随大盘</option>
<option value="做多">做多</option>
<option value="做空">做空</option>
</select>
</template>
<div class="flex gap-2 justify-end mt-2">
<button type="button" @click="showModal = false" class="btn-ghost">取消</button>
<button type="submit" class="btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import {
getRegimes, createRegime, updateRegime, deleteRegime,
getAccounts, createAccount, updateAccount, deleteAccount,
getStrategies, createStrategy, updateStrategy, deleteStrategy,
getMatches, createMatch, updateMatch, deleteMatch,
} from '../api'
const tabs = [
{ key: 'regimes', label: '大盘管理' },
{ key: 'accounts', label: '账户管理' },
{ key: 'strategies', label: '策略管理' },
{ key: 'matches', label: '匹配配置' },
]
const activeTab = ref('regimes')
const regimes = ref([])
const accounts = ref([])
const strategies = ref([])
const matchList = ref([])
// 弹窗
const showModal = ref(false)
const modalTitle = ref('')
const formType = ref('')
const formData = ref({})
const editingId = ref(null)
async function loadAll() {
const [r, a, s, m] = await Promise.all([
getRegimes(), getAccounts(), getStrategies(), getMatches(),
])
regimes.value = r.data
accounts.value = a.data
strategies.value = s.data
matchList.value = m.data
}
// ── 大盘 ──
function openRegimeForm(item = null) {
formType.value = 'regime'
editingId.value = item?.id || null
modalTitle.value = item ? '编辑大盘阶段' : '新增大盘阶段'
formData.value = item ? { ...item } : { name: '', trade_type: '顺势', allow_direction: '做多', remark: '' }
showModal.value = true
}
async function handleDeleteRegime(id) {
if (!confirm('确定删除此大盘阶段?关联匹配规则也会删除。')) return
await deleteRegime(id)
loadAll()
}
// ── 账户 ──
function openAccountForm(item = null) {
formType.value = 'account'
editingId.value = item?.id || null
modalTitle.value = item ? '编辑账户' : '新增账户'
formData.value = item ? { ...item } : { account_name: '', total_capital: 100, trade_cycle: '', risk_ratio: '', enable: 1, remark: '' }
showModal.value = true
}
async function handleDeleteAccount(id) {
if (!confirm('确定删除此账户?')) return
await deleteAccount(id)
loadAll()
}
// ── 策略 ──
function openStrategyForm(item = null) {
formType.value = 'strategy'
editingId.value = item?.id || null
modalTitle.value = item ? '编辑策略' : '新增策略'
formData.value = item ? { ...item } : { strategy_name: '', fit_cycle: '', fit_trend_strength: '强', trade_type: '顺势', strategy_rule: '', remark: '' }
showModal.value = true
}
async function handleDeleteStrategy(id) {
if (!confirm('确定删除此策略?')) return
await deleteStrategy(id)
loadAll()
}
// ── 匹配 ──
function openMatchForm(item = null) {
formType.value = 'match'
editingId.value = item?.id || null
modalTitle.value = item ? '编辑匹配规则' : '新增匹配规则'
formData.value = item
? { ...item }
: { market_regime_id: null, market_cycle: '日线', trend_strength: '强', account_id: null, strategy_id: null, force_direction: '' }
showModal.value = true
}
async function handleDeleteMatch(id) {
if (!confirm('确定删除此匹配规则?')) return
await deleteMatch(id)
loadAll()
}
// ── 提交 ──
async function handleSubmit() {
const id = editingId.value
const data = { ...formData.value }
delete data.id
delete data.regime_name
delete data.account_name
delete data.strategy_name
const actions = {
regime: id ? () => updateRegime(id, data) : () => createRegime(data),
account: id ? () => updateAccount(id, data) : () => createAccount(data),
strategy: id ? () => updateStrategy(id, data) : () => createStrategy(data),
match: id ? () => updateMatch(id, data) : () => createMatch(data),
}
await actions[formType.value]()
showModal.value = false
loadAll()
}
onMounted(loadAll)
</script>
+290
View File
@@ -0,0 +1,290 @@
<template>
<div class="flex gap-6 h-[calc(100vh-120px)]">
<!-- 左侧人工选择 -->
<div class="w-80 shrink-0 card flex flex-col gap-6">
<h2 class="text-base font-semibold border-b border-dark-border pb-3">人工选择</h2>
<!-- 大盘周期 -->
<div>
<label class="block text-sm text-dark-muted mb-2">大盘周期</label>
<div class="flex flex-col gap-2">
<button
v-for="c in cycles"
:key="c"
@click="selected.cycle = c"
class="px-4 py-2.5 rounded-md text-sm text-left transition-colors border"
:class="selected.cycle === c
? 'border-dark-accent bg-dark-accent/10 text-dark-accent'
: 'border-dark-border hover:border-dark-muted text-dark-text'"
>
{{ c }}
</button>
</div>
</div>
<!-- 大盘阶段 -->
<div>
<label class="block text-sm text-dark-muted mb-2">大盘阶段</label>
<div class="flex flex-col gap-1.5 max-h-64 overflow-y-auto">
<button
v-for="r in regimes"
:key="r.id"
@click="selected.regimeId = r.id"
class="px-3 py-2 rounded-md text-sm text-left transition-colors border"
:class="selected.regimeId === r.id
? 'border-dark-accent bg-dark-accent/10 text-dark-accent'
: 'border-dark-border hover:border-dark-muted text-dark-text'"
>
<span>{{ r.name }}</span>
<span class="ml-2 text-xs text-dark-muted">{{ r.trade_type }} · {{ r.allow_direction }}</span>
</button>
</div>
</div>
<!-- 趋势强弱 -->
<div>
<label class="block text-sm text-dark-muted mb-2">趋势强弱</label>
<div class="flex gap-2">
<button
v-for="s in strengths"
:key="s.value"
@click="selected.strength = s.value"
class="flex-1 px-3 py-2.5 rounded-md text-sm transition-colors border"
:class="selected.strength === s.value
? strengthActiveClass(s.value)
: 'border-dark-border hover:border-dark-muted text-dark-text'"
>
{{ s.label }}
</button>
</div>
</div>
<!-- 匹配按钮 -->
<button
@click="runMatchQuery"
:disabled="!canMatch || loading"
class="btn-primary w-full py-3 disabled:opacity-40 disabled:cursor-not-allowed"
>
{{ loading ? '匹配中...' : '执行前置匹配' }}
</button>
</div>
<!-- 右侧匹配结果 -->
<div class="flex-1 flex flex-col gap-4 overflow-y-auto">
<!-- 未匹配提示 -->
<div v-if="!result" class="card flex-1 flex items-center justify-center text-dark-muted">
<div class="text-center">
<p class="text-lg mb-2">请选择大盘周期阶段趋势强弱</p>
<p class="text-sm">点击执行前置匹配查看可用账户与策略</p>
</div>
</div>
<template v-else>
<!-- 状态提示 -->
<div
class="card border-l-4"
:class="statusBorderClass"
>
<div class="flex items-center gap-3">
<span class="text-2xl">{{ statusIcon }}</span>
<div>
<p class="font-medium">{{ result.message }}</p>
<p class="text-sm text-dark-muted mt-1">
{{ result.market_cycle }} · {{ result.regime_name }} · {{ result.trend_strength }}
</p>
</div>
</div>
</div>
<!-- 大盘信息 -->
<div class="grid grid-cols-3 gap-4">
<div class="card text-center">
<p class="text-xs text-dark-muted mb-1">交易类型</p>
<p class="text-lg font-semibold" :class="tradeTypeColor">{{ result.trade_type || '—' }}</p>
</div>
<div class="card text-center">
<p class="text-xs text-dark-muted mb-1">允许开仓方向</p>
<p class="text-lg font-semibold" :class="directionColor">{{ result.allow_direction || '—' }}</p>
</div>
<div class="card text-center">
<p class="text-xs text-dark-muted mb-1">匹配状态</p>
<p class="text-lg font-semibold" :class="statusTextColor">{{ statusLabel }}</p>
</div>
</div>
<!-- 可用账户 -->
<div class="card">
<h3 class="text-sm font-semibold mb-3 flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-dark-success"></span>
可用账户
<span class="text-dark-muted font-normal">({{ result.accounts?.length || 0 }})</span>
</h3>
<div v-if="!result.accounts?.length" class="text-dark-muted text-sm py-4 text-center">
无可用账户
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div
v-for="acc in result.accounts"
:key="acc.id"
class="border border-dark-border rounded-md p-3 hover:border-dark-accent/50 transition-colors"
>
<div class="flex justify-between items-start">
<p class="font-medium">{{ acc.account_name }}</p>
<span class="text-xs px-2 py-0.5 rounded bg-dark-hover text-dark-muted">
{{ acc.force_direction || result.allow_direction }}
</span>
</div>
<div class="mt-2 grid grid-cols-3 gap-2 text-xs text-dark-muted">
<div>
<span class="block text-dark-text font-medium">{{ acc.total_capital }}U</span>
本金
</div>
<div>
<span class="block text-dark-text font-medium">{{ acc.trade_cycle }}</span>
周期
</div>
<div>
<span class="block text-dark-text font-medium">{{ acc.risk_ratio }}</span>
风险
</div>
</div>
</div>
</div>
</div>
<!-- 可用策略 -->
<div class="card">
<h3 class="text-sm font-semibold mb-3 flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-dark-accent"></span>
可用策略
<span class="text-dark-muted font-normal">({{ result.strategies?.length || 0 }})</span>
</h3>
<div v-if="!result.strategies?.length" class="text-dark-muted text-sm py-4 text-center">
无可用策略
</div>
<div v-else class="flex flex-col gap-3">
<div
v-for="strat in result.strategies"
:key="strat.id"
class="border border-dark-border rounded-md p-4 hover:border-dark-accent/50 transition-colors"
>
<div class="flex justify-between items-start mb-2">
<p class="font-medium text-base">{{ strat.strategy_name }}</p>
<span class="text-xs px-2 py-0.5 rounded bg-dark-hover text-dark-muted">
{{ strat.fit_cycle }}
</span>
</div>
<pre class="text-xs text-dark-muted whitespace-pre-wrap leading-relaxed bg-dark-bg rounded p-3">{{ strat.strategy_rule }}</pre>
<p class="text-xs text-dark-warning mt-2">
策略规则仅作参考展示箱体/点位/突破条件需人工手动确认
</p>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { getRegimes, doMatch } from '../api'
const cycles = ['日线', '4H', '1H']
const strengths = [
{ value: '强', label: '强趋势' },
{ value: '弱', label: '弱趋势' },
{ value: '震荡', label: '震荡' },
]
const regimes = ref([])
const selected = ref({ cycle: '', regimeId: null, strength: '' })
const result = ref(null)
const loading = ref(false)
const canMatch = computed(() =>
selected.value.cycle && selected.value.regimeId && selected.value.strength
)
const statusLabel = computed(() => {
if (!result.value) return ''
const map = { ok: '可交易', watch: '观望', disabled: '禁用' }
return map[result.value.status] || result.value.status
})
const statusIcon = computed(() => {
if (!result.value) return ''
const map = { ok: '✅', watch: '⏸️', disabled: '🚫' }
return map[result.value.status] || '❓'
})
const statusBorderClass = computed(() => {
if (!result.value) return ''
const map = {
ok: 'border-l-dark-success',
watch: 'border-l-dark-warning',
disabled: 'border-l-dark-danger',
}
return map[result.value.status] || ''
})
const statusTextColor = computed(() => {
if (!result.value) return ''
const map = {
ok: 'text-dark-success',
watch: 'text-dark-warning',
disabled: 'text-dark-danger',
}
return map[result.value.status] || ''
})
const tradeTypeColor = computed(() => {
if (!result.value) return ''
const map = { '顺势': 'text-dark-success', '反转': 'text-dark-warning', '观望': 'text-dark-muted' }
return map[result.value.trade_type] || ''
})
const directionColor = computed(() => {
if (!result.value) return ''
if (result.value.allow_direction === '禁止') return 'text-dark-danger'
if (result.value.allow_direction === '做多') return 'text-dark-success'
if (result.value.allow_direction === '做空') return 'text-dark-danger'
return 'text-dark-accent'
})
function strengthActiveClass(val) {
const map = {
'强': 'border-dark-success bg-dark-success/10 text-dark-success',
'弱': 'border-dark-accent bg-dark-accent/10 text-dark-accent',
'震荡': 'border-dark-warning bg-dark-warning/10 text-dark-warning',
}
return map[val] || ''
}
async function loadRegimes() {
const { data } = await getRegimes()
regimes.value = data
}
async function runMatchQuery() {
if (!canMatch.value) return
loading.value = true
try {
const { data } = await doMatch({
market_cycle: selected.value.cycle,
market_regime_id: selected.value.regimeId,
trend_strength: selected.value.strength,
})
result.value = data
} catch (e) {
result.value = {
status: 'disabled',
message: '匹配请求失败:' + (e.response?.data?.detail || e.message),
}
} finally {
loading.value = false
}
}
onMounted(loadRegimes)
</script>
+23
View File
@@ -0,0 +1,23 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{vue,js}'],
theme: {
extend: {
colors: {
dark: {
bg: '#0a0a0a',
card: '#141414',
border: '#262626',
hover: '#1f1f1f',
text: '#e5e5e5',
muted: '#737373',
accent: '#3b82f6',
success: '#22c55e',
warning: '#eab308',
danger: '#ef4444',
},
},
},
},
plugins: [],
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 1125,
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
},
},
},
})