fix(frontend): 用户验收8项遗漏全补(并发中反馈漏处理对账)——①模拟/实盘改回必选档案(初始设计口径,提交拦截+必选文案)②backtest/new?class=all_weather.py&instance=1失败根治:档案类型按文件判非instance.type(老档案type不准)+组合页兼容class参数+CTA页组合档案自动重定向组合页(实测跳转+预填全通)③运行灯语义:有在跑→跳列表按档案过滤,无→创建页(用户'点带绿点按钮应看运行不该到参数页');组合档案隐藏回放按钮 ④代码编辑运行回测不好使:根因chain=dev缺GET file mock→读失败catch清code被当编辑→dirty卡死;三层修=补mock+catch走echoGrace+dirty改确认框(保存并跳/直接跳) ⑤模拟盘列表重设计对齐实盘(类型chip入名称行+新增策略/档案列+净值补日期) ⑥策略库去重复编辑代码按钮 ⑦模拟盘/实盘列表行加「快照」按钮→CodeSnapshotDrawer共享抽屉(发起时代码vs当前,MonacoDiff滚动联动);dev浏览器自验7项全过+927绿+build绿 [vps]
This commit is contained in:
@@ -31,6 +31,12 @@ const routes: Route[] = [
|
||||
{ m: 'get', re: /^\/strategy\/files$/, build: () => D.strategyFilesMock },
|
||||
{ m: 'post', re: /^\/strategy\/file\/[^/]+\/check$/, build: () => ({ ok: true }) },
|
||||
{ m: 'post', re: /^\/strategy\/file\/[^/]+$/, build: () => ({ ok: true }) },
|
||||
// 读单文件全量(编辑器主区)——此前 dev 缺此 mock,切文件报「读取失败」
|
||||
{ m: 'get', re: /^\/strategy\/file\/[^/]+$/, build: (url) => {
|
||||
const name = decodeURIComponent(url.split('/').filter(Boolean)[2] || '')
|
||||
return { name, dir: '', class_name: '', type: 'cta', lines: 10, modified: '',
|
||||
code: `# ${name}(mock 源码)\n\nclass DemoStrategy:\n """dev mock"""\n\n def initialize(self, context):\n self.n = 3\n\n def handle_data(self, context, data):\n pass\n` }
|
||||
} },
|
||||
// §12.6 补:代码版本快照(列表在前,单版本 4 段路径在后)
|
||||
{ m: 'get', re: /^\/strategy\/code-versions$/, build: (url) => D.codeVersionsMock(new URL(url, 'http://x').searchParams.get('file') || '') },
|
||||
{ m: 'get', re: /^\/strategy\/code-versions\/[^/]+\/[^/]+$/, build: (url) => { const seg = url.split('/').filter(Boolean); return D.codeVersionMock(decodeURIComponent(seg[2]), seg[3]) } },
|
||||
|
||||
@@ -16,6 +16,7 @@ interface InstLike {
|
||||
interface FileLike {
|
||||
name: string
|
||||
class_name: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
@@ -137,6 +138,21 @@ onMounted(async () => {
|
||||
])
|
||||
instanceOptions.value = ins
|
||||
classToFile.value = Object.fromEntries(fr.data.files.map((f) => [f.class_name, f.name]))
|
||||
// 组合策略档案误入 CTA 回测页 → 重定向组合页(老链接 class=all_weather.py 场景)
|
||||
const cls = route.query.class
|
||||
const instId0 = route.query.instance
|
||||
const inst = instId0 && !Array.isArray(instId0)
|
||||
? ins.find((x) => x.id === Number(instId0))
|
||||
: undefined
|
||||
const probe = inst?.code_file || (typeof cls === 'string' && cls.includes('.') ? cls : '')
|
||||
const probeFile = fr.data.files.find((f) => f.name === probe || f.name === `${probe}.py`)
|
||||
if (probeFile?.type === 'portfolio') {
|
||||
router.replace({
|
||||
path: '/backtest/portfolio',
|
||||
query: { strategy: probeFile.name.replace(/\.py$/, ''), ...(instId0 ? { instance: String(instId0) } : {}) },
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
/* 档案下拉失败不阻塞 */
|
||||
}
|
||||
|
||||
@@ -389,9 +389,9 @@ const fromTaskId = computed(() => (route.query.from_task as string) || '')
|
||||
onMounted(async () => {
|
||||
if (viewTaskId.value) {
|
||||
loadResult(viewTaskId.value)
|
||||
} else if (route.query.strategy) {
|
||||
// 从代码编辑页「运行回测」跳转:按策略预填
|
||||
form.strategy = String(route.query.strategy)
|
||||
} else if (route.query.strategy || route.query.class) {
|
||||
// 跳转预填:strategy=文件名去.py;兼容旧链接的 class=(可能是 all_weather.py 带后缀)
|
||||
form.strategy = String(route.query.strategy || route.query.class).replace(/\.py$/, '')
|
||||
} else if (fromTaskId.value) {
|
||||
// 从任务列表「点任务ID」跳转:按该任务参数完整预填
|
||||
void prefillFromTask(fromTaskId.value)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import CodeSnapshotDrawer from '@/views/strategy/CodeSnapshotDrawer.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
listLives, startLive, stopLive, deleteLive, updateLive,
|
||||
@@ -102,6 +103,16 @@ async function onDelete(a: LiveAccount): Promise<void> {
|
||||
}
|
||||
|
||||
// 编辑(仅 stopped 可改;后端运行中会 400)
|
||||
// 代码快照(§12.6 补:查看该账户发起时的代码 vs 当前)
|
||||
const snapshotRow = ref<{ instanceId: number; codeHash: string; label: string } | null>(null)
|
||||
function openSnapshot(row: { id: number; name?: string; code_hash?: string; instance_id?: number }): void {
|
||||
snapshotRow.value = {
|
||||
instanceId: Number(row.instance_id),
|
||||
codeHash: String(row.code_hash || ''),
|
||||
label: row.name || `live#${row.id}`,
|
||||
}
|
||||
}
|
||||
|
||||
const editVisible = ref(false)
|
||||
const editForm = reactive({ id: 0, name: '', account: '', vt_symbol: '', strategy_name: '', interval: '' })
|
||||
function openEdit(a: LiveAccount): void {
|
||||
@@ -235,7 +246,7 @@ async function onStop(a: LiveAccount): Promise<void> {
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240">
|
||||
<el-table-column label="操作" width="290">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status !== 'running'"
|
||||
@@ -250,6 +261,11 @@ async function onStop(a: LiveAccount): Promise<void> {
|
||||
@click="onStop(row)"
|
||||
>停止</el-button>
|
||||
<el-button link type="primary" size="small" @click="goMonitor(row)">监控</el-button>
|
||||
<el-button
|
||||
v-if="row.code_hash && row.instance_id"
|
||||
link type="primary" size="small"
|
||||
@click="openSnapshot(row)"
|
||||
>快照</el-button>
|
||||
<el-button v-if="row.status !== 'running'" link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status !== 'running'" link type="danger" size="small" :loading="actionLoading === row.id" @click="onDelete(row)">删除</el-button>
|
||||
</template>
|
||||
@@ -257,6 +273,14 @@ async function onStop(a: LiveAccount): Promise<void> {
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<CodeSnapshotDrawer
|
||||
v-if="snapshotRow"
|
||||
:instance-id="snapshotRow.instanceId"
|
||||
:code-hash="snapshotRow.codeHash"
|
||||
:label="snapshotRow.label"
|
||||
@close="snapshotRow = null"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="editVisible" title="编辑实盘实例" width="480px">
|
||||
<el-form :model="editForm" label-width="90px">
|
||||
<el-form-item label="名称"><el-input v-model="editForm.name" /></el-form-item>
|
||||
|
||||
@@ -126,6 +126,11 @@ function onPickInstance(id: number | null): void {
|
||||
}
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
// §12.6 初始设计口径(2026-08-16 用户重申):实盘必须从实例档案发起
|
||||
if (selectedInstanceId.value == null) {
|
||||
ElMessage.warning('实盘必须选择「实例档案」发起——可先在策略库创建档案')
|
||||
return
|
||||
}
|
||||
if (!form.value.name.trim()) {
|
||||
ElMessage.warning('请填写实例名')
|
||||
return
|
||||
@@ -232,7 +237,7 @@ async function onSubmit(): Promise<void> {
|
||||
</el-select>
|
||||
<el-select
|
||||
:model-value="selectedInstanceId" clearable filterable
|
||||
placeholder="实例档案(可选·带出参数)"
|
||||
placeholder="必选:实例档案(策略库可创建)"
|
||||
style="width: 260px; margin-left: 8px"
|
||||
@update:model-value="onPickInstance"
|
||||
>
|
||||
@@ -242,7 +247,7 @@ async function onSubmit(): Promise<void> {
|
||||
/>
|
||||
</el-select>
|
||||
<span v-if="selectedInstanceId != null" class="muted form-hint">绑定档案 #{{ selectedInstanceId }}(发起时快照)</span>
|
||||
<span v-else class="muted form-hint">实盘需 A 股适配,目前仅 DoubleMa 完成适配</span>
|
||||
<span v-else class="muted form-hint" style="color: var(--amber)">必选:实盘从档案发起(仅 DoubleMa 完成 A 股适配)</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="标的(vt_symbol)">
|
||||
<el-input v-model="form.vt_symbol" placeholder="600000 或 600000.SSE" style="width: 320px" />
|
||||
@@ -319,7 +324,7 @@ async function onSubmit(): Promise<void> {
|
||||
</el-select>
|
||||
<el-select
|
||||
:model-value="selectedInstanceId" clearable filterable
|
||||
placeholder="实例档案(可选·带出参数)"
|
||||
placeholder="必选:实例档案(策略库可创建)"
|
||||
style="width: 260px; margin-left: 8px"
|
||||
@update:model-value="onPickInstance"
|
||||
>
|
||||
@@ -329,7 +334,7 @@ async function onSubmit(): Promise<void> {
|
||||
/>
|
||||
</el-select>
|
||||
<span v-if="selectedInstanceId != null" class="muted form-hint">绑定档案 #{{ selectedInstanceId }}(发起时快照)</span>
|
||||
<span v-else class="muted form-hint">supervisor 拉起 runner_live 子进程(bullet_trade LiveEngine)</span>
|
||||
<span v-else class="muted form-hint" style="color: var(--amber)">必选:实盘从档案发起(supervisor 拉起 runner_live 子进程)</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="标的池">
|
||||
<el-select v-model="poolForm.pool" style="width: 320px">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import CodeSnapshotDrawer from '@/views/strategy/CodeSnapshotDrawer.vue'
|
||||
import {
|
||||
listPapers, stopPaper, resumePaper, deletePaper, updatePaper,
|
||||
type PaperAccount,
|
||||
@@ -52,6 +53,17 @@ const stats = computed(() => ({
|
||||
}))
|
||||
|
||||
const modeLabel: Record<string, string> = { replay: '回放', live: '实走', shadow: '影子' }
|
||||
// 行内解析策略名(strategies 是 JSON 字符串或已解析数组)
|
||||
function strategyOf(row: { strategies?: unknown }): string {
|
||||
const raw = row.strategies
|
||||
let list: { name?: string }[] = []
|
||||
if (typeof raw === 'string') {
|
||||
try { list = JSON.parse(raw) as { name?: string }[] } catch { list = [] }
|
||||
} else if (Array.isArray(raw)) {
|
||||
list = raw as { name?: string }[]
|
||||
}
|
||||
return list[0]?.name || ''
|
||||
}
|
||||
const statusLabel: Record<string, string> = { running: '运行中', done: '完成', failed: '失败', created: '已创建', pending: '排队', stopped: '已停止' }
|
||||
|
||||
function pct(v: number | null | undefined): string {
|
||||
@@ -137,6 +149,16 @@ async function onDelete(a: PaperAccount): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// 代码快照(§12.6 补:查看该账户发起时的代码 vs 当前)
|
||||
const snapshotRow = ref<{ instanceId: number; codeHash: string; label: string } | null>(null)
|
||||
function openSnapshot(row: PaperAccount & { code_hash?: string; instance_id?: number }): void {
|
||||
snapshotRow.value = {
|
||||
instanceId: Number(row.instance_id),
|
||||
codeHash: String(row.code_hash || ''),
|
||||
label: row.name || `paper#${row.id}`,
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑(MVP:名称/标的/初始资金)
|
||||
const editVisible = ref(false)
|
||||
const editForm = reactive({ id: 0, name: '', symbols: '', initial_capital: 0 })
|
||||
@@ -207,17 +229,21 @@ async function saveEdit(): Promise<void> {
|
||||
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table :data="filtered" size="small" empty-text="暂无模拟盘">
|
||||
<el-table-column label="名称 / ID" min-width="150">
|
||||
<el-table-column label="名称 / ID" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-name">{{ row.name }}</div>
|
||||
<div class="cell-name">
|
||||
{{ row.name }}
|
||||
<span class="chip" :class="row.strategy_type === 'portfolio' ? 'chip-portfolio' : 'chip-cta'">
|
||||
{{ typeLabel[row.strategy_type] || '个股' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="cell-id mono">#{{ row.id }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="76">
|
||||
<el-table-column label="策略 / 档案" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<span class="chip" :class="row.strategy_type === 'portfolio' ? 'chip-portfolio' : 'chip-cta'">
|
||||
{{ typeLabel[row.strategy_type] || row.strategy_type || '个股' }}
|
||||
</span>
|
||||
<div class="cell-name">{{ strategyOf(row) || '—' }}</div>
|
||||
<div class="cell-id mono muted">{{ row.instance_id ? `档案 #${row.instance_id}` : '未绑档案' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="模式" width="92">
|
||||
@@ -237,8 +263,11 @@ async function saveEdit(): Promise<void> {
|
||||
<span class="mono" :class="(row.total_return ?? 0) >= 0 ? 'up' : 'down'">{{ pct(row.total_return) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最新净值" width="104" align="right">
|
||||
<template #default="{ row }"><span class="mono">{{ num(row.latest_equity) }}</span></template>
|
||||
<el-table-column label="最新净值" width="128" align="right">
|
||||
<template #default="{ row }">
|
||||
<div class="mono">{{ num(row.latest_equity) }}</div>
|
||||
<div class="cell-id mono muted">{{ row.latest_date ?? '—' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="142">
|
||||
<template #default="{ row }"><span class="mono muted">{{ createdShort(row.created_at) }}</span></template>
|
||||
@@ -246,9 +275,14 @@ async function saveEdit(): Promise<void> {
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }"><span class="chip" :class="`st-${row.status}`">{{ statusLabel[row.status] || row.status }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="176" fixed="right">
|
||||
<el-table-column label="操作" width="216" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="open(row)">查看</el-button>
|
||||
<el-button
|
||||
v-if="row.code_hash && row.instance_id"
|
||||
link type="primary"
|
||||
@click="openSnapshot(row)"
|
||||
>快照</el-button>
|
||||
<el-button
|
||||
v-if="row.mode !== 'replay' && row.status === 'running'"
|
||||
link type="warning" :loading="actionLoading === row.id"
|
||||
@@ -266,6 +300,14 @@ async function saveEdit(): Promise<void> {
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<CodeSnapshotDrawer
|
||||
v-if="snapshotRow"
|
||||
:instance-id="snapshotRow.instanceId"
|
||||
:code-hash="snapshotRow.codeHash"
|
||||
:label="snapshotRow.label"
|
||||
@close="snapshotRow = null"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="editVisible" title="编辑模拟盘" width="480px">
|
||||
<el-form :model="editForm" label-width="90px">
|
||||
<el-form-item label="名称">
|
||||
|
||||
@@ -166,6 +166,11 @@ watch(strategyType, (t) => {
|
||||
})
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
// §12.6 初始设计口径(2026-08-16 用户重申):模拟盘必须从实例档案发起
|
||||
if (selectedInstanceId.value == null) {
|
||||
ElMessage.warning('模拟盘必须选择「实例档案」发起——可先在策略库创建档案(选参数保存即成档案)')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const payload: PaperCreate = { ...form.value, instance_id: selectedInstanceId.value }
|
||||
@@ -264,7 +269,7 @@ function onSymbols(v: string): void {
|
||||
</el-select>
|
||||
<el-select
|
||||
:model-value="selectedInstanceId" clearable filterable
|
||||
placeholder="实例档案(可选·带出参数)"
|
||||
placeholder="必选:实例档案(策略库可创建)"
|
||||
style="width: 260px; margin-left: 8px"
|
||||
@update:model-value="onPickInstance"
|
||||
>
|
||||
@@ -274,6 +279,7 @@ function onSymbols(v: string): void {
|
||||
/>
|
||||
</el-select>
|
||||
<span v-if="selectedInstanceId != null" class="muted form-hint">绑定档案 #{{ selectedInstanceId }}(发起时快照)</span>
|
||||
<span v-else class="muted form-hint" style="color: var(--amber)">必选:模拟盘从档案发起</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="标的池">
|
||||
<el-select v-model="poolForm.pool" style="width: 320px">
|
||||
@@ -330,7 +336,7 @@ function onSymbols(v: string): void {
|
||||
</el-select>
|
||||
<el-select
|
||||
:model-value="selectedInstanceId" clearable filterable
|
||||
placeholder="实例档案(可选·带出参数)"
|
||||
placeholder="必选:实例档案(策略库可创建)"
|
||||
style="width: 260px; margin-left: 8px"
|
||||
@update:model-value="onPickInstance"
|
||||
>
|
||||
@@ -340,7 +346,7 @@ function onSymbols(v: string): void {
|
||||
/>
|
||||
</el-select>
|
||||
<span v-if="selectedInstanceId != null" class="muted form-hint">绑定档案 #{{ selectedInstanceId }}(发起时快照)</span>
|
||||
<span v-else class="muted form-hint">CTA 策略(个股),如 DoubleMaStrategy</span>
|
||||
<span v-else class="muted form-hint" style="color: var(--amber)">必选:模拟盘从档案发起(CTA 个股策略)</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="撮合时点">
|
||||
<el-select v-model="form.strategies[0].match_session" style="width: 280px">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import MonacoEditor from '@/components/MonacoEditor.vue'
|
||||
import MonacoDiff from '@/components/MonacoDiff.vue'
|
||||
import { apiClient } from '@/api/client'
|
||||
@@ -71,7 +71,11 @@ async function select(f: StrategyFile): Promise<void> {
|
||||
code.value = data.code
|
||||
window.setTimeout(() => { echoGrace = false }, 300)
|
||||
} catch {
|
||||
// 失败也走 echoGrace:清空 code 的回写不能当用户编辑(否则 dirty 卡死,
|
||||
// 运行回测被「未保存」拦死——2026-08-16 实锤链)
|
||||
echoGrace = true
|
||||
code.value = ''
|
||||
window.setTimeout(() => { echoGrace = false }, 300)
|
||||
ElMessage.error(`读取 ${f.name} 失败`)
|
||||
} finally {
|
||||
loadingCode.value = false
|
||||
@@ -122,11 +126,20 @@ async function onSave(): Promise<void> {
|
||||
}
|
||||
|
||||
// 跳到对应的回测参数页:初始值按当前策略预填,区间等参数由人调整
|
||||
function runBacktest(): void {
|
||||
async function runBacktest(): Promise<void> {
|
||||
if (!active.value) return
|
||||
if (dirty.value) {
|
||||
ElMessage.warning('有未保存修改,请先保存再跳转回测')
|
||||
return
|
||||
// 未保存不再死拦:确认「保存并跳」/「直接跳」/取消
|
||||
try {
|
||||
await ElMessageBox.confirm('有未保存修改。保存后跳转,或直接跳转(不保存)?', '运行回测', {
|
||||
confirmButtonText: '保存并跳转',
|
||||
distinguishCancelAndClose: true,
|
||||
cancelButtonText: '直接跳转',
|
||||
})
|
||||
await onSave()
|
||||
} catch (action) {
|
||||
if (action === 'close') return // 右上角 X = 取消整个跳转
|
||||
}
|
||||
}
|
||||
if (active.value.type === 'portfolio') {
|
||||
router.push({ path: '/backtest/portfolio', query: { strategy: active.value.name.replace(/\.py$/, '') } })
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
// 行级代码快照查看(§12.6 补):模拟盘/实盘列表行「快照」按钮 → 该账户发起时的代码 vs 当前。
|
||||
import { ref, onMounted } from 'vue'
|
||||
import MonacoDiff from '@/components/MonacoDiff.vue'
|
||||
import { apiClient } from '@/api/client'
|
||||
import { getCodeVersion } from '@/api/strategy'
|
||||
|
||||
const props = defineProps<{
|
||||
instanceId: number
|
||||
codeHash: string // 32位 md5(账户 code_hash 列)
|
||||
label: string
|
||||
}>()
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
|
||||
const loading = ref(false)
|
||||
const snapCode = ref<string | null>(null)
|
||||
const curCode = ref<string | null>(null)
|
||||
const diffReady = ref(false)
|
||||
const err = ref('')
|
||||
const codeFile = ref('')
|
||||
|
||||
async function load(): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data: ir } = await apiClient.get<{ instance: { code_file: string } }>(`/strategy/instances/${props.instanceId}`)
|
||||
codeFile.value = ir.instance?.code_file || ''
|
||||
if (!codeFile.value) throw new Error('no code_file')
|
||||
const [snap, cur] = await Promise.all([
|
||||
getCodeVersion(codeFile.value, props.codeHash.slice(0, 8)),
|
||||
apiClient.get<{ code: string }>(`/strategy/file/${codeFile.value}`),
|
||||
])
|
||||
snapCode.value = snap
|
||||
curCode.value = cur.data.code
|
||||
// 等抽屉动画展开后容器全宽再挂 Monaco(diff 分栏测量问题同 Code.vue 版本历史)
|
||||
setTimeout(() => { diffReady.value = true }, 380)
|
||||
} catch {
|
||||
err.value = '快照不存在(该账户发起于版本快照上线前,或档案未关联代码文件)'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-drawer :model-value="true" size="860px" :with-header="false" append-to-body destroy-on-close @close="emit('close')">
|
||||
<div v-loading="loading" class="snap">
|
||||
<div class="snap-head">
|
||||
<div>
|
||||
<div style="font-size:15px;font-weight:700;color:var(--text)">代码快照 · {{ label }}</div>
|
||||
<div class="mono muted" style="font-size:11px;margin-top:2px">
|
||||
{{ codeFile || '…' }} · 发起版本 v{{ codeHash.slice(0, 8) }}
|
||||
</div>
|
||||
</div>
|
||||
<button class="term-btn sm" @click="emit('close')">关闭</button>
|
||||
</div>
|
||||
<div v-if="err" class="muted" style="padding:24px 0;text-align:center;font-size:12.5px">{{ err }}</div>
|
||||
<template v-else-if="diffReady && snapCode != null && curCode != null">
|
||||
<div class="muted mono" style="font-size:11px;margin:6px 0">左=发起时快照 · 右=当前代码 · 滚动联动</div>
|
||||
<div class="snap-diff">
|
||||
<MonacoDiff :original="snapCode" :modified="curCode" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.snap { display: flex; flex-direction: column; gap: 8px; padding: 4px 2px; }
|
||||
.snap-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
|
||||
.snap-diff { height: 520px; border: 1px solid var(--border-2); border-radius: var(--r-sm); overflow: hidden; }
|
||||
</style>
|
||||
@@ -145,16 +145,36 @@ function newInstance(): void {
|
||||
function editInstance(i: Instance): void {
|
||||
router.push(`/strategy/instance/${i.id}`)
|
||||
}
|
||||
// 以文件实际类型为准(老实例 type 字段可能不准,如组合档案标了 cta)
|
||||
function fileTypeOf(i: Instance): 'portfolio' | 'cta' {
|
||||
return files.value.find((f) => f.name === i.code_file)?.type || i.type
|
||||
}
|
||||
function runBacktest(i: Instance): void {
|
||||
const isPf = fileTypeOf(i) === 'portfolio'
|
||||
router.push({
|
||||
path: i.type === 'portfolio' ? '/backtest/portfolio' : '/backtest/new',
|
||||
query: { class: i.code_file, instance: String(i.id) },
|
||||
path: isPf ? '/backtest/portfolio' : '/backtest/new',
|
||||
// 组合页吃 strategy(文件名去.py);CTA 页吃 class(类名)
|
||||
query: isPf
|
||||
? { strategy: i.code_file.replace(/\.py$/, ''), instance: String(i.id) }
|
||||
: {
|
||||
class: files.value.find((f) => f.name === i.code_file)?.class_name || i.code_file,
|
||||
instance: String(i.id),
|
||||
},
|
||||
})
|
||||
}
|
||||
// 运行灯语义:有在跑账户 → 看运行(列表按档案过滤);没有 → 发起(创建页带档案)
|
||||
function runPaper(i: Instance, mode: 'replay' | 'live' | 'shadow'): void {
|
||||
if (mode !== 'replay' && (i.running_accounts || []).some((a) => a.kind === 'paper' || a.kind === 'shadow')) {
|
||||
router.push(`/paper?instance=${i.id}`)
|
||||
return
|
||||
}
|
||||
router.push({ path: '/paper/new', query: { mode, instance: String(i.id) } })
|
||||
}
|
||||
function runLive(i: Instance): void {
|
||||
if ((i.running_accounts || []).some((a) => a.kind === 'live')) {
|
||||
router.push(`/live?instance=${i.id}`)
|
||||
return
|
||||
}
|
||||
router.push({ path: '/live/new', query: { instance: String(i.id) } })
|
||||
}
|
||||
async function onSync(i: Instance): Promise<void> {
|
||||
@@ -211,7 +231,6 @@ const runningRows = computed(() =>
|
||||
<p class="page-subtitle">代码 → 实例档案 → 回测 / 模拟 / 实盘 · 全生命周期</p>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="term-btn" @click="editCode(selectedFile)">编辑代码</button>
|
||||
<button class="term-btn primary" @click="router.push('/strategy/new')">+ 新建策略</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -297,7 +316,7 @@ const runningRows = computed(() =>
|
||||
<div class="ret mono" :class="retClass(latestRet(i))">{{ pct(latestRet(i)) }}<span class="sub">{{ retSub(i) }}</span></div>
|
||||
<div class="runs">
|
||||
<button class="run-btn" @click="runBacktest(i)"><span class="lamp" :class="lampOf(i.status.backtest)"></span>回测</button>
|
||||
<button class="run-btn" @click="runPaper(i, 'replay')"><span class="lamp" :class="lampOf(i.status.replay)"></span>回放</button>
|
||||
<button v-if="fileTypeOf(i) !== 'portfolio'" class="run-btn" @click="runPaper(i, 'replay')"><span class="lamp" :class="lampOf(i.status.replay)"></span>回放</button>
|
||||
<button class="run-btn" :class="{ active: (i.running_accounts || []).some(a => a.kind === 'paper' || a.kind === 'shadow') }" @click="runPaper(i, 'live')">
|
||||
<span class="lamp" :class="lampOf(i.status.paper_live)"></span>{{ runLabel(i, 'paper_live') }}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user