feat(frontend): #80模拟盘命名对齐实盘+#81批量删除+#82实盘预览名清空——#80:autoName改{实例名}_v{YYYYMMDD}{minor}同实例同日递增不分模式(原{实例名}·{模式}v{N}连排+英文模式段,用户定稿与实盘同格式);#81:模拟盘/实盘列表加多选列(仅非运行行可选=运行中先停止)+批量删除按钮(选中才显示带数量徽标,确认框列账户名前5,循环既有DELETE接口,N成功/M失败汇总+失败明细toast);#82:新建实盘初始name live-600000改空串,未选实例时预览显示占位「—(选实例后自动生成)」(原默认值顶掉占位提示),选实例后autoName生成,服务端兜底(#78)双保险;build绿(vue-tsc) [nas]
CI/CD / test (push) Successful in 12s
CI/CD / nas-deploy (push) Successful in 34s
CI/CD / nas-verify (push) Successful in 14s

This commit is contained in:
2026-08-16 13:55:49 +08:00
parent 28374c341b
commit bfd42f70ae
4 changed files with 97 additions and 12 deletions
+43 -1
View File
@@ -102,6 +102,44 @@ async function onDelete(a: LiveAccount): Promise<void> {
}
}
// #81 批量删除:多选仅可选非运行行(运行中先停止),循环既有 DELETE 接口
const selection = ref<LiveAccount[]>([])
function onSelChange(rows: LiveAccount[]): void {
selection.value = rows
}
const canBatch = (row: LiveAccount): boolean => row.status !== 'running'
const batchLoading = ref(false)
async function onBatchDelete(): Promise<void> {
const rows = selection.value
if (!rows.length) return
const names = rows.map((r) => `#${r.id} ${r.name}`).slice(0, 5).join('、')
+ (rows.length > 5 ? `${rows.length}` : '')
try {
await ElMessageBox.confirm(
`删除 ${rows.length} 个实盘实例:${names}?成交/持仓/余额数据将一并删除,不可恢复。`,
'批量删除确认',
{ type: 'warning' },
)
} catch {
return
}
batchLoading.value = true
let ok = 0
const failed: string[] = []
for (const r of rows) {
try {
await deleteLive(r.id)
ok++
} catch (e: unknown) {
failed.push(`#${r.id}(${e instanceof Error ? e.message : '失败'})`)
}
}
batchLoading.value = false
if (failed.length) ElMessage.warning(`已删 ${ok} 个;失败 ${failed.length} 个:${failed.join('、')}`)
else ElMessage.success(`已删除 ${ok} 个实盘实例`)
await refreshSilent()
}
// 编辑(仅 stopped 可改;后端运行中会 400)
// 代码快照(§12.6 补:查看该账户发起时的代码 vs 当前)
const snapshotRow = ref<{ instanceId: number; codeHash: string; label: string } | null>(null)
@@ -197,10 +235,14 @@ async function onStop(a: LiveAccount): Promise<void> {
<el-option label="已停止" value="stopped" />
</el-select>
<el-button link type="primary" @click="refresh">刷新</el-button>
<el-button v-if="selection.length" link type="danger" :loading="batchLoading" @click="onBatchDelete">
批量删除({{ selection.length }})
</el-button>
</div>
<el-card class="table-card" shadow="never">
<el-table :data="filtered" size="small" empty-text="暂无实盘实例">
<el-table :data="filtered" size="small" empty-text="暂无实盘实例" @selection-change="onSelChange">
<el-table-column type="selection" width="42" :selectable="canBatch" />
<el-table-column label="名称 / ID" min-width="160">
<template #default="{ row }">
<div class="cell-name">
+1 -1
View File
@@ -63,7 +63,7 @@ onMounted(async () => {
})
const form = ref<LiveCreateRequest>({
name: 'live-600000',
name: '', // #82 空=未选实例时预览显示占位提示;选实例后 autoName 生成(服务端兜底双保险)
account: '66639661',
vt_symbol: '600000',
strategy_class: 'AShareDoubleMaStrategy',
+43 -1
View File
@@ -141,6 +141,44 @@ async function onDelete(a: PaperAccount): Promise<void> {
}
}
// #81 批量删除:多选仅可选非运行行(运行中先停止),循环既有 DELETE 接口
const selection = ref<PaperAccount[]>([])
function onSelChange(rows: PaperAccount[]): void {
selection.value = rows
}
const canBatch = (row: PaperAccount): boolean => row.status !== 'running'
const batchLoading = ref(false)
async function onBatchDelete(): Promise<void> {
const rows = selection.value
if (!rows.length) return
const names = rows.map((r) => `#${r.id} ${r.name}`).slice(0, 5).join('、')
+ (rows.length > 5 ? `${rows.length}` : '')
try {
await ElMessageBox.confirm(
`删除 ${rows.length} 个模拟盘:${names}?净值/成交/持仓数据将一并删除,不可恢复。`,
'批量删除确认',
{ type: 'warning' },
)
} catch {
return
}
batchLoading.value = true
let ok = 0
const failed: string[] = []
for (const r of rows) {
try {
await deletePaper(r.id)
ok++
} catch (e: unknown) {
failed.push(`#${r.id}(${e instanceof Error ? e.message : '失败'})`)
}
}
batchLoading.value = false
if (failed.length) ElMessage.warning(`已删 ${ok} 个;失败 ${failed.length} 个:${failed.join('、')}`)
else ElMessage.success(`已删除 ${ok} 个模拟盘`)
await refresh()
}
// 代码快照(§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 {
@@ -233,10 +271,14 @@ async function saveEdit(): Promise<void> {
<el-option label="失败" value="failed" />
</el-select>
<el-button link type="primary" @click="refresh">刷新</el-button>
<el-button v-if="selection.length" link type="danger" :loading="batchLoading" @click="onBatchDelete">
批量删除({{ selection.length }})
</el-button>
</div>
<el-card class="table-card" shadow="never">
<el-table :data="filtered" size="small" empty-text="暂无模拟盘">
<el-table :data="filtered" size="small" empty-text="暂无模拟盘" @selection-change="onSelChange">
<el-table-column type="selection" width="42" :selectable="canBatch" />
<el-table-column label="名称 / ID" min-width="160">
<template #default="{ row }">
<div class="cell-name">
+10 -9
View File
@@ -112,8 +112,8 @@ async function applyInstance(instId: number | string): Promise<void> {
])
const inst = ir.instance
fromInstance.value = inst.name || String(instId)
// #72 名称自动跟随实例 + 版本号(同实例同模式递增,对齐实盘命名)
form.value.name = await autoName(inst.name || String(instId), Number(instId), form.value.mode)
// #72 名称自动跟随实例;#80 格式对齐实盘:{实例名}_v{YYYYMMDD}{minor}
form.value.name = await autoName(inst.name || String(instId), Number(instId))
const file = fr.files.find((f) => f.name === inst.code_file)
const cls = file?.class_name || inst.code_file
const sym = inst.symbol_or_pool || '600000'
@@ -139,19 +139,20 @@ function onPickInstance(id: number | null): void {
if (id != null) applyInstance(id)
}
// 模拟盘名版本号:{实例名}·{模式}v{N}(N=同实例同模式已有账户数
async function autoName(instName: string, instId: number, mode: string): Promise<string> {
const prefix = `${instName}·${mode}v`
let n = 0
// #80 模拟盘名对齐实盘格式:{实例名}_v{YYYYMMDD}{minor}(同实例同日递增,不分模式
async function autoName(instName: string, instId: number): Promise<string> {
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '')
const prefix = `${instName}_v${today}`
let minor = 0
try {
const { listPapers } = await import('@/api/paper')
const accs = await listPapers()
n = accs.filter((a: { instance_id?: number | null; name?: string }) =>
minor = accs.filter((a: { instance_id?: number | null; name?: string }) =>
a.instance_id === instId && (a.name || '').startsWith(prefix)).length
} catch {
/* 列表拉不到 → v0 */
/* 列表拉不到 → minor=0 */
}
return `${prefix}${n}`
return `${prefix}${minor}`
}
const isPortfolio = computed(() => strategyType.value === 'portfolio')