158 lines
6.8 KiB
Vue
158 lines
6.8 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 因子分析提交页(终端风重构,方案B 设计稿 v2 拍板):
|
||
* 自选因子 × 自选标的池 × 自选区间的自由实验台。
|
||
* 要素与旧版零变化:query.factor 预填 / ≥1 因子 + ≥2 标的校验 / 混合分隔粘贴。
|
||
*/
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import { getFactors, submitFactor, type FactorItem } from '@/api/factor'
|
||
import { TEARS_SAMPLE_CODES } from '@/constants/factorSamples'
|
||
import FactorPicker from './FactorPicker.vue'
|
||
import UniversePicker from './UniversePicker.vue'
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
const factors = ref<FactorItem[]>([])
|
||
const loading = ref(false)
|
||
const submitting = ref(false)
|
||
const pickerRef = ref<InstanceType<typeof FactorPicker> | null>(null)
|
||
const today = new Date().toISOString().slice(0, 10)
|
||
|
||
const form = ref({
|
||
factor_names: [] as string[],
|
||
symbols: [...TEARS_SAMPLE_CODES.slice(0, 8)],
|
||
start: '2024-01-02',
|
||
end: '2026-06-30',
|
||
})
|
||
|
||
const spanDays = computed(() => {
|
||
const a = new Date(form.value.start).getTime()
|
||
const b = new Date(form.value.end).getTime()
|
||
if (!Number.isFinite(a) || !Number.isFinite(b)) return null
|
||
return Math.round((b - a) / 864e5)
|
||
})
|
||
|
||
const valid = computed(() => form.value.factor_names.length >= 1 && form.value.symbols.length >= 2)
|
||
const validNote = computed(() => {
|
||
if (!form.value.factor_names.length) return '待办:至少选 1 个因子'
|
||
if (form.value.symbols.length < 2) return '待办:标的池 ≥2 只'
|
||
return ''
|
||
})
|
||
|
||
onMounted(async () => {
|
||
loading.value = true
|
||
try {
|
||
factors.value = await getFactors()
|
||
// 排行榜详情「自选样本分析」跳入时预填该因子
|
||
const qf = route.query.factor as string | undefined
|
||
if (qf && factors.value.some((f) => f.name === qf)) {
|
||
form.value.factor_names = [qf]
|
||
}
|
||
pickerRef.value?.hydrate()
|
||
} catch {
|
||
ElMessage.error('因子列表加载失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
})
|
||
|
||
async function onSubmit(): Promise<void> {
|
||
if (!valid.value) return
|
||
submitting.value = true
|
||
try {
|
||
const tid = await submitFactor({
|
||
symbols: form.value.symbols,
|
||
factor_names: form.value.factor_names,
|
||
start: form.value.start,
|
||
end: form.value.end,
|
||
})
|
||
ElMessage.success('因子分析已提交')
|
||
router.push(`/factor/progress/${tid}`)
|
||
} catch {
|
||
ElMessage.error('提交失败')
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div v-loading="loading" class="page factor-new">
|
||
<div class="page-head">
|
||
<div>
|
||
<h2 class="page-title">因子分析</h2>
|
||
<p class="page-sub">IC 横截面 · 分层 tears 报告 · 自由实验台(换池 / 分年 / 多因子并排归这里)</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pane">
|
||
<div class="ph"><span class="pt">因子选择</span><span class="tag">FACTOR PICKER · 支持多选</span></div>
|
||
<div class="pb">
|
||
<FactorPicker ref="pickerRef" v-model="form.factor_names" :factors="factors" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pane">
|
||
<div class="ph"><span class="pt">标的池与区间</span><span class="tag">UNIVERSE & SPAN</span></div>
|
||
<div class="pb">
|
||
<div class="formrow">
|
||
<div class="flab">标的池</div>
|
||
<div class="fbody">
|
||
<UniversePicker v-model="form.symbols" />
|
||
</div>
|
||
</div>
|
||
<div class="formrow gap-top">
|
||
<div class="flab">分析区间</div>
|
||
<div class="fbody">
|
||
<div class="dates">
|
||
<input v-model="form.start" type="date" :max="form.end">
|
||
<span class="sep">→</span>
|
||
<input v-model="form.end" type="date" :max="today">
|
||
<span v-if="spanDays != null && spanDays >= 0" class="span-note">跨度 {{ spanDays }} 天 ≈ {{ (spanDays / 365).toFixed(1) }} 年</span>
|
||
</div>
|
||
<p class="span-hint">区间前半作因子计算预热,后半(test 段)进入 IC 与分层统计</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="submit-bar">
|
||
<button type="button" class="btn primary" :disabled="!valid || submitting" @click="onSubmit">
|
||
{{ submitting ? '提交中…' : '提交分析 →' }}
|
||
</button>
|
||
<span v-if="validNote" class="valid-note">{{ validNote }}</span>
|
||
<span class="hint">提交后进入进度页,完成自动跳转结果页</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.factor-new { display: flex; flex-direction: column; gap: 14px; }
|
||
.pane { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--r-lg); min-width: 0; }
|
||
.pane .ph { display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--border-2); flex-wrap: wrap; }
|
||
.pane .pt { font-size: 12.5px; font-weight: 700; letter-spacing: .06em; }
|
||
.pane .tag { font-family: var(--mono); font-size: 9.5px; color: var(--text-3); letter-spacing: .1em; }
|
||
.pane .pb { padding: 14px; }
|
||
.formrow { display: flex; align-items: flex-start; gap: 14px; }
|
||
.formrow.gap-top { margin-top: 14px; }
|
||
.flab { width: 88px; flex-shrink: 0; padding-top: 7px; text-align: right; color: var(--text-2); font-size: 12.5px; }
|
||
.fbody { flex: 1; min-width: 0; }
|
||
.dates { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||
.dates input[type='date'] { background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-sm); color: var(--text); font-family: var(--mono); font-size: 12px; padding: 6px 10px; color-scheme: dark; }
|
||
.dates input[type='date']:focus { outline: 1px solid rgba(0,229,255,.5); }
|
||
.sep { color: var(--text-3); font-family: var(--mono); font-size: 11px; }
|
||
.span-note { font-family: var(--mono); font-size: 10.5px; color: var(--text-3); }
|
||
.span-hint { font-family: var(--mono); font-size: 10.5px; color: var(--text-3); margin: 8px 0 0; }
|
||
.submit-bar { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 2px 0; }
|
||
.btn { font-size: 12.5px; font-weight: 600; border-radius: var(--r-md); padding: 8px 18px; border: 1px solid var(--border); color: var(--text-2); background: var(--bg-card); transition: all .15s ease; cursor: pointer; }
|
||
.btn:hover { color: var(--text); border-color: var(--text-3); }
|
||
/* 对齐排行榜「发起新评估」同款(青软底+荧光青字,el-button--primary 观感) */
|
||
.btn.primary { color: var(--brand); background: var(--cyan-soft); border-color: rgba(0,229,255,.5); font-weight: 600; letter-spacing: .3px; }
|
||
.btn.primary:hover:not(:disabled) { color: var(--brand); background: rgba(0,229,255,.22); border-color: rgba(0,229,255,.65); }
|
||
.btn:disabled { opacity: .45; cursor: not-allowed; box-shadow: none; }
|
||
.valid-note { font-family: var(--mono); font-size: 11px; color: var(--amber); }
|
||
.hint { font-family: var(--mono); font-size: 10.5px; color: var(--text-3); }
|
||
</style>
|