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:
@@ -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"];
|
||||
|
||||
@@ -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 参数解析会 422;null/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);
|
||||
|
||||
Reference in New Issue
Block a user