Files
dpb/_gen_breeding_fe.py
T

794 lines
30 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""生成 breeding 剩余 10 个模块的前端文件。
运行: python _gen_breeding_fe.py
依赖: _gen_specs.py
产出:
frontend/web/src/api/module_bre/<domain>.ts
frontend/web/src/views/module_bre/<domain>/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/<domain>.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<ApiResponse<PageResult<%sTable>>>({" % 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<ApiResponse<%sTable>>({" % 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<ApiResponse<%sOption[]>>({" % 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<ApiResponse>({")
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<ApiResponse>({")
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<ApiResponse>({")
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<Blob>({")
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<Blob>({")
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<ApiResponse>({")
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/<domain>/index.vue ───────────────────────────────────────────────
def gen_vue_template(m):
cls = cls_of(m["domain"])
domain = m["domain"]
t = '''<template>
<div class="fa-full-height">
<FaSearchBar
v-show="showSearchBar"
ref="searchBarRef"
v-model="searchForm"
:items="__CLS__SearchItems"
: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:__DOMAIN__:create']"
:perm-import="['module_bre:__DOMAIN__:import']"
:perm-export="['module_bre:__DOMAIN__:export']"
:perm-delete="['module_bre:__DOMAIN__: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="920px"
dialog-class="crud-embed-dialog"
modal-class="crud-embed-dialog"
:form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
:close-on-click-modal="false"
@cancel="crud.handleCloseDialog"
@close="crud.handleCloseDialog"
@confirm="crud.handleSubmit"
>
<template v-if="dialogVisible.type === 'detail'">
<FaDescriptions
:column="4"
:data="detailFormData"
:items="__CLS__DetailItems"
max-height="70vh"
/>
</template>
<template v-else>
<FaForm
:key="__CLS__FormRenderKey"
scrollbar
max-height="70vh"
ref="dataFormRef"
v-model="formData"
:items="__CLS__DialogFormItems"
: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"
/>
</template>
</FaDialog>
<FaImportDialog
v-model="importVisible"
:content-config="__CLS__ImportContentConfig"
default-template-file-name="__DOMAIN___import_template.xlsx"
@upload="handleCrudImportUpload"
/>
<FaExportDialog
v-model="exportVisible"
:content-config="__CLS__ExportContentConfig"
:query-params="exportQueryParams"
:page-data="data"
:selection-data="selectedRows"
/>
</div>
</template>
'''
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<number, string> = {};")
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<boolean> } | null>(null);")
L.append("const searchBarRules: Record<string, unknown> = {};")
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<string, unknown>);")
L.append("});")
L.append("")
L.append("const %sImportContentConfig = computed<IContentConfig>(() => ({" % 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<string, unknown>),")
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<FormItem[]>(() => [" % 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<InstanceType<typeof FaForm> | 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<string, unknown>);")
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<script setup lang="ts">\n' + gen_vue_script(m) + "\n</script>\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()