794 lines
30 KiB
Python
794 lines
30 KiB
Python
# -*- 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()
|