You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

623 lines
20 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="detect-missing">
<el-card>
<el-form :model="form" label-width="100px">
<el-form-item label="证券类型">
<el-radio-group v-model="form.securityType">
<el-radio-button label="stock">股票</el-radio-button>
<el-radio-button label="future">期货</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="合约类型" v-if="form.securityType === 'future'">
<el-radio-group v-model="form.contractType">
<el-radio-button label="all">所有合约</el-radio-button>
<el-radio-button label="main">主力合约</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="周期">
<el-select v-model="form.periodType" style="width: 120px;">
<el-option label="日线" value="daily" />
<el-option label="1分钟" value="min1" />
<el-option label="5分钟" value="min5" />
</el-select>
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker
v-model="form.startDate"
type="date"
placeholder="开始日期"
value-format="YYYYMMDD"
/>
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker
v-model="form.endDate"
type="date"
placeholder="结束日期"
value-format="YYYYMMDD"
/>
</el-form-item>
<el-form-item>
<el-button type="warning" @click="handleDetectAll" :loading="detectingAll">
<el-icon><Search /></el-icon> 一键检测所有数据
</el-button>
<el-button type="success" @click="handleCacheAll" :loading="cachingAll">
<el-icon><Download /></el-icon> 一键缓存所有数据
</el-button>
</el-form-item>
</el-form>
<el-divider content-position="left">批量检测(指定代码)</el-divider>
<el-form :model="form" label-width="100px">
<el-form-item label="代码列表">
<el-input
v-model="codeInput"
type="textarea"
:rows="4"
placeholder="输入代码,每行一个或逗号分隔"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleDetect" :loading="detecting">
<el-icon><Search /></el-icon> 检测缺失数据
</el-button>
<el-button type="success" @click="handleCache" :loading="caching" :disabled="!hasMissing">
<el-icon><Download /></el-icon> 一键缓存
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 实时进度显示 -->
<el-card class="progress-card" v-if="wsProgress.status">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>实时进度</span>
<el-tag :type="getProgressTagType(wsProgress.status)">
{{ wsProgress.status === 'starting' ? '启动中' :
wsProgress.status === 'running' ? '进行中' :
wsProgress.status === 'completed' ? '已完成' : '失败' }}
</el-tag>
</div>
</template>
<el-progress
:percentage="wsProgress.progress || 0"
:status="getProgressStatus(wsProgress.status)"
:stroke-width="20"
:text-inside="true"
/>
<el-descriptions :column="4" border style="margin-top: 15px;">
<el-descriptions-item label="当前状态">{{ wsProgress.message || '-' }}</el-descriptions-item>
<el-descriptions-item label="总数">{{ wsProgress.total_count || 0 }}</el-descriptions-item>
<el-descriptions-item label="已处理">{{ wsProgress.processed || 0 }}</el-descriptions-item>
<el-descriptions-item label="缺失">{{ wsProgress.missing || 0 }}</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 检测结果汇总 -->
<el-card class="summary-card" v-if="detectResult">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>检测结果汇总</span>
<el-button
type="success"
@click="handleFillMissing"
:loading="fillingMissing"
:disabled="!detectResult.missing_count || detectResult.missing_count === 0"
>
<el-icon><Download /></el-icon> 一键补齐缺失数据
</el-button>
</div>
</template>
<el-row :gutter="20">
<el-col :span="6">
<el-statistic title="检测总数" :value="detectResult.total_count || 0" />
</el-col>
<el-col :span="6">
<el-statistic title="数据完整" :value="detectResult.complete_count || 0" suffix="个">
<template #suffix>
<el-tag type="success" size="small">完整</el-tag>
</template>
</el-statistic>
</el-col>
<el-col :span="6">
<el-statistic title="数据缺失" :value="detectResult.missing_count || 0" suffix="个">
<template #suffix>
<el-tag type="warning" size="small">缺失</el-tag>
</template>
</el-statistic>
</el-col>
<el-col :span="6">
<el-statistic title="检测错误" :value="detectResult.error_count || 0" suffix="个">
<template #suffix>
<el-tag type="danger" size="small">错误</el-tag>
</template>
</el-statistic>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="6">
<el-statistic title="检测周期" :value="detectResult.expected_days || 0" suffix="天" />
</el-col>
<el-col :span="6">
<el-statistic title="开始日期" :value="detectResult.start_date || '-'" />
</el-col>
<el-col :span="6">
<el-statistic title="结束日期" :value="detectResult.end_date || '-'" />
</el-col>
<el-col :span="6">
<el-statistic title="检测状态">
<el-tag :type="getStatusType(detectResult.status)">
{{ detectResult.status || '-' }}
</el-tag>
</el-statistic>
</el-col>
</el-row>
</el-card>
<!-- 缓存进度 -->
<el-card class="progress-card" v-if="cacheTask">
<template #header>
<span>缓存进度</span>
</template>
<el-progress :percentage="Math.round(cacheTask.progress || 0)" :status="getProgressStatus(cacheTask.status)" />
<el-descriptions :column="4" border style="margin-top: 15px;">
<el-descriptions-item label="任务名称">{{ cacheTask.task_name }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusType(cacheTask.status)">{{ cacheTask.status }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="总数">{{ cacheTask.total_count }}</el-descriptions-item>
<el-descriptions-item label="已处理">{{ cacheTask.success_count || 0 }}</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 每日数据统计 -->
<el-card class="daily-card" v-if="detectResult && detectResult.daily_stats">
<template #header>
<span>每日数据统计</span>
</template>
<el-table :data="dailyStatsList" stripe height="300">
<el-table-column prop="date" label="日期" width="120" />
<el-table-column prop="expected" label="预期数量" width="100" />
<el-table-column prop="actual" label="实际数量" width="100" />
<el-table-column prop="missing" label="缺失数量" width="100">
<template #default="{ row }">
<el-tag :type="row.missing > 0 ? 'warning' : 'success'" size="small">
{{ row.missing }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="完整率">
<template #default="{ row }">
<el-progress
:percentage="Math.round((row.actual / row.expected) * 100)"
:status="row.missing > row.expected * 0.5 ? 'exception' : row.missing > 0 ? 'warning' : 'success'"
/>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 缺失代码列表 -->
<el-card class="missing-card" v-if="detectResult && detectResult.missing_codes && detectResult.missing_codes.length > 0">
<template #header>
<span>缺失代码列表前100个</span>
</template>
<el-table :data="detectResult.missing_codes" stripe height="300">
<el-table-column prop="code" label="代码" width="120" />
<el-table-column prop="expected_count" label="预期天数" width="100" />
<el-table-column prop="actual_count" label="实际天数" width="100" />
<el-table-column prop="missing_count" label="缺失天数" width="100">
<template #default="{ row }">
<el-tag type="warning" size="small">{{ row.missing_count }}</el-tag>
</template>
</el-table-column>
<el-table-column label="缺失率">
<template #default="{ row }">
<el-progress
:percentage="Math.round(row.missing_ratio * 100)"
:status="row.missing_ratio > 0.5 ? 'exception' : 'warning'"
/>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 批量检测结果 -->
<el-card class="result-card" v-if="batchDetectResult.length > 0">
<template #header>
<span>批量检测结果</span>
</template>
<el-table :data="batchDetectResult" stripe>
<el-table-column prop="code" label="代码" width="120" />
<el-table-column prop="missingCount" label="缺失天数" width="100" />
<el-table-column label="缺失率">
<template #default="{ row }">
<el-progress
:percentage="Math.round(row.missingRatio * 100)"
:status="row.missingRatio > 0.5 ? 'exception' : 'warning'"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button type="primary" size="small" @click="showDetail(row)">
详情
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { detectMissingData, batchCacheData, detectAllMissingData, cacheAllMissingData, getCacheTask, fillMissingData } from '@/api/cache'
const detecting = ref(false)
const caching = ref(false)
const detectingAll = ref(false)
const cachingAll = ref(false)
const fillingMissing = ref(false)
const codeInput = ref('000001.SZ\n600000.SH')
const detectResult = ref<any>(null)
const batchDetectResult = ref<any[]>([])
const cacheTask = ref<any>(null)
const wsProgress = reactive({
status: '',
progress: 0,
message: '',
total_count: 0,
processed: 0,
missing: 0,
complete: 0
})
let ws: WebSocket | null = null
const hasMissing = computed(() => batchDetectResult.value.some(r => r.missingCount > 0))
const dailyStatsList = computed(() => {
if (!detectResult.value || !detectResult.value.daily_stats) return []
return Object.entries(detectResult.value.daily_stats).map(([date, stats]) => ({
date,
expected: (stats as any).expected,
actual: (stats as any).actual,
missing: (stats as any).missing
}))
})
const form = reactive({
securityType: 'stock',
periodType: 'daily',
contractType: 'all',
startDate: getDefaultStartDate(),
endDate: getDefaultEndDate()
})
function getDefaultStartDate() {
const date = new Date()
date.setFullYear(date.getFullYear() - 1)
return formatDate(date)
}
function getDefaultEndDate() {
return formatDate(new Date())
}
function formatDate(date: Date) {
return date.toISOString().slice(0, 10).replace(/-/g, '')
}
function getStatusType(status: string) {
if (status === 'completed') return 'success'
if (status === 'running') return 'warning'
return 'danger'
}
function getProgressStatus(status: string) {
if (status === 'completed') return 'success'
if (status === 'failed') return 'exception'
return undefined
}
function getProgressTagType(status: string) {
if (status === 'completed') return 'success'
if (status === 'running') return 'warning'
if (status === 'starting') return 'info'
return 'danger'
}
const parseCodes = () => {
return codeInput.value
.split(/[\n,]/)
.map(c => c.trim())
.filter(c => c.length > 0)
}
const connectWebSocket = (taskId: string) => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const host = window.location.host || 'localhost:3000'
ws = new WebSocket(`${protocol}//${host}/api/v1/progress/${taskId}`)
ws.onopen = () => {
console.log('WebSocket连接成功')
}
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
wsProgress.status = data.status
wsProgress.progress = data.progress || 0
wsProgress.message = data.message || ''
wsProgress.total_count = data.total_count || 0
wsProgress.processed = data.processed || 0
wsProgress.missing = data.missing || 0
wsProgress.complete = data.complete || 0
if (data.status === 'completed' || data.status === 'failed') {
if (ws) {
ws.close()
ws = null
}
}
} catch (e) {
console.error('WebSocket消息解析失败', e)
}
}
ws.onerror = (error) => {
console.error('WebSocket错误', error)
}
ws.onclose = () => {
console.log('WebSocket连接关闭')
ws = null
}
}
const closeWebSocket = () => {
if (ws) {
ws.close()
ws = null
}
wsProgress.status = ''
wsProgress.progress = 0
}
const pollTaskProgress = async (taskId: number) => {
const poll = async () => {
if (!cacheTask.value || cacheTask.value.status === 'running' || cacheTask.value.status === 'pending') {
const res: any = await getCacheTask(taskId)
if (res.data && res.data.task) {
cacheTask.value = res.data.task
}
if (cacheTask.value && (cacheTask.value.status === 'running' || cacheTask.value.status === 'pending')) {
setTimeout(poll, 2000)
} else if (cacheTask.value && cacheTask.value.status === 'completed') {
ElMessage.success(`缓存完成:成功${cacheTask.value.success_count}个,错误${cacheTask.value.error_count}`)
} else if (cacheTask.value && cacheTask.value.status === 'failed') {
ElMessage.error(`缓存失败:${cacheTask.value.error_message}`)
}
}
}
setTimeout(poll, 1000)
}
const handleDetectAll = async () => {
detectingAll.value = true
detectResult.value = null
const taskId = `detect_${form.securityType}_${Date.now()}`
connectWebSocket(taskId)
try {
const res: any = await detectAllMissingData({
security_type: form.securityType,
period_type: form.periodType,
contract_type: form.contractType,
start_date: form.startDate,
end_date: form.endDate,
task_id: taskId
})
if (res.data) {
detectResult.value = res.data
if (res.data.status === 'completed') {
ElMessage.success(`检测完成:完整${res.data.complete_count}个,缺失${res.data.missing_count}个,错误${res.data.error_count}`)
} else if (res.data.status === 'failed') {
ElMessage.error(`检测失败:${res.data.error_message || '未知错误'}`)
}
} else {
ElMessage.error(res.message || '检测失败,请稍后重试')
}
} catch (error: any) {
console.error(error)
const errorMsg = error.response?.data?.message || error.message || '网络请求失败'
ElMessage.error(`检测失败: ${errorMsg}`)
} finally {
detectingAll.value = false
}
}
const handleCacheAll = async () => {
cachingAll.value = true
cacheTask.value = null
try {
const res: any = await cacheAllMissingData({
security_type: form.securityType,
period_type: form.periodType,
contract_type: form.contractType,
start_date: form.startDate,
end_date: form.endDate
})
if (res.data) {
cacheTask.value = {
task_id: res.data.task_id,
task_name: res.data.task_name,
status: res.data.status,
total_count: res.data.total_count,
progress: res.data.progress,
success_count: 0,
error_count: 0
}
ElMessage.success(`缓存任务已启动,共${res.data.total_count}个代码`)
pollTaskProgress(res.data.task_id)
} else {
ElMessage.error(res.message || '缓存任务启动失败')
}
} catch (error: any) {
console.error(error)
const errorMsg = error.response?.data?.message || error.message || '网络请求失败'
ElMessage.error(`缓存失败: ${errorMsg}`)
} finally {
cachingAll.value = false
}
}
const handleFillMissing = async () => {
fillingMissing.value = true
cacheTask.value = null
try {
const res: any = await fillMissingData({
security_type: form.securityType,
period_type: form.periodType,
contract_type: form.contractType,
start_date: form.startDate,
end_date: form.endDate
})
if (res.data) {
if (res.data.message) {
ElMessage.info(res.data.message)
} else {
cacheTask.value = {
task_id: res.data.task_id,
task_name: res.data.task_name,
status: res.data.status,
total_count: res.data.total_count,
progress: res.data.progress,
success_count: 0,
error_count: 0
}
ElMessage.success(`补齐任务已启动,共${res.data.missing_count}个缺失代码`)
if (res.data.task_id) {
pollTaskProgress(res.data.task_id)
}
}
} else {
ElMessage.error(res.message || '补齐任务启动失败')
}
} catch (error: any) {
console.error(error)
const errorMsg = error.response?.data?.message || error.message || '网络请求失败'
ElMessage.error(`补齐失败: ${errorMsg}`)
} finally {
fillingMissing.value = false
}
}
const handleDetect = async () => {
const codes = parseCodes()
if (codes.length === 0) {
ElMessage.warning('请输入代码')
return
}
detecting.value = true
try {
const res: any = await detectMissingData({
security_type: form.securityType,
period_type: form.periodType,
start_date: form.startDate,
end_date: form.endDate,
code_list: codes
})
if (res.data) {
batchDetectResult.value = res.data.missing_codes.map((item: any) => ({
code: item.code,
missingCount: item.missing_dates.length,
missingRatio: item.missing_dates.length > 0
? item.missing_dates.reduce((sum: number, d: any) => sum + d.missing_ratio, 0) / item.missing_dates.length
: 0,
details: item.missing_dates
}))
if (batchDetectResult.value.length === 0) {
ElMessage.success('所有代码数据完整,无缺失')
}
} else {
ElMessage.error(res.message || '检测失败,请稍后重试')
}
} catch (error: any) {
console.error(error)
const errorMsg = error.response?.data?.message || error.message || '网络请求失败'
ElMessage.error(`检测失败: ${errorMsg}`)
} finally {
detecting.value = false
}
}
const handleCache = async () => {
const codes = parseCodes()
if (codes.length === 0) return
caching.value = true
try {
const res: any = await batchCacheData({
security_type: form.securityType,
period_type: form.periodType,
start_date: form.startDate,
end_date: form.endDate,
code_list: codes
})
if (res.data) {
ElMessage.success('缓存任务已启动')
} else {
ElMessage.error(res.message || '缓存任务启动失败')
}
} catch (error: any) {
console.error(error)
const errorMsg = error.response?.data?.message || error.message || '网络请求失败'
ElMessage.error(`缓存失败: ${errorMsg}`)
} finally {
caching.value = false
}
}
const showDetail = (row: any) => {
console.log(row.details)
}
</script>
<style scoped>
.detect-missing {
padding: 10px;
}
.summary-card {
margin-top: 20px;
}
.progress-card {
margin-top: 20px;
}
.daily-card {
margin-top: 20px;
}
.missing-card {
margin-top: 20px;
}
.result-card {
margin-top: 20px;
}
</style>