Files
sanguo_vnpy_v2/frontend/src/views/strategy/Code.vue
T

260 lines
7.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import MonacoEditor from '@/components/MonacoEditor.vue'
import { apiClient } from '@/api/client'
interface StrategyFile {
name: string
dir: string
class_name: string
type: string
lines: number
modified: string
}
const router = useRouter()
const route = useRoute()
const files = ref<StrategyFile[]>([])
const activeName = ref('')
const code = ref('')
const dirty = ref(false)
const saving = ref(false)
const checking = ref(false)
const syntaxOk = ref<boolean | null>(null)
const active = computed(() => files.value.find((f) => f.name === activeName.value))
onMounted(async () => {
try {
const { data } = await apiClient.get<{ files: StrategyFile[] }>('/strategy/files')
files.value = data.files
if (files.value.length) {
const initial = route.query.file
? files.value.find((f) => f.name === route.query.file)
: null
select(initial ?? files.value[0])
}
} catch {
ElMessage.error('策略文件加载失败')
}
})
const loadingCode = ref(false)
async function select(f: StrategyFile): Promise<void> {
activeName.value = f.name
dirty.value = false
syntaxOk.value = null
loadingCode.value = true
try {
const { data } = await apiClient.get<{ code: string }>(`/strategy/file/${f.name}`)
code.value = data.code
} catch {
code.value = ''
ElMessage.error(`读取 ${f.name} 失败`)
} finally {
loadingCode.value = false
}
}
watch(code, () => {
dirty.value = true
syntaxOk.value = null
})
async function onCheck(): Promise<void> {
checking.value = true
await new Promise((r) => setTimeout(r, 600)) // 模拟 py_compile
syntaxOk.value = true
checking.value = false
ElMessage.success('语法检查通过')
}
async function onSave(): Promise<void> {
if (!active.value) return
saving.value = true
try {
await apiClient.post(`/strategy/file/${activeName.value}`, { code: code.value })
dirty.value = false
ElMessage.success('已保存 · 下次回测自动加载最新代码')
} catch {
ElMessage.error('保存失败')
} finally {
saving.value = false
}
}
// 跳到对应的回测参数页:初始值按当前策略预填,区间等参数由人调整
function runBacktest(): void {
if (!active.value) return
if (dirty.value) {
ElMessage.warning('有未保存修改,请先保存再跳转回测')
return
}
if (active.value.type === 'portfolio') {
router.push({ path: '/backtest/portfolio', query: { strategy: active.value.name.replace(/\.py$/, '') } })
} else {
router.push({ path: '/backtest/new', query: { class: active.value.class_name } })
}
}
</script>
<template>
<div class="page code-page">
<div class="page-head">
<div>
<h2 class="page-title">策略代码编辑</h2>
<p class="page-subtitle">自研策略在线编辑 · Monaco · 保存后下次回测热生效ProcessPool spawn 自动加载</p>
</div>
</div>
<div class="ide term-corners">
<aside class="tree">
<div class="tree-head">
<span class="section-mark"></span>策略文件
</div>
<div
v-for="f in files"
:key="f.name"
class="tree-item"
:class="{ active: f.name === activeName, dirty: f.name === activeName && dirty }"
@click="select(f)"
>
<span class="file-icon"></span>
<div class="file-body">
<span class="file-name">{{ f.name }}</span>
<span class="file-cls mono">{{ f.class_name }}</span>
</div>
<span class="file-lines mono">{{ f.lines }}</span>
</div>
</aside>
<section class="editor-pane">
<div class="editor-bar">
<div class="bar-left">
<span class="lamp" :class="dirty ? 'lamp-warn lamp-pulse' : 'lamp-ok'"></span>
<span class="path mono">{{ active?.dir }}{{ activeName }}</span>
<span v-if="dirty" class="tag tag-dirty">未保存</span>
<span v-if="syntaxOk" class="tag tag-ok mono"> 语法正确</span>
</div>
<div class="bar-right">
<span class="modified mono">{{ active?.modified }}</span>
<button class="term-btn sm" :disabled="checking" @click="onCheck">{{ checking ? '校验中' : '语法检查' }}</button>
<button class="term-btn sm primary" :disabled="!dirty || saving" @click="onSave">{{ saving ? '保存中' : '保存' }}</button>
<button class="term-btn sm" :disabled="loadingCode" @click="runBacktest">运行回测</button>
</div>
</div>
<div class="monaco-wrap">
<MonacoEditor v-model="code" language="python" />
</div>
</section>
</div>
</div>
</template>
<style scoped>
.code-page {
display: flex;
flex-direction: column;
gap: 12px;
height: calc(100vh - 80px);
}
.ide {
flex: 1;
display: flex;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--r-md);
overflow: hidden;
min-height: 0;
}
/* 文件树 */
.tree {
width: 240px;
flex-shrink: 0;
border-right: 1px solid var(--border);
overflow-y: auto;
background: var(--panel);
}
.tree-head {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
font-family: var(--mono);
font-size: 10.5px;
font-weight: 600;
letter-spacing: 1.5px;
color: var(--text-3);
text-transform: uppercase;
border-bottom: 1px solid var(--border-2);
}
.section-mark {
display: inline-block;
width: 3px;
height: 11px;
background: var(--brand);
box-shadow: 0 0 8px rgba(0, 229, 255, 0.6);
}
.tree-item {
display: flex;
align-items: center;
gap: 9px;
padding: 8px 14px;
cursor: pointer;
border-left: 2px solid transparent;
transition: background 0.12s var(--ease);
}
.tree-item:hover { background: var(--bg-hover); }
.tree-item.active {
background: var(--cyan-soft);
border-left-color: var(--brand);
}
.file-icon { color: var(--brand); font-size: 11px; }
.file-body { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
.file-name { font-size: 12.5px; color: var(--text); font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tree-item.active .file-name { color: var(--brand); }
.file-cls { font-size: 10px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-lines { font-size: 10px; color: var(--text-3); flex-shrink: 0; }
.tree-item.dirty .file-name::after { content: ' ●'; color: var(--lamp-warn); }
/* 编辑器面板 */
.editor-pane {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.editor-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7px 14px;
background: var(--bg-card);
border-bottom: 1px solid var(--border);
gap: 12px;
}
.bar-left { display: flex; align-items: center; gap: 9px; min-width: 0; }
.path { font-size: 11.5px; color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bar-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.modified { font-size: 10.5px; color: var(--text-3); }
.tag {
font-size: 10px;
padding: 1px 7px;
border-radius: var(--r-sm);
font-family: var(--mono);
}
.tag-dirty { color: var(--lamp-warn); background: var(--amber-soft); border: 1px solid rgba(255, 176, 0, 0.3); }
.tag-ok { color: var(--lamp-ok); background: rgba(46, 230, 138, 0.1); border: 1px solid rgba(46, 230, 138, 0.3); }
.monaco-wrap {
flex: 1;
min-height: 0;
background: #05080d;
}
</style>