fix(web): 生成tears按钮任务状态化防重复提交(用户08-30补充) [nas]
CI/CD / test (push) Successful in 5s
CI/CD / nas-deploy (push) Successful in 12s
CI/CD / nas-verify (push) Successful in 7s

痛点:点击生成→progress→返回详情页按钮仍可点,重复生成同类任务。
方案:每因子记task_id(localStorage factorTearsTask.v1),按钮三态——
无任务「生成tears报告」/进行中「分析进行中·查看进度→」(跳progress复用)/
已完成「✓查看tears报告→」(跳result复用);failed或tears缺失(修复前旧
失败任务getTearsData探测404)自动清态恢复可重新生成;任务404(已清理)同。
mounted/watch factor刷新态;build绿
This commit is contained in:
2026-08-30 10:48:45 +08:00
parent ad2270821c
commit 51c400030b
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { ref, onMounted, watch, nextTick, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getEvalDetail, submitFactor, type EvalDetail } from '@/api/factor'
import { getEvalDetail, submitFactor, getTearsData, type EvalDetail } from '@/api/factor'
import { getStatus } from '@/api/backtest'
import { TEARS_SAMPLE_CODES } from '@/constants/factorSamples'
import { useChart } from '@/composables/useChart'
import { darkTitle, darkTooltip, darkGrid, darkAxis, BRAND } from '@/utils/echartsDark'
@@ -51,13 +52,92 @@ function toggleCompare() {
// —— 一键生成 tears(方案A):排行榜数据没有分层序列,tears 要跑一次 alphalens 分析
// (固定 30 只跨行业样本 × 近 2.5 年,分钟级),跑完结果页即新版 ECharts tears。
// 任务状态化(用户 08-30 补充):每因子记住 task_id(localStorage),
// 进行中→「查看进度」/已完成→「查看报告」复用任务,不再重复提交;
// failed 或 tears 缺失(修复前旧任务)自动清态可重新生成。
const TEARS_SAMPLE_SPAN = { start: '2024-01-01', end: '2026-06-30' }
const TEARS_TASK_KEY = 'factorTearsTask.v1'
const tearsSubmitting = ref(false)
const tearsTaskId = ref('')
const tearsBtn = ref<'none' | 'running' | 'view'>('none')
function tearsTaskMap(): Record<string, string> {
try {
return JSON.parse(localStorage.getItem(TEARS_TASK_KEY) || '{}')
} catch {
return {}
}
}
function saveTearsTask(tid: string): void {
if (!detail.value) return
const map = tearsTaskMap()
map[detail.value.factor] = tid
try {
localStorage.setItem(TEARS_TASK_KEY, JSON.stringify(map))
} catch {
/* 隐私模式等:不可持久化时仅本页防重 */
}
}
function forgetTearsTask(): void {
if (!detail.value) return
const map = tearsTaskMap()
delete map[detail.value.factor]
try {
localStorage.setItem(TEARS_TASK_KEY, JSON.stringify(map))
} catch {
/* ignore */
}
tearsTaskId.value = ''
}
async function refreshTearsBtn(): Promise<void> {
const f = detail.value?.factor
if (!f) return
const tid = tearsTaskMap()[f]
if (!tid) {
tearsTaskId.value = ''
tearsBtn.value = 'none'
return
}
try {
const s = await getStatus(tid)
if (s.status === 'running' || s.status === 'pending') {
tearsTaskId.value = tid
tearsBtn.value = 'running'
return
}
if (s.status === 'done') {
tearsTaskId.value = tid
tearsBtn.value = 'view'
// done 但 tears 缺失(修复前的失败任务)→ 恢复可重新生成
try {
await getTearsData(tid, f)
} catch {
forgetTearsTask()
tearsBtn.value = 'none'
}
return
}
forgetTearsTask() // failed → 可重跑
} catch {
forgetTearsTask() // 任务不存在(已清理)→ 可重跑
}
}
async function generateTears() {
if (!detail.value || tearsSubmitting.value) return
if (tearsBtn.value === 'running') {
router.push(`/factor/progress/${tearsTaskId.value}`)
return
}
if (tearsBtn.value === 'view') {
router.push(`/factor/result/${tearsTaskId.value}`)
return
}
tearsSubmitting.value = true
try {
const tid = await submitFactor({
@@ -66,6 +146,7 @@ async function generateTears() {
start: TEARS_SAMPLE_SPAN.start,
end: TEARS_SAMPLE_SPAN.end,
})
saveTearsTask(tid)
ElMessage.success('tears 分析已发起(30 只样本 · 约 1~3 分钟)')
router.push(`/factor/progress/${tid}`)
} catch {
@@ -190,7 +271,15 @@ function toggleExpr() {
expandedExpr.value = !expandedExpr.value
}
onMounted(load)
onMounted(async () => {
await load()
void refreshTearsBtn()
})
watch(
() => detail.value?.factor,
() => { void refreshTearsBtn() },
)
watch(
() => [detail.value, period.value],
@@ -343,7 +432,10 @@ function pct(val: number | null | undefined): string {
<!-- Actions -->
<div class="actions">
<button class="btn primary" :disabled="tearsSubmitting" @click="generateTears">
{{ tearsSubmitting ? '发起中' : '生成 tears 报告' }}
{{ tearsSubmitting ? '发起中'
: tearsBtn === 'running' ? '分析进行中 · 查看进度 '
: tearsBtn === 'view' ? ' 查看 tears 报告 '
: '生成 tears 报告' }}
</button>
<button class="btn" @click="goToNewFactor">自选样本分析</button>
<button class="btn" :class="{ chosen: inCompare }" @click="toggleCompare">