952 lines
28 KiB
Plaintext
952 lines
28 KiB
Plaintext
<template>
|
|
<div class="fa-full-height">
|
|
<FaSearchBar
|
|
v-show="showSearchBar"
|
|
ref="searchBarRef"
|
|
v-model="searchForm"
|
|
:items="TreeEvaluationSearchItems"
|
|
:rules="searchBarRules"
|
|
:is-expand="false"
|
|
:show-expand="true"
|
|
:show-reset="true"
|
|
:show-search="true"
|
|
:disabled-search="false"
|
|
:default-expanded="false"
|
|
include-audit
|
|
@search="handleSearch"
|
|
@reset="onResetSearch"
|
|
/>
|
|
|
|
<ElCard class="fa-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
|
|
<FaTableHeader
|
|
v-model:columns="columnChecks"
|
|
v-model:showSearchBar="showSearchBar"
|
|
:loading="loading"
|
|
@refresh="refreshData"
|
|
>
|
|
<template #left>
|
|
<FaTableHeaderLeft
|
|
:remove-ids="selectedIds"
|
|
:perm-create="['module_bre:tree_evaluation:create']"
|
|
:perm-import="['module_bre:tree_evaluation:import']"
|
|
:perm-export="['module_bre:tree_evaluation:export']"
|
|
:perm-delete="['module_bre:tree_evaluation:delete']"
|
|
:delete-loading="batchDeleting"
|
|
:create-loading="createLoading"
|
|
@add="handleAdd"
|
|
@import="openImport"
|
|
@export="openExport"
|
|
@delete="handleBatchDelete"
|
|
/>
|
|
</template>
|
|
</FaTableHeader>
|
|
|
|
<FaTable
|
|
ref="faTableRef"
|
|
:loading="loading"
|
|
:data="data"
|
|
:columns="columns"
|
|
:pagination="pagination"
|
|
@selection-change="onTableSelectionChange"
|
|
@pagination:size-change="handleSizeChange"
|
|
@pagination:current-change="handleCurrentChange"
|
|
/>
|
|
</ElCard>
|
|
|
|
<FaDialog
|
|
v-model="dialogVisible.visible"
|
|
:title="dialogVisible.title"
|
|
width="960px"
|
|
dialog-class="crud-embed-dialog"
|
|
modal-class="crud-embed-dialog"
|
|
:confirm-loading="submitLoading"
|
|
:close-on-click-modal="false"
|
|
@cancel="handleCloseDialog"
|
|
@close="handleCloseDialog"
|
|
@confirm="handleSubmit"
|
|
>
|
|
<el-tabs v-model="activeTab" class="eval-tabs">
|
|
<el-tab-pane label="基本信息" name="基本信息">
|
|
<div v-if="treeDetailData" class="tree-basic">
|
|
<FaDescriptions :column="3" :data="treeDetailData" :items="treeDetailItems" border />
|
|
</div>
|
|
<el-alert
|
|
v-else
|
|
class="tree-basic-tip"
|
|
title="请先在下方选择单株以载入基本信息"
|
|
type="info"
|
|
:closable="false"
|
|
show-icon
|
|
/>
|
|
<div class="eval-form-wrap">
|
|
<FaForm
|
|
v-if="dialogVisible.type !== 'detail'"
|
|
:key="TreeEvaluationFormRenderKey"
|
|
scrollbar
|
|
max-height="50vh"
|
|
ref="dataFormRef"
|
|
v-model="formData"
|
|
:items="evalFormItems"
|
|
:rules="rules"
|
|
label-suffix=":"
|
|
:label-width="120"
|
|
label-position="right"
|
|
:span="12"
|
|
:gutter="16"
|
|
:show-reset="false"
|
|
:show-submit="false"
|
|
class="crud-dialog-art-form"
|
|
/>
|
|
<FaDescriptions
|
|
v-else
|
|
:column="3"
|
|
:data="formData"
|
|
:items="evalDetailItems"
|
|
border
|
|
/>
|
|
</div>
|
|
</el-tab-pane>
|
|
|
|
<el-tab-pane v-for="cat in TRAIT_TABS" :key="cat" :label="cat" :name="cat">
|
|
<div class="trait-grid">
|
|
<div v-for="trait in traitsByCategory[cat] || []" :key="trait.id" class="trait-row">
|
|
<div class="trait-label">
|
|
{{ trait.trait_name }}
|
|
<span v-if="trait.unit" class="trait-unit">({{ trait.unit }})</span>
|
|
</div>
|
|
<div class="trait-value">
|
|
<template v-if="dialogVisible.type === 'detail'">
|
|
<span class="trait-readonly">{{ displayValue(trait) }}</span>
|
|
</template>
|
|
<template v-else>
|
|
<el-input-number
|
|
v-if="trait.data_type === 'numeric'"
|
|
v-model="getObs(trait.id!).value_numeric"
|
|
:precision="2"
|
|
:controls="false"
|
|
:min="0"
|
|
style="width: 100%"
|
|
placeholder="请输入"
|
|
/>
|
|
<el-select
|
|
v-else-if="trait.data_type === 'categorical'"
|
|
v-model="getObs(trait.id!).value_text"
|
|
clearable
|
|
filterable
|
|
style="width: 100%"
|
|
placeholder="请选择"
|
|
>
|
|
<el-option
|
|
v-for="opt in parseScale(trait.scale_json)"
|
|
:key="opt"
|
|
:label="opt"
|
|
:value="opt"
|
|
/>
|
|
</el-select>
|
|
<el-date-picker
|
|
v-else-if="trait.data_type === 'date'"
|
|
v-model="getObs(trait.id!).value_date"
|
|
type="date"
|
|
value-format="YYYY-MM-DD"
|
|
style="width: 100%"
|
|
placeholder="请选择"
|
|
/>
|
|
<el-input v-else v-model="getObs(trait.id!).value_text" clearable placeholder="请输入" />
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<el-empty
|
|
v-if="!(traitsByCategory[cat] && traitsByCategory[cat].length)"
|
|
description="暂无性状"
|
|
:image-size="60"
|
|
/>
|
|
</div>
|
|
</el-tab-pane>
|
|
</el-tabs>
|
|
</FaDialog>
|
|
|
|
<FaImportDialog
|
|
v-model="importVisible"
|
|
:content-config="TreeEvaluationImportContentConfig"
|
|
default-template-file-name="tree_evaluation_import_template.xlsx"
|
|
@upload="handleCrudImportUpload"
|
|
/>
|
|
|
|
<FaExportDialog
|
|
v-model="exportVisible"
|
|
:content-config="TreeEvaluationExportContentConfig"
|
|
:query-params="exportQueryParams"
|
|
:page-data="data"
|
|
:selection-data="selectedRows"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, reactive, ref, watch } from "vue";
|
|
import { useTable } from "@/hooks";
|
|
import type { TableOperationAction } from "@/utils/table";
|
|
import { renderTableOperationCell, stripPaginationParams, toCrudCols } from "@utils";
|
|
import { ResultEnum } from "@/enums/api/result.enum";
|
|
import { ElMessage } from "element-plus";
|
|
import type { IContentConfig, IObject } from "@/components/modal/types";
|
|
import type { AuditSearchFormParams } from "@/components/forms/fa-search-bar/auditSearchFormItems";
|
|
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
|
import type { ColumnOption } from "@/types/component";
|
|
import CrossCombinationAPI, { type CrossCombinationOption } from "@/api/module_bre/cross_combination";
|
|
import TreeAPI, { type TreeOption, type TreeTable } from "@/api/module_bre/tree";
|
|
import PersonnelAPI, { type PersonnelOption } from "@/api/module_bre/personnel";
|
|
import TraitAPI, { type TraitTable } from "@/api/module_bre/trait";
|
|
import TreeEvaluationAPI, {
|
|
type TreeEvaluationForm,
|
|
type TreeEvaluationPageQuery,
|
|
type TreeEvaluationTable,
|
|
} from "@/api/module_bre/tree_evaluation";
|
|
import TraitObservationAPI from "@/api/module_bre/trait_observation";
|
|
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
|
|
import FaForm from "@/components/forms/fa-form/index.vue";
|
|
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
|
|
|
defineOptions({
|
|
name: "TreeEvaluation",
|
|
inheritAttrs: false,
|
|
});
|
|
|
|
// 性状标签页(不含基本信息)
|
|
const TRAIT_TABS = ["基本鉴定", "果实外观", "果皮", "果实大小", "果肉", "果核"];
|
|
|
|
const cross_combinationOptions = ref<CrossCombinationOption[]>([]);
|
|
const treeOptions = ref<TreeOption[]>([]);
|
|
const personnelOptions = ref<PersonnelOption[]>([]);
|
|
|
|
const cross_combinationOptionsMap = computed(() => {
|
|
const m: Record<number, string> = {};
|
|
for (const o of cross_combinationOptions.value) m[o.value] = o.label;
|
|
return m;
|
|
});
|
|
const treeOptionsMap = computed(() => {
|
|
const m: Record<number, string> = {};
|
|
for (const o of treeOptions.value) m[o.value] = o.label;
|
|
return m;
|
|
});
|
|
const personnelOptionsMap = computed(() => {
|
|
const m: Record<number, string> = {};
|
|
for (const o of personnelOptions.value) m[o.value] = o.label;
|
|
return m;
|
|
});
|
|
|
|
async function loadCrossCombinationOptions() {
|
|
try {
|
|
const res = await CrossCombinationAPI.getCrossCombinationOptions();
|
|
if (res.data.code === ResultEnum.SUCCESS) {
|
|
cross_combinationOptions.value = res.data.data ?? [];
|
|
}
|
|
} catch (error: unknown) {
|
|
if (import.meta.env.DEV) console.error("[TreeEvaluation] loadCrossCombinationOptions", error);
|
|
}
|
|
}
|
|
async function loadTreeOptions() {
|
|
try {
|
|
const res = await TreeAPI.getTreeOptions();
|
|
if (res.data.code === ResultEnum.SUCCESS) {
|
|
treeOptions.value = res.data.data ?? [];
|
|
}
|
|
} catch (error: unknown) {
|
|
if (import.meta.env.DEV) console.error("[TreeEvaluation] loadTreeOptions", error);
|
|
}
|
|
}
|
|
async function loadPersonnelOptions() {
|
|
try {
|
|
const res = await PersonnelAPI.getPersonnelOptions();
|
|
if (res.data.code === ResultEnum.SUCCESS) {
|
|
personnelOptions.value = res.data.data ?? [];
|
|
}
|
|
} catch (error: unknown) {
|
|
if (import.meta.env.DEV) console.error("[TreeEvaluation] loadPersonnelOptions", error);
|
|
}
|
|
}
|
|
|
|
// 性状字典(驱动动态表单)
|
|
const traitsByCategory = reactive<Record<string, TraitTable[]>>({});
|
|
const traitsLoaded = ref(false);
|
|
interface ObsInput {
|
|
value_numeric: number | null;
|
|
value_text: string | null;
|
|
value_date: string | null;
|
|
remark: string | null;
|
|
}
|
|
const observations = reactive<Record<number, ObsInput>>({});
|
|
|
|
function getObs(traitId: number): ObsInput {
|
|
if (!observations[traitId]) {
|
|
observations[traitId] = { value_numeric: null, value_text: null, value_date: null, remark: null };
|
|
}
|
|
return observations[traitId];
|
|
}
|
|
|
|
function parseScale(scale?: string): string[] {
|
|
if (!scale) return [];
|
|
try {
|
|
const v = JSON.parse(scale);
|
|
if (Array.isArray(v)) return v.map(String);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function displayValue(trait: TraitTable): string {
|
|
const o = trait.id != null ? observations[trait.id] : undefined;
|
|
if (!o) return "—";
|
|
if (trait.data_type === "numeric") return o.value_numeric != null ? `${o.value_numeric}${trait.unit ?? ""}` : "—";
|
|
if (trait.data_type === "date") return o.value_date || "—";
|
|
return o.value_text || "—";
|
|
}
|
|
|
|
async function loadTraits() {
|
|
if (traitsLoaded.value) return;
|
|
try {
|
|
const res = await TraitAPI.getTraitList({ page_no: 1, page_size: 500 });
|
|
if (res.data.code === ResultEnum.SUCCESS) {
|
|
const list = res.data.data.list ?? [];
|
|
for (const t of TRAIT_TABS) traitsByCategory[t] = [];
|
|
for (const t of list) {
|
|
const cat = t.category;
|
|
if (cat && traitsByCategory[cat]) traitsByCategory[cat].push(t);
|
|
}
|
|
traitsLoaded.value = true;
|
|
}
|
|
} catch (error: unknown) {
|
|
if (import.meta.env.DEV) console.error("[TreeEvaluation] loadTraits", error);
|
|
}
|
|
}
|
|
|
|
async function loadObservations(evaluationId: number) {
|
|
try {
|
|
const res = await TraitObservationAPI.getTraitObservationList({
|
|
evaluation_id: evaluationId,
|
|
page_no: 1,
|
|
page_size: 500,
|
|
});
|
|
if (res.data.code === ResultEnum.SUCCESS) {
|
|
const list = res.data.data.list ?? [];
|
|
for (const o of list) {
|
|
if (o.trait_id == null) continue;
|
|
observations[o.trait_id] = {
|
|
value_numeric: o.value_numeric ?? null,
|
|
value_text: o.value_text ?? null,
|
|
value_date: o.value_date ?? null,
|
|
remark: o.remark ?? null,
|
|
};
|
|
}
|
|
}
|
|
} catch (error: unknown) {
|
|
if (import.meta.env.DEV) console.error("[TreeEvaluation] loadObservations", error);
|
|
}
|
|
}
|
|
|
|
const treeDetailData = ref<TreeTable | null>(null);
|
|
async function loadTreeDetail(treeId: number) {
|
|
try {
|
|
const res = await TreeAPI.getTreeDetail(treeId);
|
|
if (res.data.code === ResultEnum.SUCCESS) {
|
|
treeDetailData.value = res.data.data;
|
|
// 创建时自动带出杂交组合
|
|
if (dialogVisible.type === "create" && !formData.value.combination_id && treeDetailData.value.combination_id) {
|
|
formData.value.combination_id = treeDetailData.value.combination_id;
|
|
}
|
|
}
|
|
} catch (error: unknown) {
|
|
if (import.meta.env.DEV) console.error("[TreeEvaluation] loadTreeDetail", error);
|
|
}
|
|
}
|
|
|
|
function resetObservations() {
|
|
for (const k of Object.keys(observations)) delete observations[Number(k)];
|
|
}
|
|
|
|
const createInitialFormData = (): TreeEvaluationForm => ({
|
|
id: undefined,
|
|
combination_id: undefined,
|
|
tree_id: undefined,
|
|
evaluate_year: undefined,
|
|
evaluate_date: undefined,
|
|
overall_score: undefined,
|
|
bre_personnel_id: undefined,
|
|
remark: undefined,
|
|
});
|
|
|
|
type TreeEvaluationSearchFormParams = {
|
|
combination_id?: number;
|
|
tree_id?: number;
|
|
evaluate_year?: number;
|
|
bre_personnel_id?: number;
|
|
} & AuditSearchFormParams;
|
|
|
|
const searchForm = ref<TreeEvaluationSearchFormParams>({
|
|
combination_id: undefined,
|
|
tree_id: undefined,
|
|
evaluate_year: undefined,
|
|
bre_personnel_id: undefined,
|
|
created_id: undefined,
|
|
updated_id: undefined,
|
|
created_time: [],
|
|
updated_time: [],
|
|
});
|
|
|
|
const showSearchBar = ref(true);
|
|
const searchBarRef = ref<{ validate: () => Promise<boolean> } | null>(null);
|
|
const searchBarRules: Record<string, unknown> = {};
|
|
|
|
const TreeEvaluationSearchItems = computed(() => [
|
|
{
|
|
label: "杂交组合",
|
|
key: "combination_id",
|
|
type: "select",
|
|
props: {
|
|
placeholder: "请选择杂交组合",
|
|
options: cross_combinationOptions.value,
|
|
clearable: true,
|
|
filterable: true,
|
|
},
|
|
span: 8,
|
|
},
|
|
{
|
|
label: "单株",
|
|
key: "tree_id",
|
|
type: "select",
|
|
props: {
|
|
placeholder: "请选择单株",
|
|
options: treeOptions.value,
|
|
clearable: true,
|
|
filterable: true,
|
|
},
|
|
span: 8,
|
|
},
|
|
{
|
|
label: "评价年份",
|
|
key: "evaluate_year",
|
|
type: "input",
|
|
placeholder: "请输入评价年份",
|
|
clearable: true,
|
|
span: 8,
|
|
},
|
|
{
|
|
label: "评价人",
|
|
key: "bre_personnel_id",
|
|
type: "select",
|
|
props: {
|
|
placeholder: "请选择评价人",
|
|
options: personnelOptions.value,
|
|
clearable: true,
|
|
filterable: true,
|
|
},
|
|
span: 8,
|
|
},
|
|
]);
|
|
|
|
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
|
|
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } = useTableSelection<TreeEvaluationTable>();
|
|
|
|
const createLoading = ref(false);
|
|
|
|
const {
|
|
columns,
|
|
columnChecks,
|
|
data,
|
|
loading,
|
|
pagination,
|
|
searchParams,
|
|
getData,
|
|
replaceSearchParams,
|
|
resetSearchParams,
|
|
handleSizeChange,
|
|
handleCurrentChange,
|
|
refreshData,
|
|
refreshCreate,
|
|
refreshUpdate,
|
|
refreshRemove,
|
|
} = useTable({
|
|
core: {
|
|
apiFn: TreeEvaluationAPI.getTreeEvaluationList,
|
|
apiParams: {
|
|
page_no: 1,
|
|
page_size: 10,
|
|
},
|
|
columnsFactory: (): ColumnOption<TreeEvaluationTable>[] => [
|
|
{ type: "globalIndex", width: 56, label: "序号" },
|
|
{ type: "selection", width: 48, fixed: "left" },
|
|
{ prop: "combination_name", label: "杂交组合", minWidth: 140, showOverflowTooltip: true },
|
|
{ prop: "tree_name", label: "单株", minWidth: 140, showOverflowTooltip: true },
|
|
{ prop: "evaluate_year", label: "评价年份", minWidth: 100 },
|
|
{ prop: "evaluate_date", label: "评价日期", minWidth: 140, showOverflowTooltip: true },
|
|
{ prop: "overall_score", label: "综合评分", minWidth: 100 },
|
|
{ prop: "bre_personnel_name", label: "评价人", minWidth: 140, showOverflowTooltip: true },
|
|
{
|
|
prop: "created_time",
|
|
label: "创建时间",
|
|
width: 168,
|
|
sortable: true,
|
|
showOverflowTooltip: true,
|
|
},
|
|
{
|
|
prop: "updated_time",
|
|
label: "更新时间",
|
|
width: 168,
|
|
sortable: true,
|
|
showOverflowTooltip: true,
|
|
},
|
|
{
|
|
prop: "created_by",
|
|
label: "创建人",
|
|
minWidth: 100,
|
|
formatter: (row: TreeEvaluationTable) => row.created_by?.name ?? "—",
|
|
},
|
|
{
|
|
prop: "updated_by",
|
|
label: "更新人",
|
|
minWidth: 100,
|
|
formatter: (row: TreeEvaluationTable) => row.updated_by?.name ?? "—",
|
|
},
|
|
{
|
|
prop: "operation",
|
|
label: "操作",
|
|
width: 180,
|
|
fixed: "right",
|
|
align: "center",
|
|
formatter: (row: TreeEvaluationTable) => formatTreeEvaluationOperationCell(row),
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const TreeEvaluationCrudCols = toCrudCols(columns);
|
|
|
|
const exportQueryParams = computed(() => {
|
|
return stripPaginationParams(searchParams as Record<string, unknown>);
|
|
});
|
|
|
|
const TreeEvaluationImportContentConfig = computed<IContentConfig>(() => ({
|
|
permPrefix: "module_bre:tree_evaluation",
|
|
cols: TreeEvaluationCrudCols.value,
|
|
indexAction: async () => ({}),
|
|
importTemplate: () => TreeEvaluationAPI.downloadTemplateTreeEvaluation(),
|
|
}));
|
|
|
|
const TreeEvaluationExportContentConfig = computed(() => ({
|
|
permPrefix: "module_bre:tree_evaluation",
|
|
cols: TreeEvaluationCrudCols.value,
|
|
exportsBlobAction: async (params: IObject) => {
|
|
const merged = {
|
|
...(exportQueryParams.value as unknown as Record<string, unknown>),
|
|
...params,
|
|
} as unknown as TreeEvaluationPageQuery;
|
|
const res = await TreeEvaluationAPI.exportTreeEvaluation(merged);
|
|
return res.data as Blob;
|
|
},
|
|
}));
|
|
|
|
const dialogVisible = reactive<{ visible: boolean; title: string; type: "create" | "update" | "detail" }>({
|
|
visible: false,
|
|
title: "",
|
|
type: "create",
|
|
});
|
|
const activeTab = ref("基本信息");
|
|
const submitLoading = ref(false);
|
|
|
|
const treeDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] = [
|
|
{ label: "杂交组合", prop: "combination_name" },
|
|
{ label: "基地/位置", prop: "plot_name" },
|
|
{ label: "单株编号", prop: "tree_no" },
|
|
{ label: "行向", prop: "row_orientation" },
|
|
{ label: "行数", prop: "row_no" },
|
|
{ label: "列数", prop: "col_no" },
|
|
{ label: "定植时间", prop: "planted_date" },
|
|
{ label: "责任人", prop: "bre_personnel_name" },
|
|
{ label: "状态", prop: "status" },
|
|
];
|
|
|
|
const evalDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] = [
|
|
{ label: "杂交组合", prop: "combination_name" },
|
|
{ label: "单株", prop: "tree_name" },
|
|
{ label: "评价年份", prop: "evaluate_year" },
|
|
{ label: "评价日期", prop: "evaluate_date" },
|
|
{ label: "综合评分", prop: "overall_score" },
|
|
{ label: "评价人", prop: "bre_personnel_name" },
|
|
{ label: "备注", prop: "remark" },
|
|
];
|
|
|
|
const evalFormItems = computed<FormItem[]>(() => [
|
|
{
|
|
key: "combination_id",
|
|
label: "杂交组合",
|
|
type: "select",
|
|
props: {
|
|
placeholder: "请选择杂交组合",
|
|
options: cross_combinationOptions.value,
|
|
clearable: true,
|
|
filterable: true,
|
|
},
|
|
span: 12,
|
|
},
|
|
{
|
|
key: "tree_id",
|
|
label: "单株",
|
|
type: "select",
|
|
props: {
|
|
placeholder: "请选择单株",
|
|
options: treeOptions.value,
|
|
clearable: true,
|
|
filterable: true,
|
|
},
|
|
span: 12,
|
|
},
|
|
{
|
|
key: "evaluate_date",
|
|
label: "评价日期",
|
|
type: "date",
|
|
props: { type: "date", placeholder: "请选择评价日期" },
|
|
span: 12,
|
|
},
|
|
{
|
|
key: "evaluate_year",
|
|
label: "评价年份",
|
|
type: "number",
|
|
props: { placeholder: "请输入评价年份" },
|
|
span: 12,
|
|
},
|
|
{
|
|
key: "bre_personnel_id",
|
|
label: "评价人",
|
|
type: "select",
|
|
props: {
|
|
placeholder: "请选择评价人",
|
|
options: personnelOptions.value,
|
|
clearable: true,
|
|
filterable: true,
|
|
},
|
|
span: 12,
|
|
},
|
|
{
|
|
key: "overall_score",
|
|
label: "综合评分",
|
|
type: "number",
|
|
props: { placeholder: "请输入综合评分", precision: 1 },
|
|
span: 12,
|
|
},
|
|
{
|
|
key: "remark",
|
|
label: "备注",
|
|
type: "input",
|
|
props: { type: "textarea", rows: 3, placeholder: "请输入备注" },
|
|
span: 24,
|
|
},
|
|
]);
|
|
|
|
const formData = ref<TreeEvaluationForm>(createInitialFormData());
|
|
|
|
const rules = reactive({
|
|
combination_id: [{ required: true, message: "请选择杂交组合", trigger: "change" }],
|
|
tree_id: [{ required: true, message: "请选择单株", trigger: "change" }],
|
|
});
|
|
|
|
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
|
|
const TreeEvaluationFormRenderKey = ref(0);
|
|
|
|
const { importVisible, exportVisible, openImport, openExport } = useImportExport();
|
|
|
|
const handleSearch = async (params: TreeEvaluationSearchFormParams) => {
|
|
await searchBarRef.value?.validate();
|
|
replaceSearchParams({
|
|
combination_id: params.combination_id,
|
|
tree_id: params.tree_id,
|
|
evaluate_year: params.evaluate_year,
|
|
bre_personnel_id: params.bre_personnel_id,
|
|
created_id: params.created_id ?? undefined,
|
|
updated_id: params.updated_id ?? undefined,
|
|
created_time:
|
|
Array.isArray(params.created_time) && params.created_time.length === 2
|
|
? params.created_time
|
|
: undefined,
|
|
updated_time:
|
|
Array.isArray(params.updated_time) && params.updated_time.length === 2
|
|
? params.updated_time
|
|
: undefined,
|
|
} as Record<string, unknown>);
|
|
await getData();
|
|
};
|
|
|
|
const onResetSearch = async () => {
|
|
searchForm.value = {
|
|
combination_id: undefined,
|
|
tree_id: undefined,
|
|
evaluate_year: undefined,
|
|
bre_personnel_id: undefined,
|
|
created_id: undefined,
|
|
updated_id: undefined,
|
|
created_time: [],
|
|
updated_time: [],
|
|
};
|
|
await resetSearchParams();
|
|
};
|
|
|
|
function buildTreeEvaluationRowActions(row: TreeEvaluationTable): TableOperationAction[] {
|
|
const all: TableOperationAction[] = [
|
|
{
|
|
key: "detail",
|
|
label: "详情",
|
|
artType: "view",
|
|
perm: "module_bre:tree_evaluation:detail",
|
|
run: () => void openDialog("detail", row.id),
|
|
},
|
|
{
|
|
key: "edit",
|
|
label: "编辑",
|
|
artType: "edit",
|
|
icon: "ri:edit-2-line",
|
|
perm: "module_bre:tree_evaluation:update",
|
|
run: () => void openDialog("update", row.id),
|
|
},
|
|
{
|
|
key: "delete",
|
|
label: "删除",
|
|
artType: "delete",
|
|
icon: "ri:delete-bin-4-line",
|
|
perm: "module_bre:tree_evaluation:delete",
|
|
run: () => deleteTreeEvaluationRow(row),
|
|
},
|
|
];
|
|
return all;
|
|
}
|
|
|
|
function formatTreeEvaluationOperationCell(row: TreeEvaluationTable) {
|
|
return renderTableOperationCell(buildTreeEvaluationRowActions(row), {
|
|
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1",
|
|
});
|
|
}
|
|
|
|
async function openDialog(type: "create" | "update" | "detail", id?: number) {
|
|
activeTab.value = "基本信息";
|
|
TreeEvaluationFormRenderKey.value += 1;
|
|
formData.value = createInitialFormData();
|
|
resetObservations();
|
|
treeDetailData.value = null;
|
|
await loadTraits();
|
|
if (type === "detail" || type === "update") {
|
|
if (id == null) return;
|
|
try {
|
|
const res = await TreeEvaluationAPI.getTreeEvaluationDetail(id);
|
|
const d = res.data.data as TreeEvaluationTable;
|
|
formData.value = { ...createInitialFormData(), ...d };
|
|
if (d.tree_id) await loadTreeDetail(d.tree_id);
|
|
await loadObservations(d.id!);
|
|
} catch (error: unknown) {
|
|
if (import.meta.env.DEV) console.error("[TreeEvaluation] openDialog", error);
|
|
}
|
|
}
|
|
dialogVisible.type = type;
|
|
dialogVisible.title =
|
|
type === "create" ? "新增单株评价" : type === "update" ? "修改单株评价" : "单株评价详情";
|
|
dialogVisible.visible = true;
|
|
}
|
|
|
|
function handleCloseDialog() {
|
|
dialogVisible.visible = false;
|
|
}
|
|
|
|
async function handleAdd() {
|
|
createLoading.value = true;
|
|
try {
|
|
await openDialog("create");
|
|
} finally {
|
|
createLoading.value = false;
|
|
}
|
|
}
|
|
|
|
function buildObservationPayload(evaluationId: number) {
|
|
const f = formData.value;
|
|
const payload: IObject[] = [];
|
|
for (const cat of TRAIT_TABS) {
|
|
for (const trait of traitsByCategory[cat] ?? []) {
|
|
if (trait.id == null) continue;
|
|
const o = observations[trait.id];
|
|
if (!o) continue;
|
|
const numeric = trait.data_type === "numeric" ? (o.value_numeric ?? null) : null;
|
|
const text = trait.data_type === "categorical" ? (o.value_text ?? null) : null;
|
|
const date = trait.data_type === "date" ? (o.value_date ?? null) : null;
|
|
if (numeric == null && !text && !date) continue;
|
|
payload.push({
|
|
evaluation_id: evaluationId,
|
|
tree_id: f.tree_id,
|
|
combination_id: f.combination_id,
|
|
trait_id: trait.id,
|
|
evaluate_year: f.evaluate_year ?? undefined,
|
|
value_numeric: numeric ?? undefined,
|
|
value_text: text ?? undefined,
|
|
value_date: date ?? undefined,
|
|
remark: o.remark ?? undefined,
|
|
});
|
|
}
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
async function syncObservations(evaluationId: number) {
|
|
const payload = buildObservationPayload(evaluationId);
|
|
if (dialogVisible.type === "update") {
|
|
// 更新:先删除该评价已有观测,再重建
|
|
const existing = await TraitObservationAPI.getTraitObservationList({
|
|
evaluation_id: evaluationId,
|
|
page_no: 1,
|
|
page_size: 500,
|
|
});
|
|
const ids = (existing.data.data.list ?? []).map((o) => o.id).filter((x): x is number => x != null);
|
|
if (ids.length) await TraitObservationAPI.deleteTraitObservation(ids);
|
|
}
|
|
for (const item of payload) {
|
|
await TraitObservationAPI.createTraitObservation(item as IObject);
|
|
}
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
if (dialogVisible.type === "detail") {
|
|
handleCloseDialog();
|
|
return;
|
|
}
|
|
try {
|
|
await dataFormRef.value?.validate();
|
|
} catch {
|
|
return;
|
|
}
|
|
const f = formData.value;
|
|
if (!f.combination_id || !f.tree_id) {
|
|
ElMessage.warning("请选择杂交组合与单株");
|
|
return;
|
|
}
|
|
if (!f.evaluate_year && f.evaluate_date) {
|
|
f.evaluate_year = new Date(f.evaluate_date).getFullYear();
|
|
}
|
|
submitLoading.value = true;
|
|
try {
|
|
let evaluationId: number;
|
|
if (dialogVisible.type === "create") {
|
|
const res = await TreeEvaluationAPI.createTreeEvaluation(f);
|
|
evaluationId = (res.data.data as { id?: number })?.id ?? 0;
|
|
} else {
|
|
if (f.id == null) throw new Error("缺少评价ID");
|
|
await TreeEvaluationAPI.updateTreeEvaluation(f.id, f);
|
|
evaluationId = f.id;
|
|
}
|
|
await syncObservations(evaluationId);
|
|
ElMessage.success("保存成功");
|
|
handleCloseDialog();
|
|
if (dialogVisible.type === "create") await refreshCreate();
|
|
else await refreshUpdate();
|
|
} catch (error: unknown) {
|
|
if (import.meta.env.DEV) console.error("[TreeEvaluation] handleSubmit", error);
|
|
} finally {
|
|
submitLoading.value = false;
|
|
}
|
|
}
|
|
|
|
const deleteTreeEvaluationRow = async (row: TreeEvaluationTable) => {
|
|
if (!row.id) return;
|
|
try {
|
|
await confirmDelete(`确定删除「${row.id}」吗?此操作不可恢复!`);
|
|
await TreeEvaluationAPI.deleteTreeEvaluation([row.id!]);
|
|
faTableRef.value?.elTableRef?.clearSelection();
|
|
await refreshRemove();
|
|
} catch {
|
|
// 用户取消
|
|
}
|
|
};
|
|
|
|
async function handleBatchDelete() {
|
|
const ids = selectedIds.value;
|
|
if (ids.length === 0) return;
|
|
try {
|
|
await confirmBatchDelete(ids.length);
|
|
batchDeleting.value = true;
|
|
await TreeEvaluationAPI.deleteTreeEvaluation(ids);
|
|
faTableRef.value?.elTableRef?.clearSelection();
|
|
await refreshRemove();
|
|
} catch {
|
|
// 用户取消
|
|
} finally {
|
|
batchDeleting.value = false;
|
|
}
|
|
}
|
|
|
|
async function handleCrudImportUpload(uploadFormData: FormData) {
|
|
try {
|
|
const res = await TreeEvaluationAPI.importTreeEvaluation(uploadFormData);
|
|
if (res.data.code === ResultEnum.SUCCESS) {
|
|
ElMessage.success(res.data.msg || "导入成功");
|
|
importVisible.value = false;
|
|
await refreshData();
|
|
}
|
|
} catch (error: unknown) {
|
|
if (import.meta.env.DEV) console.error("[Import]", error);
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => formData.value.tree_id,
|
|
(id) => {
|
|
if (id) void loadTreeDetail(id);
|
|
else treeDetailData.value = null;
|
|
},
|
|
);
|
|
|
|
onMounted(() => {
|
|
loadCrossCombinationOptions();
|
|
loadTreeOptions();
|
|
loadPersonnelOptions();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.tree-basic {
|
|
margin-bottom: 16px;
|
|
}
|
|
.tree-basic-tip {
|
|
margin-bottom: 16px;
|
|
}
|
|
.eval-form-wrap {
|
|
border-top: 1px solid var(--el-border-color-lighter);
|
|
padding-top: 12px;
|
|
}
|
|
.trait-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 12px 24px;
|
|
padding: 12px 4px;
|
|
}
|
|
.trait-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
.trait-label {
|
|
width: 130px;
|
|
flex-shrink: 0;
|
|
font-size: 13px;
|
|
color: var(--el-text-color-regular);
|
|
text-align: right;
|
|
}
|
|
.trait-unit {
|
|
color: var(--el-text-color-secondary);
|
|
font-size: 12px;
|
|
}
|
|
.trait-value {
|
|
flex: 1;
|
|
}
|
|
.trait-readonly {
|
|
color: var(--el-text-color-primary);
|
|
}
|
|
.eval-tabs :deep(.el-tabs__content) {
|
|
max-height: 62vh;
|
|
overflow-y: auto;
|
|
}
|
|
</style>
|