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)
|
// 编辑(仅 stopped 可改;后端运行中会 400)
|
||||||
// 代码快照(§12.6 补:查看该账户发起时的代码 vs 当前)
|
// 代码快照(§12.6 补:查看该账户发起时的代码 vs 当前)
|
||||||
const snapshotRow = ref<{ instanceId: number; codeHash: string; label: string } | null>(null)
|
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-option label="已停止" value="stopped" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-button link type="primary" @click="refresh">刷新</el-button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<el-card class="table-card" shadow="never">
|
<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">
|
<el-table-column label="名称 / ID" min-width="160">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="cell-name">
|
<div class="cell-name">
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const form = ref<LiveCreateRequest>({
|
const form = ref<LiveCreateRequest>({
|
||||||
name: 'live-600000',
|
name: '', // #82 空=未选实例时预览显示占位提示;选实例后 autoName 生成(服务端兜底双保险)
|
||||||
account: '66639661',
|
account: '66639661',
|
||||||
vt_symbol: '600000',
|
vt_symbol: '600000',
|
||||||
strategy_class: 'AShareDoubleMaStrategy',
|
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 当前)
|
// 代码快照(§12.6 补:查看该账户发起时的代码 vs 当前)
|
||||||
const snapshotRow = ref<{ instanceId: number; codeHash: string; label: string } | null>(null)
|
const snapshotRow = ref<{ instanceId: number; codeHash: string; label: string } | null>(null)
|
||||||
function openSnapshot(row: PaperAccount & { code_hash?: string; instance_id?: number }): void {
|
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-option label="失败" value="failed" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-button link type="primary" @click="refresh">刷新</el-button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<el-card class="table-card" shadow="never">
|
<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">
|
<el-table-column label="名称 / ID" min-width="160">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="cell-name">
|
<div class="cell-name">
|
||||||
|
|||||||
@@ -112,8 +112,8 @@ async function applyInstance(instId: number | string): Promise<void> {
|
|||||||
])
|
])
|
||||||
const inst = ir.instance
|
const inst = ir.instance
|
||||||
fromInstance.value = inst.name || String(instId)
|
fromInstance.value = inst.name || String(instId)
|
||||||
// #72 名称自动跟随实例 + 版本号(同实例同模式递增,对齐实盘命名)
|
// #72 名称自动跟随实例;#80 格式对齐实盘:{实例名}_v{YYYYMMDD}{minor}
|
||||||
form.value.name = await autoName(inst.name || String(instId), Number(instId), form.value.mode)
|
form.value.name = await autoName(inst.name || String(instId), Number(instId))
|
||||||
const file = fr.files.find((f) => f.name === inst.code_file)
|
const file = fr.files.find((f) => f.name === inst.code_file)
|
||||||
const cls = file?.class_name || inst.code_file
|
const cls = file?.class_name || inst.code_file
|
||||||
const sym = inst.symbol_or_pool || '600000'
|
const sym = inst.symbol_or_pool || '600000'
|
||||||
@@ -139,19 +139,20 @@ function onPickInstance(id: number | null): void {
|
|||||||
if (id != null) applyInstance(id)
|
if (id != null) applyInstance(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟盘名版本号:{实例名}·{模式}v{N}(N=同实例同模式已有账户数)
|
// #80 模拟盘名对齐实盘格式:{实例名}_v{YYYYMMDD}{minor}(同实例同日递增,不分模式)
|
||||||
async function autoName(instName: string, instId: number, mode: string): Promise<string> {
|
async function autoName(instName: string, instId: number): Promise<string> {
|
||||||
const prefix = `${instName}·${mode}v`
|
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '')
|
||||||
let n = 0
|
const prefix = `${instName}_v${today}`
|
||||||
|
let minor = 0
|
||||||
try {
|
try {
|
||||||
const { listPapers } = await import('@/api/paper')
|
const { listPapers } = await import('@/api/paper')
|
||||||
const accs = await listPapers()
|
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
|
a.instance_id === instId && (a.name || '').startsWith(prefix)).length
|
||||||
} catch {
|
} catch {
|
||||||
/* 列表拉不到 → v0 */
|
/* 列表拉不到 → minor=0 */
|
||||||
}
|
}
|
||||||
return `${prefix}${n}`
|
return `${prefix}${minor}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPortfolio = computed(() => strategyType.value === 'portfolio')
|
const isPortfolio = computed(() => strategyType.value === 'portfolio')
|
||||||
|
|||||||
Reference in New Issue
Block a user