first commit
This commit is contained in:
@@ -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>
|
||||
Generated
+2773
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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: [],
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user