rebuild(runtime): govern market operations and job truth
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import { operationsApi } from "../../shared/api/operations";
|
||||
import { findWorkspace } from "../workspaceRegistry";
|
||||
|
||||
const route = useRoute();
|
||||
@@ -9,12 +10,20 @@ const title = computed(() => {
|
||||
if (typeof route.meta.title === "string") return route.meta.title;
|
||||
return findWorkspace(String(route.params.workspace ?? ""))?.title ?? "小白复盘";
|
||||
});
|
||||
const runtimeMessage = ref("正在读取行情状态");
|
||||
let timer: ReturnType<typeof setInterval> | undefined;
|
||||
async function loadStatus(): Promise<void> {
|
||||
try { runtimeMessage.value = (await operationsApi.status()).message; }
|
||||
catch { runtimeMessage.value = "行情状态暂不可用"; }
|
||||
}
|
||||
onMounted(() => { void loadStatus(); timer = setInterval(loadStatus, 15_000); });
|
||||
onBeforeUnmount(() => { if (timer) clearInterval(timer); });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="statusbar">
|
||||
<span>{{ title }}</span>
|
||||
<span class="statusbar-center">股市有风险,投资需谨慎</span>
|
||||
<span class="statusbar-right">等待行情数据</span>
|
||||
<span class="statusbar-right">{{ runtimeMessage }}</span>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
|
||||
import { api } from "../../shared/api/client";
|
||||
import { operationsApi, type JobRun } from "../../shared/api/operations";
|
||||
import { useMarketStore } from "../../shared/stores/market";
|
||||
import { useUiStore } from "../../shared/stores/ui";
|
||||
|
||||
type CredentialStatus = { name: string; configured: boolean; updated_at: string | null };
|
||||
@@ -12,6 +14,7 @@ const labels: Record<string, string> = {
|
||||
ifind_access_token: "iFinD Access Token",
|
||||
};
|
||||
const ui = useUiStore();
|
||||
const market = useMarketStore();
|
||||
const statuses = ref<CredentialStatus[]>([]);
|
||||
const values = reactive<Record<string, string>>({});
|
||||
const loading = ref(true);
|
||||
@@ -19,11 +22,36 @@ const errorMessage = ref("");
|
||||
const syncing = ref(false);
|
||||
const syncResult = ref("");
|
||||
const syncError = ref("");
|
||||
const jobs = ref<JobRun[]>([]);
|
||||
const backfill = reactive({ start_date: market.selectedDate, end_date: market.selectedDate });
|
||||
const backfilling = ref(false);
|
||||
const operationMessage = ref("");
|
||||
const event = reactive({
|
||||
trade_date: market.selectedDate,
|
||||
identifier: "",
|
||||
event_type: "limit_up" as "limit_up" | "broken" | "limit_down",
|
||||
reason: "",
|
||||
first_time: "",
|
||||
last_time: "",
|
||||
open_times: null as number | null,
|
||||
});
|
||||
const eventBusy = ref(false);
|
||||
const eventHistory = ref<Record<string, unknown>[]>([]);
|
||||
|
||||
const jobLabels: Record<string, string> = {
|
||||
"market.refresh": "行情刷新",
|
||||
"market.backfill": "历史回补",
|
||||
"auction.collect": "竞价采集",
|
||||
"market.event-supplement": "事件补充",
|
||||
"screener.after-close": "盘后选股",
|
||||
};
|
||||
const statusLabels = { running: "执行中", completed: "已完成", failed: "失败" };
|
||||
|
||||
async function load(): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
statuses.value = await api.get<CredentialStatus[]>("/admin/system/credentials");
|
||||
jobs.value = await operationsApi.jobs();
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : "行情凭据读取失败。";
|
||||
} finally {
|
||||
@@ -31,6 +59,70 @@ async function load(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadJobs(): Promise<void> {
|
||||
try { jobs.value = await operationsApi.jobs(); }
|
||||
catch (error) { ui.showToast(error instanceof Error ? error.message : "任务状态读取失败"); }
|
||||
}
|
||||
|
||||
async function runBackfill(): Promise<void> {
|
||||
backfilling.value = true;
|
||||
operationMessage.value = "";
|
||||
try {
|
||||
const result = await operationsApi.backfill(backfill.start_date, backfill.end_date);
|
||||
operationMessage.value = `已完成 ${Number(result.completed ?? 0)} 个交易日回补`;
|
||||
ui.showToast("历史数据回补已完成");
|
||||
await loadJobs();
|
||||
} catch (error) {
|
||||
ui.showToast(error instanceof Error ? error.message : "历史回补失败");
|
||||
} finally { backfilling.value = false; }
|
||||
}
|
||||
|
||||
async function supplementEvents(): Promise<void> {
|
||||
eventBusy.value = true;
|
||||
try {
|
||||
const result = await operationsApi.supplementEvents(event.trade_date);
|
||||
operationMessage.value = `事件原因已匹配 ${Number(result.matched ?? 0)} 条`;
|
||||
ui.showToast("事件原因补充完成");
|
||||
await loadJobs();
|
||||
} catch (error) {
|
||||
ui.showToast(error instanceof Error ? error.message : "事件补充暂不可用");
|
||||
} finally { eventBusy.value = false; }
|
||||
}
|
||||
|
||||
async function reviseEvent(): Promise<void> {
|
||||
eventBusy.value = true;
|
||||
try {
|
||||
await operationsApi.reviseEvent(event.trade_date, event.identifier.trim(), {
|
||||
event_type: event.event_type,
|
||||
reason: event.reason,
|
||||
first_time: event.first_time,
|
||||
last_time: event.last_time,
|
||||
open_times: event.open_times,
|
||||
});
|
||||
operationMessage.value = "人工修订已保存,并优先于自动补充内容";
|
||||
ui.showToast("事件原因修订已保存");
|
||||
await showEventHistory();
|
||||
} catch (error) {
|
||||
ui.showToast(error instanceof Error ? error.message : "事件修订失败");
|
||||
} finally { eventBusy.value = false; }
|
||||
}
|
||||
|
||||
async function showEventHistory(): Promise<void> {
|
||||
if (!event.identifier.trim()) return;
|
||||
try {
|
||||
eventHistory.value = await operationsApi.eventHistory(
|
||||
event.trade_date, event.identifier.trim(),
|
||||
);
|
||||
} catch (error) {
|
||||
ui.showToast(error instanceof Error ? error.message : "修订记录读取失败");
|
||||
}
|
||||
}
|
||||
|
||||
function duration(value: number | null): string {
|
||||
if (value === null) return "";
|
||||
return value < 1000 ? `${value} ms` : `${(value / 1000).toFixed(1)} s`;
|
||||
}
|
||||
|
||||
async function save(name: string): Promise<void> {
|
||||
const value = values[name]?.trim();
|
||||
if (!value) {
|
||||
@@ -97,11 +189,32 @@ onMounted(load);
|
||||
</div>
|
||||
</section>
|
||||
<section class="card">
|
||||
<header class="card-header"><h2>历史数据回补</h2><span class="tag">暂不可用</span></header>
|
||||
<div class="card-body disabled-row">
|
||||
<p class="muted">历史数据回补将在行情数据迁移完成后开放。</p>
|
||||
<button class="btn" type="button" disabled>开始回补</button>
|
||||
</div>
|
||||
<header class="card-header"><h2>历史数据回补</h2><span class="faint">单次最多15个交易日</span></header>
|
||||
<form class="card-body operation-form" @submit.prevent="runBackfill">
|
||||
<label class="field"><span class="field-label">开始日期</span><input v-model="backfill.start_date" class="input" type="date" required /></label>
|
||||
<label class="field"><span class="field-label">结束日期</span><input v-model="backfill.end_date" class="input" type="date" required /></label>
|
||||
<button class="btn" type="submit" :disabled="backfilling">{{ backfilling ? "正在回补" : "开始回补" }}</button>
|
||||
</form>
|
||||
</section>
|
||||
<section class="card">
|
||||
<header class="card-header"><h2>事件原因治理</h2><span class="faint">人工修订优先并保留记录</span></header>
|
||||
<form class="card-body event-form" @submit.prevent="reviseEvent">
|
||||
<label class="field"><span class="field-label">数据日期</span><input v-model="event.trade_date" class="input" type="date" required /></label>
|
||||
<label class="field"><span class="field-label">股票代码</span><input v-model="event.identifier" class="input" placeholder="000001.SZ" required /></label>
|
||||
<label class="field"><span class="field-label">事件类型</span><select v-model="event.event_type" class="select"><option value="limit_up">涨停</option><option value="broken">炸板</option><option value="limit_down">跌停</option></select></label>
|
||||
<label class="field event-reason"><span class="field-label">原因</span><input v-model="event.reason" class="input" maxlength="200" required /></label>
|
||||
<label class="field"><span class="field-label">首次时间</span><input v-model="event.first_time" class="input" type="time" /></label>
|
||||
<label class="field"><span class="field-label">最后时间</span><input v-model="event.last_time" class="input" type="time" /></label>
|
||||
<label class="field"><span class="field-label">开板次数</span><input v-model.number="event.open_times" class="input" type="number" min="0" /></label>
|
||||
<div class="table-actions event-actions"><button class="btn btn-primary" type="submit" :disabled="eventBusy">保存修订</button><button class="btn" type="button" :disabled="eventBusy" @click="supplementEvents">自动补充</button><button class="btn" type="button" @click="showEventHistory">查看记录</button></div>
|
||||
</form>
|
||||
<div v-if="eventHistory.length" class="data-table-wrap event-history"><table class="data-table"><thead><tr><th>时间</th><th>来源</th><th>类型</th><th>原因</th><th>修订人</th></tr></thead><tbody><tr v-for="row in eventHistory" :key="String(row.id)"><td>{{ String(row.created_at).replace('T', ' ').slice(0, 19) }}</td><td>{{ row.source === 'admin' ? '人工修订' : '自动补充' }}</td><td>{{ row.event_type }}</td><td>{{ row.reason }}</td><td>{{ row.created_by_name ?? '' }}</td></tr></tbody></table></div>
|
||||
</section>
|
||||
<section class="card">
|
||||
<header class="card-header"><h2>后台任务状态</h2><button class="btn btn-small" type="button" @click="loadJobs">刷新状态</button></header>
|
||||
<p v-if="operationMessage" class="notice">{{ operationMessage }}</p>
|
||||
<div v-if="jobs.length" class="data-table-wrap operation-jobs"><table class="data-table"><thead><tr><th>任务</th><th>数据日期</th><th>状态</th><th>开始时间</th><th class="numeric">耗时</th><th class="numeric">覆盖率(%)</th><th>说明</th></tr></thead><tbody><tr v-for="job in jobs" :key="job.id"><td>{{ jobLabels[job.kind] ?? job.kind }}</td><td>{{ job.requested_date }}</td><td><span class="tag" :class="{ warning: job.status === 'failed' }">{{ statusLabels[job.status] }}</span></td><td>{{ job.started_at.replace('T', ' ').slice(0, 19) }}</td><td class="numeric">{{ duration(job.duration_ms) }}</td><td class="numeric">{{ job.coverage === null ? '' : (job.coverage * 100).toFixed(1) }}</td><td>{{ job.error_message }}</td></tr></tbody></table></div>
|
||||
<p v-else class="card-body muted">暂无后台任务记录</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user