checkpoint: real-scale 数据链 + 统计引擎 + ABLUP 稀疏性能修复

init 后首次落盘,累计工作:
- 数据侧:simulate_breeding_data real-scale 3.5万树重建 + 分层观测
- 引擎侧:ABLUP 稀疏生产档 EM-REML(blup 1.6.0/1.7.0);性能倒挂修复——
  系谱闭包收口为 BFS 可达祖先 + N_EXACT 3000 对齐 N_SUBSAMPLE(选中 200~3000 树
  不再顶进数小时精确迹尾),A/B 数值等价实证 + 守卫探针
- 前端:观测过滤 + 表单/HTTP 工具链完善
- 文档:业务链/观测梳理 + 引擎实施记录 + 规划对照总表
- gitignore:排除 Temp/调试脚本/一次性验证输出
This commit is contained in:
34047007@qq.com
2026-08-07 08:03:10 +08:00
parent b95053c52c
commit e937fc930c
16 changed files with 2293 additions and 238 deletions
@@ -33,6 +33,9 @@ export interface SanitizeOutputOptions {
/** 传递给组件时需排除的表单配置属性 */
const ROOT_PROPS = ["label", "labelWidth", "key", "type", "hidden", "span", "slots"];
/** 选项超过该数量时自动切换为虚拟滚动选择器(ElSelectV2),避免渲染数万个 el-option 卡死页面 */
const VIRTUAL_SELECT_THRESHOLD = 2000;
/** 日期选择器类型列表(getProps 中用于传递 type 到 FaDatePicker */
const DATE_PICKER_TYPES = ["date", "daterange", "datetime", "datetimerange", "monthrange"];
@@ -167,6 +170,18 @@ export const getProps = (item: FormItemBase): Record<string, any> => {
return props;
};
/**
* 大选项集 select 是否切换为虚拟滚动(ElSelectV2)。
* 有自定义插槽 / render 的表单项保持原样,避免破坏自定义选项渲染。
*/
export const isVirtualizedSelect = (item: FormItemBase): boolean => {
if (item.type !== "select" || item.slots) return false;
const props = getProps(item);
if (props?.default) return false;
const opts = props?.options;
return Array.isArray(opts) && opts.length > VIRTUAL_SELECT_THRESHOLD;
};
/**
* 获取插槽 —— 过滤掉未定义的插槽
*/
@@ -40,8 +40,10 @@
@update:model-value="setFieldValue(item.key, $event)"
v-bind="getProps(item)"
>
<!-- 下拉选择 -->
<template v-if="item.type === 'select' && getProps(item)?.options">
<!-- 下拉选择大选项集自动走虚拟滚动 ElSelectV2选项经 props 传入 -->
<template
v-if="item.type === 'select' && getProps(item)?.options && !isVirtualizedSelect(item)"
>
<ElOption
v-for="option in getProps(item).options"
v-bind="option"
@@ -137,7 +139,9 @@
@update:model-value="setFieldValue(item.key, $event)"
v-bind="getProps(item)"
>
<template v-if="item.type === 'select' && getProps(item)?.options">
<template
v-if="item.type === 'select' && getProps(item)?.options && !isVirtualizedSelect(item)"
>
<ElOption
v-for="option in getProps(item).options"
v-bind="option"
@@ -205,13 +209,14 @@
import { useI18n } from "vue-i18n";
import { type Component } from "vue";
import FaDatePicker from "@/components/forms/fa-search-bar/FaDatePicker.vue";
import {ElCascader, ElCheckbox, ElCheckboxGroup, ElInput, ElInputTag, ElInputNumber, ElRadioGroup, ElRate, ElSelect, ElSlider, ElSwitch, ElTimePicker, ElTimeSelect, ElTreeSelect, type FormInstance} from "element-plus";
import {ElCascader, ElCheckbox, ElCheckboxGroup, ElInput, ElInputTag, ElInputNumber, ElRadioGroup, ElRate, ElSelect, ElSelectV2, ElSlider, ElSwitch, ElTimePicker, ElTimeSelect, ElTreeSelect, type FormInstance} from "element-plus";
import {
cloneModelValue as cloneModelValueShared,
sanitizeOutputValue as sanitizeOutputValueShared,
getProps as getPropsShared,
getSlots as getSlotsShared,
getColSpan as getColSpanShared,
isVirtualizedSelect,
useSanitizeOutputOptions,
type SanitizeOutputOptions,
} from "../composables/useFormBase";
@@ -425,6 +430,10 @@ const getComponent = (item: FormItem) => {
if (item.render) {
return item.render;
}
// 大选项集自动切换虚拟滚动选择器,避免渲染数万个 el-option 卡死页面
if (isVirtualizedSelect(item)) {
return ElSelectV2;
}
// 使用 type 获取预定义组件
const { type } = item;
const comp = componentMap[type as keyof typeof componentMap];
@@ -66,8 +66,10 @@
@update:model-value="setFieldValue(item.key, $event)"
v-bind="getProps(item)"
>
<!-- 下拉选择 -->
<template v-if="item.type === 'select' && getProps(item)?.options">
<!-- 下拉选择(大选项集自动走虚拟滚动 ElSelectV2,选项经 props 传入) -->
<template
v-if="item.type === 'select' && getProps(item)?.options && !isVirtualizedSelect(item)"
>
<ElOption
v-for="option in getProps(item).options"
v-bind="option"
@@ -169,13 +171,14 @@ import {
getAuditSearchFormItems,
type GetAuditSearchFormItemsOptions,
} from "./auditSearchFormItems";
import {ElCascader, ElCheckbox, ElCheckboxGroup, ElInput, ElInputTag, ElInputNumber, ElRadioGroup, ElRate, ElSelect, ElSlider, ElSwitch, ElTimePicker, ElTimeSelect, ElTreeSelect, type FormInstance} from "element-plus";
import {ElCascader, ElCheckbox, ElCheckboxGroup, ElInput, ElInputTag, ElInputNumber, ElRadioGroup, ElRate, ElSelect, ElSelectV2, ElSlider, ElSwitch, ElTimePicker, ElTimeSelect, ElTreeSelect, type FormInstance} from "element-plus";
import {
cloneModelValue as cloneModelValueShared,
sanitizeOutputValue as sanitizeOutputValueShared,
getProps as getPropsShared,
getSlots as getSlotsShared,
getColSpan as getColSpanShared,
isVirtualizedSelect,
useSanitizeOutputOptions,
type SanitizeOutputOptions,
} from "../composables/useFormBase";
@@ -379,6 +382,10 @@ const getComponent = (item: SearchFormItem) => {
if (item.render) {
return item.render;
}
// 大选项集自动切换虚拟滚动选择器,避免渲染数万个 el-option 卡死页面
if (isVirtualizedSelect(item)) {
return ElSelectV2;
}
// 使用 type 获取预定义组件
const { type } = item;
return componentMap[type as keyof typeof componentMap] || componentMap["input"];
+9 -1
View File
@@ -199,7 +199,15 @@ export const request: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: Number(import.meta.env.VITE_API_TIMEOUT) || 15000,
headers: { "Content-Type": "application/json;charset=utf-8" },
paramsSerializer: (params) => qs.stringify(params, { indices: false }),
// qs 会把 null 序列化成空串(?year=),后端 int 参数解析会 422null/undefined 应直接省略
paramsSerializer: (params) => {
const cleaned: Record<string, unknown> = {};
for (const [key, value] of Object.entries(params ?? {})) {
if (value === null || value === undefined) continue;
cleaned[key] = value;
}
return qs.stringify(cleaned, { indices: false });
},
});
request.interceptors.request.use(
@@ -1349,15 +1349,11 @@
<el-select v-model="mabcBgPanels" multiple collapse-tags placeholder="背景恢复面板全基因组标记" style="min-width: 220px">
<el-option v-for="p in masPanels" :key="p.id" :label="p.panel_name" :value="p.id" />
</el-select>
<el-select v-model="mabcRpTree" placeholder="轮回亲本树" style="min-width: 170px">
<el-option v-for="t in treeOptions" :key="t.value" :label="t.label" :value="t.value" />
</el-select>
<el-select-v2 v-model="mabcRpTree" :options="treeOptions" placeholder="轮回亲本树" style="min-width: 170px" />
<el-button :loading="treeOptLoading" @click="loadTreeOptions">刷新树</el-button>
</div>
<div class="toolbar">
<el-select v-model="mabcCands" multiple collapse-tags placeholder="候选回交单株BC 分离群体" style="min-width: 300px">
<el-option v-for="t in treeOptions" :key="t.value" :label="t.label" :value="t.value" />
</el-select>
<el-select-v2 v-model="mabcCands" :options="treeOptions" multiple collapse-tags placeholder="候选回交单株BC 分离群体" style="min-width: 300px" />
<span style="color:#909399; font-size:12px">前景阈值</span>
<el-input-number v-model="mabcMinHits" :min="1" style="width: 90px" />
<span style="color:#909399; font-size:12px">当前世代</span>
@@ -2435,7 +2431,7 @@ async function loadAnova() {
}
async function loadTrialStudies() {
try {
const res: any = await TrialStudyAPI.getTrialStudyList({ page_no: 1, page_size: 200 } as any);
const res: any = await TrialStudyAPI.getTrialStudyList({ page_no: 1, page_size: 100 } as any);
trialStudies.value = (res?.data?.data?.items ?? []) as Array<{ id: number; study_name: string; block_count?: number }>;
} catch (e: any) {
msg.error(e?.message || '研究点列表加载失败');
@@ -306,7 +306,7 @@ function displayValue(trait: TraitTable): string {
async function loadTraits() {
if (traitsLoaded.value) return;
try {
const res = await TraitAPI.getTraitList({ page_no: 1, page_size: 500 });
const res = await TraitAPI.getTraitList({ page_no: 1, page_size: 100 });
if (res.data.code === ResultEnum.SUCCESS) {
const list = res.data.data.items ?? [];
for (const t of TRAIT_TABS) traitsByCategory[t] = [];
@@ -326,7 +326,7 @@ async function loadObservations(evaluationId: number) {
const res = await TraitObservationAPI.getTraitObservationList({
evaluation_id: evaluationId,
page_no: 1,
page_size: 500,
page_size: 100,
});
if (res.data.code === ResultEnum.SUCCESS) {
const list = res.data.data.items ?? [];
@@ -856,7 +856,7 @@ async function syncObservations(evaluationId: number) {
const existing = await TraitObservationAPI.getTraitObservationList({
evaluation_id: evaluationId,
page_no: 1,
page_size: 500,
page_size: 100,
});
const ids = (existing.data.data.items ?? []).map((o) => o.id).filter((x): x is number => x != null);
if (ids.length) await TraitObservationAPI.deleteTraitObservation(ids);