# -*- coding: utf-8 -*- """生成 breeding 剩余 10 个模块的前端文件。 运行: python _gen_breeding_fe.py 依赖: _gen_specs.py 产出: frontend/web/src/api/module_bre/.ts frontend/web/src/views/module_bre//index.vue """ import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _gen_specs import MOD, cls_of # noqa: E402 FE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "frontend", "web", "src") API_DIR = os.path.join(FE, "api", "module_bre") VIEW_DIR = os.path.join(FE, "views", "module_bre") def ts_type(f): if f["type"] in ("int", "float", "fk"): return "number" return "string" def fk_out_name(field): return field[:-3] + "_name" def disp_field(m): # 删除确认时展示的字段 names = ("target_name", "combination_code", "name", "plot_code", "cultivar_name") for f in m["fields"]: if f["name"] in names: return f["name"] for f in m["fields"]: if f["type"] in ("str", "text", "select", "date") and f["req"]: return f["name"] return "id" # ── api/.ts ──────────────────────────────────────────────────────── def gen_api_ts(m): cls = cls_of(m["domain"]) domain = m["domain"] searchable = [f for f in m["fields"] if f["search"]] L = [] L.append('import { request } from "@utils";') L.append("") L.append('const API_PATH = "/bre/%s";' % domain) L.append("") L.append("const %sAPI = {" % cls) L.append(" get%sList(query: %sPageQuery) {" % (cls, cls)) L.append(" return request>>({" % cls) L.append(' url: `${API_PATH}/list`,') L.append(' method: "get",') L.append(" params: query,") L.append(" });") L.append(" },") L.append("") L.append(" get%sDetail(query: number) {" % cls) L.append(" return request>({" % cls) L.append(' url: `${API_PATH}/detail/${query}`,') L.append(' method: "get",') L.append(" });") L.append(" },") L.append("") L.append(" get%sOptions() {" % cls) L.append(" return request>({" % cls) L.append(' url: `${API_PATH}/options`,') L.append(' method: "get",') L.append(" });") L.append(" },") L.append("") L.append(" create%s(body: %sForm) {" % (cls, cls)) L.append(" return request({") L.append(' url: `${API_PATH}/create`,') L.append(' method: "post",') L.append(" data: body,") L.append(" });") L.append(" },") L.append("") L.append(" update%s(id: number, body: %sForm) {" % (cls, cls)) L.append(" return request({") L.append(' url: `${API_PATH}/update/${id}`,') L.append(' method: "put",') L.append(" data: body,") L.append(" });") L.append(" },") L.append("") L.append(" delete%s(body: number[]) {" % cls) L.append(" return request({") L.append(' url: `${API_PATH}/delete`,') L.append(' method: "delete",') L.append(" data: body,") L.append(" });") L.append(" },") L.append("") L.append(" export%s(body: %sPageQuery) {" % (cls, cls)) L.append(" return request({") L.append(' url: `${API_PATH}/export`,') L.append(' method: "post",') L.append(" data: body,") L.append(' responseType: "blob",') L.append(" });") L.append(" },") L.append("") L.append(" downloadTemplate%s() {" % cls) L.append(" return request({") L.append(' url: `${API_PATH}/download/template`,') L.append(' method: "post",') L.append(' responseType: "blob",') L.append(" });") L.append(" },") L.append("") L.append(" import%s(body: FormData) {" % cls) L.append(" return request({") L.append(' url: `${API_PATH}/import`,') L.append(' method: "post",') L.append(" data: body,") L.append(' headers: {') L.append(' "Content-Type": "multipart/form-data",') L.append(" },") L.append(" });") L.append(" },") L.append("};") L.append("") L.append("export default %sAPI;" % cls) L.append("") L.append("export interface %sPageQuery extends PageQuery, UserByQueryParams {" % cls) for f in searchable: L.append(" %s?: %s;" % (f["name"], ts_type(f))) L.append("}") L.append("") L.append("export interface %sOption {" % cls) L.append(" value: number;") L.append(" label: string;") L.append("}") L.append("") L.append("export interface %sTable extends BaseType {" % cls) for f in m["fields"]: if f["type"] == "fk": L.append(" %s?: number;" % f["name"]) L.append(" %s?: string;" % fk_out_name(f["name"])) else: L.append(" %s?: %s;" % (f["name"], ts_type(f))) L.append("}") L.append("") L.append("export interface %sForm extends BaseFormType {" % cls) for f in m["fields"]: L.append(" %s?: %s;" % (f["name"], ts_type(f))) L.append("}") return "\n".join(L) # ── views//index.vue ─────────────────────────────────────────────── def gen_vue_template(m): cls = cls_of(m["domain"]) domain = m["domain"] t = ''' ''' return t.replace("__CLS__", cls).replace("__DOMAIN__", domain) def gen_vue_script(m): cls = cls_of(m["domain"]) domain = m["domain"] title = m["title"] fields = m["fields"] fks = [f for f in fields if f["type"] == "fk"] reqs = [f for f in fields if f["req"]] searchable = [f for f in fields if f["search"]] table_fields = [f for f in fields if f["table"]] L = [] # imports L.append('import type { TableOperationAction } from "@/utils/table";') L.append('import { renderTableOperationCell, stripPaginationParams, toCrudCols } from "@utils";') L.append('import { useCrudForm } from "@/hooks/core/useCrudForm";') L.append('import { ResultEnum } from "@/enums/api/result.enum";') L.append('import type { IContentConfig, IObject } from "@/components/modal/types";') L.append('import type { AuditSearchFormParams } from "@/components/forms/fa-search-bar/auditSearchFormItems";') L.append('import type { FormItem } from "@/components/forms/fa-form/index.vue";') seen = set() for f in fks: rd = f["fk"][0] if rd not in seen: seen.add(rd) L.append('import %sAPI, { type %sOption } from "@/api/module_bre/%s";' % (cls_of(rd), cls_of(rd), rd)) L.append('import %sAPI, { type %sForm, type %sPageQuery, type %sTable } from "@/api/module_bre/%s";' % (cls, cls, cls, cls, domain)) L.append('import type { ColumnOption } from "@/types/component";') L.append('import FaDescriptions from "@/components/others/fa-descriptions/index.vue";') L.append('import FaForm from "@/components/forms/fa-form/index.vue";') L.append('import FaTableHeader from "@/components/tables/fa-table-header/index.vue";') L.append("") L.append("defineOptions({") L.append(" name: \"%s\"," % cls) L.append(" inheritAttrs: false,") L.append("});") L.append("") # fk options refs / maps / loaders (按 ref 域去重) ref_domains = [] for f in fks: if f["fk"][0] not in ref_domains: ref_domains.append(f["fk"][0]) for rd in ref_domains: rcls = cls_of(rd) L.append("const %sOptions = ref<%sOption[]>([]);" % (rd, rcls)) L.append("") for rd in ref_domains: L.append("const %sOptionsMap = computed(() => {" % rd) L.append(" const m: Record = {};") L.append(" for (const o of %sOptions.value) m[o.value] = o.label;" % rd) L.append(" return m;") L.append("});") L.append("") for rd in ref_domains: rcls = cls_of(rd) L.append("async function load%sOptions() {" % rcls) L.append(" try {") L.append(" const res = await %sAPI.get%sOptions();" % (rcls, rcls)) L.append(" if (res.data.code === ResultEnum.SUCCESS) {") L.append(" %sOptions.value = res.data.data ?? [];" % rd) L.append(" }") L.append(" } catch (error: unknown) {") L.append(' if (import.meta.env.DEV) console.error("[%s] load%sOptions", error);' % (cls, rcls)) L.append(" }") L.append("}") L.append("") # initial form data L.append("const createInitialFormData = (): %sForm => ({" % cls) L.append(" id: undefined,") for f in fields: L.append(" %s: undefined," % f["name"]) L.append("});") L.append("") # searchForm type + ref L.append("type %sSearchFormParams = {" % cls) for f in searchable: L.append(" %s?: %s;" % (f["name"], ts_type(f))) L.append("} & AuditSearchFormParams;") L.append("") L.append("const searchForm = ref<%sSearchFormParams>({" % cls) for f in searchable: L.append(" %s: undefined," % f["name"]) L.append(" created_id: undefined,") L.append(" updated_id: undefined,") L.append(" created_time: [],") L.append(" updated_time: [],") L.append("});") L.append("") L.append("const showSearchBar = ref(true);") L.append("const searchBarRef = ref<{ validate: () => Promise } | null>(null);") L.append("const searchBarRules: Record = {};") L.append("") # search items computed L.append("const %sSearchItems = computed(() => [" % cls) for f in searchable: if f["type"] == "fk": rd = f["fk"][0] L.append(" {") L.append(' label: "%s",' % f["label"]) L.append(' key: "%s",' % f["name"]) L.append(' type: "select",') L.append(" props: {") L.append(' placeholder: "请选择%s",' % f["label"]) L.append(" options: %sOptions.value," % rd) L.append(" clearable: true,") L.append(" filterable: true,") L.append(" },") L.append(" span: 8,") L.append(" },") elif f["type"] == "select": opts = ", ".join('{ label: "%s", value: "%s" }' % (o[1], o[0]) for o in f["options"]) L.append(" {") L.append(' label: "%s",' % f["label"]) L.append(' key: "%s",' % f["name"]) L.append(' type: "radiogroup",') L.append(" props: {") L.append(' placeholder: "请选择%s",' % f["label"]) L.append(" options: [%s]," % opts) L.append(" clearable: true,") L.append(" },") L.append(" span: 8,") L.append(" },") elif f["type"] == "date": L.append(" {") L.append(' label: "%s",' % f["label"]) L.append(' key: "%s",' % f["name"]) L.append(' type: "date",') L.append(" props: {") L.append(' type: "date",') L.append(' placeholder: "请选择%s",' % f["label"]) L.append(" },") L.append(" span: 8,") L.append(" },") else: L.append(" {") L.append(' label: "%s",' % f["label"]) L.append(' key: "%s",' % f["name"]) L.append(' type: "input",') L.append(' placeholder: "请输入%s",' % f["label"]) L.append(" clearable: true,") L.append(" span: 8,") L.append(" },") L.append("]);") L.append("") # table hooks L.append("const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);") L.append("const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } = useTableSelection<%sTable>();" % cls) L.append("") L.append("const createLoading = ref(false);") L.append("") L.append("const {") L.append(" columns,") L.append(" columnChecks,") L.append(" data,") L.append(" loading,") L.append(" pagination,") L.append(" searchParams,") L.append(" getData,") L.append(" replaceSearchParams,") L.append(" resetSearchParams,") L.append(" handleSizeChange,") L.append(" handleCurrentChange,") L.append(" refreshData,") L.append(" refreshCreate,") L.append(" refreshUpdate,") L.append(" refreshRemove,") L.append("} = useTable({") L.append(" core: {") L.append(" apiFn: %sAPI.get%sList," % (cls, cls)) L.append(" apiParams: {") L.append(" page_no: 1,") L.append(" page_size: 10,") L.append(" },") L.append(" columnsFactory: (): ColumnOption<%sTable>[] => [" % cls) L.append(' { type: "globalIndex", width: 56, label: "序号" },') L.append(' { type: "selection", width: 48, fixed: "left" },') for f in table_fields: if f["type"] == "fk": L.append(' { prop: "%s", label: "%s", minWidth: 140, showOverflowTooltip: true },' % (fk_out_name(f["name"]), f["label"])) elif f["type"] in ("int", "float"): L.append(' { prop: "%s", label: "%s", minWidth: 100 },' % (f["name"], f["label"])) else: L.append(' { prop: "%s", label: "%s", minWidth: 140, showOverflowTooltip: true },' % (f["name"], f["label"])) L.append(' {') L.append(' prop: "created_time",') L.append(' label: "创建时间",') L.append(" width: 168,") L.append(" sortable: true,") L.append(" showOverflowTooltip: true,") L.append(" },") L.append(" {") L.append(' prop: "updated_time",') L.append(' label: "更新时间",') L.append(" width: 168,") L.append(" sortable: true,") L.append(" showOverflowTooltip: true,") L.append(" },") L.append(" {") L.append(' prop: "created_by",') L.append(' label: "创建人",') L.append(" minWidth: 100,") L.append(" formatter: (row: %sTable) => row.created_by?.name ?? \"—\"," % cls) L.append(" },") L.append(" {") L.append(' prop: "updated_by",') L.append(' label: "更新人",') L.append(" minWidth: 100,") L.append(" formatter: (row: %sTable) => row.updated_by?.name ?? \"—\"," % cls) L.append(" },") L.append(" {") L.append(' prop: "operation",') L.append(' label: "操作",') L.append(" width: 180,") L.append(' fixed: "right",') L.append(' align: "center",') L.append(" formatter: (row: %sTable) => format%sOperationCell(row)," % (cls, cls)) L.append(" },") L.append(" ],") L.append(" },") L.append("});") L.append("") L.append("const %sCrudCols = toCrudCols(columns);" % cls) L.append("") L.append("const exportQueryParams = computed(() => {") L.append(" return stripPaginationParams(searchParams as Record);") L.append("});") L.append("") L.append("const %sImportContentConfig = computed(() => ({" % cls) L.append(' permPrefix: "module_bre:%s",' % domain) L.append(" cols: %sCrudCols.value," % cls) L.append(" indexAction: async () => ({}),") L.append(" importTemplate: () => %sAPI.downloadTemplate%s()," % (cls, cls)) L.append("}));") L.append("") L.append("const %sExportContentConfig = computed(() => ({" % cls) L.append(' permPrefix: "module_bre:%s",' % domain) L.append(" cols: %sCrudCols.value," % cls) L.append(" exportsBlobAction: async (params: IObject) => {") L.append(" const merged = {") L.append(" ...(exportQueryParams.value as unknown as Record),") L.append(" ...params,") L.append(" } as unknown as %sPageQuery;" % cls) L.append(" const res = await %sAPI.export%s(merged);" % (cls, cls)) L.append(" return res.data as Blob;") L.append(" },") L.append("}));") L.append("") L.append("const { dialogVisible } = useCrudDialog();") L.append("") L.append("const detailFormData = ref<%sTable>({});" % cls) L.append("") # detail items L.append("const %sDetailItems: import(\"@/components/others/fa-descriptions/index.vue\").DescriptionsItem[] = [" % cls) for f in fields: if f["type"] == "fk": L.append(' { label: "%s", prop: "%s" },' % (f["label"], fk_out_name(f["name"]))) else: L.append(' { label: "%s", prop: "%s" },' % (f["label"], f["name"])) L.append(' { label: "UUID", prop: "uuid" },') L.append(' { label: "创建人", prop: "created_by.name" },') L.append(' { label: "更新人", prop: "updated_by.name" },') L.append(' { label: "创建时间", prop: "created_time" },') L.append(' { label: "更新时间", prop: "updated_time" },') L.append("];") L.append("") # dialog form items (computed for reactive fk options) L.append("const %sDialogFormItems = computed(() => [" % cls) for f in fields: if f["type"] == "fk": rd = f["fk"][0] L.append(" {") L.append(' key: "%s",' % f["name"]) L.append(' label: "%s",' % f["label"]) L.append(' type: "select",') L.append(" props: {") L.append(' placeholder: "请选择%s",' % f["label"]) L.append(" options: %sOptions.value," % rd) L.append(" clearable: true,") L.append(" filterable: true,") L.append(" },") L.append(" span: 12,") L.append(" },") elif f["type"] == "select": opts = ", ".join('{ label: "%s", value: "%s" }' % (o[1], o[0]) for o in f["options"]) L.append(" {") L.append(' key: "%s",' % f["name"]) L.append(' label: "%s",' % f["label"]) L.append(' type: "radiogroup",') L.append(" props: {") L.append(' placeholder: "请选择%s",' % f["label"]) L.append(" options: [%s]," % opts) L.append(" clearable: true,") L.append(" },") L.append(" span: 12,") L.append(" },") elif f["type"] == "date": L.append(" {") L.append(' key: "%s",' % f["name"]) L.append(' label: "%s",' % f["label"]) L.append(' type: "date",') L.append(" props: {") L.append(' type: "date",') L.append(' placeholder: "请选择%s",' % f["label"]) L.append(" },") L.append(" span: 12,") L.append(" },") elif f["type"] in ("int", "float"): L.append(" {") L.append(' key: "%s",' % f["name"]) L.append(' label: "%s",' % f["label"]) L.append(' type: "number",') L.append(" props: { placeholder: \"请输入%s\" }," % f["label"]) L.append(" span: 12,") L.append(" },") elif f["type"] == "text": L.append(" {") L.append(' key: "%s",' % f["name"]) L.append(' label: "%s",' % f["label"]) L.append(' type: "input",') L.append(" props: { type: \"textarea\", rows: 3, placeholder: \"请输入%s\" }," % f["label"]) L.append(" span: 24,") L.append(" },") else: L.append(" {") L.append(' key: "%s",' % f["name"]) L.append(' label: "%s",' % f["label"]) L.append(' type: "input",') L.append(" props: { placeholder: \"请输入%s\", maxlength: 100 }," % f["label"]) L.append(" span: 12,") L.append(" },") L.append("]);") L.append("") # formData L.append("const formData = ref<%sForm>(createInitialFormData());" % cls) L.append("") # rules L.append("const rules = reactive({") for f in reqs: trig = "change" if f["type"] in ("fk", "select") else "blur" verb = "请选择" if f["type"] in ("fk", "select") else "请输入" L.append(' %s: [{ required: true, message: "%s%s", trigger: "%s" }],' % (f["name"], verb, f["label"], trig)) L.append("});") L.append("") L.append("const dataFormRef = ref | null>(null);") L.append("const %sFormRenderKey = ref(0);" % cls) L.append("") L.append("const crud = useCrudForm<%sForm>({" % cls) L.append(" formData,") L.append(" initialFormData: createInitialFormData(),") L.append(" dialogVisible,") L.append(" dataFormRef,") L.append(" formRenderKey: %sFormRenderKey," % cls) L.append(" detailApi: %sAPI.get%sDetail," % (cls, cls)) L.append(" createApi: %sAPI.create%s," % (cls, cls)) L.append(" updateApi: %sAPI.update%s," % (cls, cls)) L.append(" titles: { create: \"新增%s\", update: \"修改%s\", detail: \"%s详情\" }," % (title, title, title)) L.append(" detailFormData,") L.append(" onCreateSuccess: async () => {") L.append(" await refreshCreate();") L.append(" },") L.append(" onUpdateSuccess: async () => {") L.append(" await refreshUpdate();") L.append(" },") L.append("});") L.append("") L.append("const { submitLoading } = crud;") L.append("") L.append("const { importVisible, exportVisible, openImport, openExport } = useImportExport();") L.append("") # handleSearch L.append("const handleSearch = async (params: %sSearchFormParams) => {" % cls) L.append(" await searchBarRef.value?.validate();") L.append(" replaceSearchParams({") for f in searchable: L.append(" %s: params.%s," % (f["name"], f["name"])) L.append(" created_id: params.created_id ?? undefined,") L.append(" updated_id: params.updated_id ?? undefined,") L.append(" created_time:") L.append(" Array.isArray(params.created_time) && params.created_time.length === 2") L.append(" ? params.created_time") L.append(" : undefined,") L.append(" updated_time:") L.append(" Array.isArray(params.updated_time) && params.updated_time.length === 2") L.append(" ? params.updated_time") L.append(" : undefined,") L.append(" } as Record);") L.append(" await getData();") L.append("};") L.append("") # onResetSearch L.append("const onResetSearch = async () => {") L.append(" searchForm.value = {") for f in searchable: L.append(" %s: undefined," % f["name"]) L.append(" created_id: undefined,") L.append(" updated_id: undefined,") L.append(" created_time: [],") L.append(" updated_time: [],") L.append(" };") L.append(" await resetSearchParams();") L.append("};") L.append("") # row actions L.append("function build%sRowActions(row: %sTable): TableOperationAction[] {" % (cls, cls)) L.append(" const all: TableOperationAction[] = [") L.append(" {") L.append(' key: "detail",') L.append(' label: "详情",') L.append(' artType: "view",') L.append(' perm: "module_bre:%s:detail",' % domain) L.append(' run: () => void crud.handleOpenDialog("detail", row.id),') L.append(" },") L.append(" {") L.append(' key: "edit",') L.append(' label: "编辑",') L.append(' artType: "edit",') L.append(' icon: "ri:edit-2-line",') L.append(' perm: "module_bre:%s:update",' % domain) L.append(' run: () => void crud.handleOpenDialog("update", row.id),') L.append(" },") L.append(" {") L.append(' key: "delete",') L.append(' label: "删除",') L.append(' artType: "delete",') L.append(' icon: "ri:delete-bin-4-line",') L.append(' perm: "module_bre:%s:delete",' % domain) L.append(" run: () => delete%sRow(row)," % cls) L.append(" },") L.append(" ];") L.append(" return all;") L.append("}") L.append("") L.append("function format%sOperationCell(row: %sTable) {" % (cls, cls)) L.append(" return renderTableOperationCell(build%sRowActions(row), {" % cls) L.append(' wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1",') L.append(" });") L.append("}") L.append("") L.append("async function handleAdd() {") L.append(" createLoading.value = true;") L.append(" try {") L.append(' await crud.handleOpenDialog("create");') L.append(" } finally {") L.append(" createLoading.value = false;") L.append(" }") L.append("}") L.append("") L.append("const delete%sRow = async (row: %sTable) => {" % (cls, cls)) L.append(" if (!row.id) return;") L.append(' try {') disp = disp_field(m) L.append(' await confirmDelete(`确定删除「${row.%s ?? row.id}」吗?此操作不可恢复!`);' % disp) L.append(" await %sAPI.delete%s([row.id!]);" % (cls, cls)) L.append(" faTableRef.value?.elTableRef?.clearSelection();") L.append(" await refreshRemove();") L.append(" } catch {") L.append(" // 用户取消") L.append(" }") L.append("};") L.append("") L.append("async function handleBatchDelete() {") L.append(" const ids = selectedIds.value;") L.append(" if (ids.length === 0) return;") L.append(" try {") L.append(" await confirmBatchDelete(ids.length);") L.append(" batchDeleting.value = true;") L.append(" await %sAPI.delete%s(ids);" % (cls, cls)) L.append(" faTableRef.value?.elTableRef?.clearSelection();") L.append(" await refreshRemove();") L.append(" } catch {") L.append(" // 用户取消") L.append(" } finally {") L.append(" batchDeleting.value = false;") L.append(" }") L.append("}") L.append("") L.append("async function handleCrudImportUpload(uploadFormData: FormData) {") L.append(" try {") L.append(" const res = await %sAPI.import%s(uploadFormData);" % (cls, cls)) L.append(" if (res.data.code === ResultEnum.SUCCESS) {") L.append(' ElMessage.success(res.data.msg || "导入成功");') L.append(" importVisible.value = false;") L.append(" await refreshData();") L.append(" }") L.append(" } catch (error: unknown) {") L.append(' if (import.meta.env.DEV) console.error("[Import]", error);') L.append(" }") L.append("}") L.append("") L.append("onMounted(() => {") for f in fks: rcls = cls_of(f["fk"][0]) L.append(" load%sOptions();" % rcls) L.append("});") return "\n".join(L) def main(): for m in MOD: os.makedirs(os.path.join(API_DIR), exist_ok=True) with open(os.path.join(API_DIR, m["domain"] + ".ts"), "w", encoding="utf-8") as fh: fh.write(gen_api_ts(m) + "\n") vd = os.path.join(VIEW_DIR, m["domain"]) os.makedirs(vd, exist_ok=True) content = gen_vue_template(m) + '\n\n" with open(os.path.join(vd, "index.vue"), "w", encoding="utf-8") as fh: fh.write(content) print("generated frontend:", m["domain"]) if __name__ == "__main__": main()