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]
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -63,7 +63,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
const form = ref<LiveCreateRequest>({
|
||||
name: 'live-600000',
|
||||
name: '', // #82 空=未选实例时预览显示占位提示;选实例后 autoName 生成(服务端兜底双保险)
|
||||
account: '66639661',
|
||||
vt_symbol: '600000',
|
||||
strategy_class: 'AShareDoubleMaStrategy',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user